From 6289978cd4806583cc6d03424e5f81a21012612b Mon Sep 17 00:00:00 2001 From: Codex Date: Fri, 29 May 2026 19:26:58 +0800 Subject: [PATCH] fix: list multiple device pod executors --- cmd/hwlab-device-pod/main.test.ts | 35 +++++++++++++++++++++++++++ cmd/hwlab-device-pod/main.ts | 40 ++++++++++++++++++++++--------- 2 files changed, 64 insertions(+), 11 deletions(-) diff --git a/cmd/hwlab-device-pod/main.test.ts b/cmd/hwlab-device-pod/main.test.ts index 6a083d44..b8a7cbcc 100644 --- a/cmd/hwlab-device-pod/main.test.ts +++ b/cmd/hwlab-device-pod/main.test.ts @@ -106,6 +106,41 @@ test("device pod executor exposes cloud-api authority boundary and method guards } }); +test("device pod executor lists and isolates multiple logical device pods", async () => { + const service = await startDevicePod("device-pod-main", { + HWLAB_DEVICE_POD_IDS: "device-pod-main, device-pod-alt" + }); + + try { + const list = await fetchJson(`http://127.0.0.1:${service.port}/v1/device-pods`); + assert.equal(list.selectedDevicePodId, "device-pod-main"); + assert.deepEqual(list.devicePods.map((pod) => pod.devicePodId), ["device-pod-main", "device-pod-alt"]); + assert.equal(list.devicePods[1].routes.internalJob, "/v1/device-pods/device-pod-alt/jobs"); + + const mainJobResponse = await fetch(`http://127.0.0.1:${service.port}/v1/device-pods/device-pod-main/jobs`, { + method: "POST", + headers: { "content-type": "application/json", ...internalHeaders() }, + body: JSON.stringify({ jobId: "job_multi_main", intent: "workspace.ls", traceId: "trc_multi_main", operationId: "op_multi_main" }) + }); + assert.equal(mainJobResponse.status, 409); + const altJobResponse = await fetch(`http://127.0.0.1:${service.port}/v1/device-pods/device-pod-alt/jobs`, { + method: "POST", + headers: { "content-type": "application/json", ...internalHeaders() }, + body: JSON.stringify({ jobId: "job_multi_alt", intent: "workspace.ls", traceId: "trc_multi_alt", operationId: "op_multi_alt" }) + }); + assert.equal(altJobResponse.status, 409); + + const mainJob = await fetchJson(`http://127.0.0.1:${service.port}/v1/device-pods/device-pod-main/jobs/job_multi_main`, { headers: internalHeaders() }); + assert.equal(mainJob.job.devicePodId, "device-pod-main"); + const altJob = await fetchJson(`http://127.0.0.1:${service.port}/v1/device-pods/device-pod-alt/jobs/job_multi_alt`, { headers: internalHeaders() }); + assert.equal(altJob.job.devicePodId, "device-pod-alt"); + const crossRead = await fetch(`http://127.0.0.1:${service.port}/v1/device-pods/device-pod-alt/jobs/job_multi_main`, { headers: internalHeaders() }); + assert.equal(crossRead.status, 404); + } finally { + await service.stop(); + } +}); + test("device-pod executor bounds gateway output text", async () => { const longText = "z".repeat(13000); const cloudApi = createServer(async (request, response) => { diff --git a/cmd/hwlab-device-pod/main.ts b/cmd/hwlab-device-pod/main.ts index dc6d490c..f22b4a7b 100644 --- a/cmd/hwlab-device-pod/main.ts +++ b/cmd/hwlab-device-pod/main.ts @@ -9,6 +9,7 @@ const CONTRACT_VERSION = "device-pod-executor-v1"; const DEFAULT_DEVICE_POD_ID = "device-pod-71-freq"; const port = parsePort(process.env.HWLAB_DEVICE_POD_PORT, parsePort(process.env.PORT, 7601)); const devicePodId = process.env.HWLAB_DEVICE_POD_ID || DEFAULT_DEVICE_POD_ID; +const devicePodIds = configuredDevicePodIds(process.env.HWLAB_DEVICE_POD_IDS, devicePodId); const environment = process.env.HWLAB_ENVIRONMENT || process.env.HWLAB_GITOPS_PROFILE || "dev"; const cloudApiInternalUrl = normalizeBaseUrl(process.env.HWLAB_CLOUD_API_INTERNAL_URL || process.env.HWLAB_CLOUD_API_URL); const internalToken = textOr(process.env.HWLAB_DEVICE_POD_INTERNAL_TOKEN, ""); @@ -70,6 +71,7 @@ function healthPayload() { ready: true, contractVersion: CONTRACT_VERSION, devicePodId, + devicePodIds, observedAt: new Date().toISOString(), role: "device-pod-internal-executor", authority: "hwlab-cloud-api", @@ -88,17 +90,21 @@ function listPayload() { status: "ok", source: sourcePayload(), selectedDevicePodId: devicePodId, - devicePods: [{ - devicePodId, - status: "executor-ready", - authority: "hwlab-cloud-api", - fake: false, - gatewayAdapter: gatewayAdapterState(), - routes: { - cloudAuthority: "/v1/device-pods", - internalJob: `/v1/device-pods/${encodeURIComponent(devicePodId)}/jobs` - } - }] + devicePods: devicePodIds.map((id) => executorDevicePodSummary(id)) + }; +} + +function executorDevicePodSummary(id) { + return { + devicePodId: id, + status: "executor-ready", + authority: "hwlab-cloud-api", + fake: false, + gatewayAdapter: gatewayAdapterState(), + routes: { + cloudAuthority: "/v1/device-pods", + internalJob: `/v1/device-pods/${encodeURIComponent(id)}/jobs` + } }; } @@ -513,6 +519,18 @@ function normalizeBaseUrl(value) { return text ? text.replace(/\/+$/u, "") : ""; } +function configuredDevicePodIds(value, fallbackId) { + const ids = String(value ?? "") + .split(",") + .map((item) => item.trim()) + .filter(Boolean); + const unique = []; + for (const id of [fallbackId, ...ids]) { + if (!unique.includes(id)) unique.push(id); + } + return unique; +} + function jobOutputPayload(job) { return { ...jobPayload(job),