From bc059fa49dd3ec7a0182ecbe6305d8afd2c6bdc9 Mon Sep 17 00:00:00 2001 From: Codex Agent Date: Thu, 4 Jun 2026 10:36:11 +0800 Subject: [PATCH] fix(tools): v0.2 device-pod-cli maps build/download evidence to first-class workspace.evidence/debug.evidence (HWLAB #801) The CLI was emitting workspace.build/debug.download with action=evidence for ":workspace build evidence " and ":debug-probe download evidence ". Cloud-api accepted the sub-action form, but the device-pod executor mapped it to device-host-cli argv as "workspace build evidence" / "debug-probe download evidence" -- neither of which exists in the host CLI dispatcher; the host returned "unsupported command: --profile-json-b64 ..." and the evidence job was marked failed with blocker.code gateway_dispatch_failed. Per docs/reference/g14.md (v0.2 device-pod cloud-api architecture), "The evidence sub-action on workspace.evidence / debug.evidence is a first-class intent, not a workspace.build sub-action." The executor's deviceHostArgs already produces the correct argv for the first-class intent, so the fix lives in the CLI selector parser: when operation === "build" and rest[1] === "evidence", emit intent=workspace.evidence with kind=build and the explicit jobId; the same shape for operation === "download" -> debug.evidence with kind=download. Non-evidence sub-actions (start, status, wait, output, cancel) are unchanged. Live verification on G14 against http://74.48.78.17:19667 (D601-F103-V2, profileHash sha256:0352cd14...): - before: device-host-cli argv = "workspace build evidence" -> gateway_dispatch_failed - after: device-host-cli argv = "workspace evidence build ..." accepted by cloud-api with status=running, blocker=null - the executor argv proves the fix; the D601 Windows host F:\Work\ConStart\tools\device-host-cli.mjs is still a stale D601 ops-side copy and is tracked separately in #779 (out of scope for #801). Adds: - device-pod-cli.test.ts: evidence sub-action -> first-class intent - device-pod-cli.test.ts: build start / download start regression (non-evidence sub-actions keep workspace.build / debug.download) Out of scope: COM9 -> COM3 UART port drift on the Windows device pod host (separate issue from #801; no UART/profile port code touched). --- tools/device-pod-cli.test.ts | 81 +++++++++++++++++++++++++++++++++ tools/src/device-pod-cli-lib.ts | 46 ++++++++++++------- 2 files changed, 111 insertions(+), 16 deletions(-) diff --git a/tools/device-pod-cli.test.ts b/tools/device-pod-cli.test.ts index c187b550..373bfe5c 100644 --- a/tools/device-pod-cli.test.ts +++ b/tools/device-pod-cli.test.ts @@ -462,6 +462,87 @@ test("device-pod-cli forwards host build and download job ids as named device-po }); }); +test("device-pod-cli maps build evidence / download evidence to first-class workspace.evidence / debug.evidence intents (HWLAB #801)", async () => { + const seen: any[] = []; + const fetchImpl = async (url, init) => { + seen.push({ url: String(url), body: JSON.parse(String(init?.body ?? "{}")) }); + return jsonResponse(202, { ok: true, status: "running", job: { id: `job_${seen.length}`, status: "running" } }); + }; + const build = await runAssembledDevicePodCli([ + "D601-F103-V2:workspace:/projects/01_baseline", + "build", + "evidence", + "job_devicepod_7ce895d1-bb3e-4daa-b4a1-6d7d05522923", + "--tail", + "60" + ], { fetchImpl, now: () => "2026-06-04T00:00:00.000Z" }); + assert.equal(build.exitCode, 0); + assert.deepEqual(seen[0].body, { + intent: "workspace.evidence", + args: { + kind: "build", + jobId: "job_devicepod_7ce895d1-bb3e-4daa-b4a1-6d7d05522923", + tail: 60 + } + }); + + const download = await runAssembledDevicePodCli([ + "D601-F103-V2:debug-probe", + "download", + "evidence", + "job_devicepod_fb23ec52-339c-45cb-9b82-1af1fb7b6ee0", + "--full" + ], { fetchImpl, now: () => "2026-06-04T00:00:00.000Z" }); + assert.equal(download.exitCode, 0); + assert.deepEqual(seen[1].body, { + intent: "debug.evidence", + args: { + kind: "download", + jobId: "job_devicepod_fb23ec52-339c-45cb-9b82-1af1fb7b6ee0", + full: true + } + }); +}); + +test("device-pod-cli keeps build start / download start on workspace.build / debug.download intents (HWLAB #801 regression)", async () => { + const seen: any[] = []; + const fetchImpl = async (url, init) => { + seen.push({ url: String(url), body: JSON.parse(String(init?.body ?? "{}")) }); + return jsonResponse(202, { ok: true, status: "running", job: { id: `job_${seen.length}`, status: "running" } }); + }; + const build = await runAssembledDevicePodCli([ + "D601-F103-V2:workspace:/projects/01_baseline", + "build", + "start", + "--reason", + "smoke test" + ], { fetchImpl, now: () => "2026-06-04T00:00:00.000Z" }); + assert.equal(build.exitCode, 0); + assert.deepEqual(seen[0].body, { + intent: "workspace.build", + args: { action: "start" }, + reason: "smoke test" + }); + + const download = await runAssembledDevicePodCli([ + "D601-F103-V2:debug-probe", + "download", + "start", + "--capture-uart", + "uart/1", + "--capture-duration-ms", + "8000", + "--reason", + "smoke test" + ], { fetchImpl, now: () => "2026-06-04T00:00:00.000Z" }); + assert.equal(download.exitCode, 0); + assert.deepEqual(seen[1].body, { + intent: "debug.download", + args: { action: "start", captureUart: "uart/1", captureDurationMs: 8000 }, + reason: "smoke test" + }); +}); + test("device-pod-cli maps selector build/download cloud job aliases to job routes", async () => { const seen: any[] = []; const fetchImpl = async (url, init) => { diff --git a/tools/src/device-pod-cli-lib.ts b/tools/src/device-pod-cli-lib.ts index 00e45ad1..d3ab2eb0 100644 --- a/tools/src/device-pod-cli-lib.ts +++ b/tools/src/device-pod-cli-lib.ts @@ -296,10 +296,17 @@ async function buildJob(selector: any, operation: string, rest: string[], parsed Object.assign(args, passthroughOptions(parsed, ["encoding", "charset", "createDirs"])); } else if (operation === "build") { - intent = "workspace.build"; - args.action = rest[1] || "start"; - if (["status", "wait"].includes(String(args.action))) args.jobId = text(rest[2] ?? parsed.jobId); - Object.assign(args, passthroughOptions(parsed, ["target", "timeoutMs", "clean", "dryRun"])); + if (text(rest[1]) === "evidence") { + intent = "workspace.evidence"; + args.kind = "build"; + args.jobId = text(rest[2] ?? parsed.jobId); + Object.assign(args, passthroughOptions(parsed, ["tail", "full", "target"])); + } else { + intent = "workspace.build"; + args.action = rest[1] || "start"; + if (["status", "wait"].includes(String(args.action))) args.jobId = text(rest[2] ?? parsed.jobId); + Object.assign(args, passthroughOptions(parsed, ["target", "timeoutMs", "clean", "dryRun"])); + } } else throw cliError("unsupported_workspace_operation", `unsupported workspace operation: ${operation}`); } else if (selector.surface === "debug-probe") { @@ -312,18 +319,25 @@ async function buildJob(selector: any, operation: string, rest: string[], parsed ); } else if (operation === "download") { - intent = "debug.download"; - args.action = rest[1] || "start"; - if (["status", "wait"].includes(String(args.action))) args.jobId = text(rest[2] ?? parsed.jobId); - Object.assign(args, passthroughOptions(parsed, [ - "target", - "timeoutMs", - "captureUart", - "captureDurationMs", - "durationMs", - "port", - "baudRate" - ])); + if (text(rest[1]) === "evidence") { + intent = "debug.evidence"; + args.kind = "download"; + args.jobId = text(rest[2] ?? parsed.jobId); + Object.assign(args, passthroughOptions(parsed, ["tail", "full", "target"])); + } else { + intent = "debug.download"; + args.action = rest[1] || "start"; + if (["status", "wait"].includes(String(args.action))) args.jobId = text(rest[2] ?? parsed.jobId); + Object.assign(args, passthroughOptions(parsed, [ + "target", + "timeoutMs", + "captureUart", + "captureDurationMs", + "durationMs", + "port", + "baudRate" + ])); + } } else if (operation === "reset") intent = "debug.reset"; else throw cliError(