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 "<pod>:workspace build evidence <jobId>" and "<pod>:debug-probe
download evidence <jobId>". 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 <jobId> ..."
            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).
This commit is contained in:
Codex Agent
2026-06-04 10:36:11 +08:00
parent f7731d1421
commit bc059fa49d
2 changed files with 111 additions and 16 deletions
+81
View File
@@ -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) => {
+30 -16
View File
@@ -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(