import assert from "node:assert/strict"; import { mkdtemp, mkdir, writeFile } from "node:fs/promises"; import { createServer } from "node:http"; import os from "node:os"; import path from "node:path"; import { test } from "bun:test"; import { runDevicePodCli } from "./src/device-pod-cli-lib.ts"; const ASSEMBLED_RUNTIME_ENV = { HWLAB_RUNTIME_API_URL: "http://cloud.test", HWLAB_RUNTIME_NAMESPACE: "hwlab-v02", HWLAB_RUNTIME_LANE: "v02", HWLAB_RUNTIME_ENDPOINT_LOCKED: "1", HWLAB_CODE_AGENT_ASSEMBLED_RUNTIME: "1", HWLAB_DEVICE_POD_API_KEY: "device-pod-api-key-a" }; function runAssembledDevicePodCli(argv: string[], options: any = {}) { return runDevicePodCli(argv, { ...options, env: { ...ASSEMBLED_RUNTIME_ENV, ...(options.env ?? {}) } }); } async function runHostCli(root: string, profile: any, args: string[]) { const profileJsonB64 = Buffer.from(JSON.stringify(profile), "utf8").toString("base64"); const proc = Bun.spawn([ process.env.HWLAB_TEST_NODE_COMMAND || "node", path.join(process.cwd(), "skills/device-pod-cli/assets/device-host-cli.mjs"), "--profile-json-b64", profileJsonB64, "--pod-id", profile.devicePodId || "device-pod-test", ...args ], { cwd: root, stdout: "pipe", stderr: "pipe" }); const stdout = await new Response(proc.stdout).text(); const stderr = await new Response(proc.stderr).text(); const exitCode = await proc.exited; assert.equal(stderr, ""); return { exitCode, payload: JSON.parse(stdout) }; } test("device-pod-cli first-admin setup can seed a cloud-api device pod without local profile authority", async () => { const seen: any[] = []; const profile = { schemaVersion: 1, target: { id: "target-71-freq" }, route: { gatewaySessionId: "gws_admin_seed" } }; const result = await runAssembledDevicePodCli([ "--username", "admin", "--password", "admin-pass", "--pod-id", "device-pod-71-freq", "--name", "71-FREQ", "--profile-json", JSON.stringify(profile), "setup", "first-admin" ], { fetchImpl: async (url, init) => { seen.push({ url: String(url), init, body: JSON.parse(String(init?.body ?? "{}")) }); return new Response(JSON.stringify({ created: true, actor: { id: "usr_admin", role: "admin" }, devicePodBootstrap: { requested: 1, initialized: 1 } }), { status: 201, headers: { "content-type": "application/json", "set-cookie": "hwlab_session=session-a" } }); }, now: () => "2026-05-29T00:00:00.000Z" }); assert.equal(result.exitCode, 0); assert.equal(result.payload.action, "setup.first-admin"); assert.equal(result.payload.devicePodSeedProvided, true); assert.equal(result.payload.localProfileAuthority, false); assert.equal(result.payload.setCookie, "hwlab_session=session-a"); assert.equal(seen[0].url, "http://cloud.test/v1/setup/first-admin"); assert.equal(seen[0].init.headers["x-hwlab-session-token"], undefined); assert.deepEqual(seen[0].body.devicePod, { devicePodId: "device-pod-71-freq", name: "71-FREQ", profile }); }); test("device-pod-cli admin wrappers call cloud-api profile and grant routes", async () => { const seen: any[] = []; const profile = { schemaVersion: 1, target: { id: "target-71-freq" }, route: { gatewaySessionId: "gws_admin_seed" } }; const fetchImpl = async (url, init) => { seen.push({ url: String(url), init, body: JSON.parse(String(init?.body ?? "{}")) }); return jsonResponse(201, { ok: true }); }; const adminEnv = { HWLAB_RUNTIME_API_URL: "http://cloud.test", HWLAB_RUNTIME_NAMESPACE: "hwlab-v02", HWLAB_RUNTIME_LANE: "v02", HWLAB_RUNTIME_ENDPOINT_LOCKED: "1", HWLAB_CODE_AGENT_ASSEMBLED_RUNTIME: "1", HWLAB_CLOUD_API_SESSION_TOKEN: "session-a" }; const upsert = await runDevicePodCli([ "--pod-id", "device-pod-71-freq", "--profile-json", JSON.stringify(profile), "admin", "device-pod", "upsert" ], { env: adminEnv, fetchImpl, now: () => "2026-05-29T00:00:00.000Z" }); assert.equal(upsert.exitCode, 0); assert.equal(seen[0].url, "http://cloud.test/v1/admin/device-pods"); assert.equal(seen[0].init.headers["x-hwlab-session-token"], "session-a"); assert.equal(seen[0].init.headers["x-hwlab-device-pod-api-key"], undefined); assert.deepEqual(seen[0].body, { devicePodId: "device-pod-71-freq", profile }); const grant = await runDevicePodCli([ "--pod-id", "device-pod-71-freq", "--user-id", "usr_alice", "admin", "grant" ], { env: adminEnv, fetchImpl, now: () => "2026-05-29T00:00:00.000Z" }); assert.equal(grant.exitCode, 0); assert.equal(seen[1].url, "http://cloud.test/v1/admin/device-pod-grants"); assert.deepEqual(seen[1].body, { devicePodId: "device-pod-71-freq", userId: "usr_alice" }); }); test("device-pod-cli lists cloud-api authority and ignores local profile routes", async () => { const root = await mkdtemp(path.join(os.tmpdir(), "hwlab-device-pod-cli-")); const profileDir = path.join(root, ".device-pod"); await mkdir(profileDir, { recursive: true }); await writeFile(path.join(profileDir, "device-pod-71-freq.json"), JSON.stringify({ gatewaySessionId: "gws_local_should_not_be_read", hostWorkspaceRoot: "F:\\legacy" }), "utf8"); const seen: any[] = []; const result = await runAssembledDevicePodCli(["profile", "list"], { env: { DEVICE_POD_PROFILE_DIR: profileDir }, fetchImpl: async (url, init) => { seen.push({ url: String(url), init }); return jsonResponse(200, { ok: true, contractVersion: "device-pod-authority-v1", devicePods: [{ devicePodId: "device-pod-71-freq", profileHash: "sha256:abc", profile: { route: { gatewaySessionId: "redacted" } } }] }); }, now: () => "2026-05-29T00:00:00.000Z" }); assert.equal(result.exitCode, 0); assert.equal(result.payload.localProfileAuthority, false); assert.equal(result.payload.localProfileRead, false); assert.equal(seen[0].url, "http://cloud.test/v1/device-pods"); assert.equal(seen[0].init.headers["x-hwlab-session-token"], undefined); assert.equal(seen[0].init.headers["x-hwlab-device-pod-api-key"], "device-pod-api-key-a"); assert.equal(JSON.stringify(result.payload).includes("gws_local_should_not_be_read"), false); }); test("device-pod-cli auto-locates from assembled runtime and rejects manual URL in locked runners", async () => { const ok = await runAssembledDevicePodCli(["profile", "list"], { fetchImpl: async (url, init) => { assert.equal(String(url), "http://cloud.test/v1/device-pods"); assert.equal(init?.headers?.["x-hwlab-session-token"], undefined); assert.equal(init?.headers?.["x-hwlab-device-pod-api-key"], "device-pod-api-key-a"); return jsonResponse(200, { ok: true, devicePods: [] }); }, now: () => "2026-06-01T00:00:00.000Z" }); assert.equal(ok.exitCode, 0); assert.equal(ok.payload.apiBaseUrl, "http://cloud.test"); assert.equal(ok.payload.runtimeEndpoint.source, "runtime-env"); assert.equal(ok.payload.runtimeEndpoint.explicitOverride, false); const rejected = await runAssembledDevicePodCli(["--api-base-url", "http://74.48.78.17:17667", "profile", "list"], { fetchImpl: async () => jsonResponse(500, { ok: false }), now: () => "2026-06-01T00:00:00.000Z" }); assert.equal(rejected.exitCode, 1); assert.equal(rejected.payload.error.code, "runtime_endpoint_manual_url_forbidden"); }); test("device-pod-cli default transport uses native HTTP for assembled cloud-api URLs", async () => { const seen: any[] = []; const server = createServer((request, response) => { let body = ""; request.on("data", (chunk) => { body += chunk; }); request.on("end", () => { seen.push({ method: request.method, url: request.url, headers: request.headers, body }); response.writeHead(200, { "content-type": "application/json" }); response.end(JSON.stringify({ ok: true, devicePods: [] })); }); }); await listen(server); try { const result = await runDevicePodCli(["profile", "list"], { env: { ...ASSEMBLED_RUNTIME_ENV, HWLAB_RUNTIME_API_URL: serverUrl(server) }, now: () => "2026-06-02T00:00:00.000Z" }); assert.equal(result.exitCode, 0); assert.equal(seen[0].method, "GET"); assert.equal(seen[0].url, "/v1/device-pods"); assert.equal(seen[0].headers["x-hwlab-device-pod-api-key"], "device-pod-api-key-a"); } finally { await close(server); } }); test("device-pod-cli reuses matching hwlab-cli web session cookie when no explicit auth is assembled", async () => { const root = await mkdtemp(path.join(os.tmpdir(), "hwlab-device-pod-cli-session-")); await mkdir(path.join(root, ".state/hwlab-cli"), { recursive: true }); await writeFile(path.join(root, ".state/hwlab-cli/session.json"), JSON.stringify({ baseUrl: "http://74.48.78.17:19666", cookie: "hwlab_session=session-from-web", user: { username: "admin" } }), "utf8"); const originalCwd = process.cwd(); process.chdir(root); try { const result = await runDevicePodCli(["profile", "list"], { env: { HWLAB_RUNTIME_NAMESPACE: "hwlab-v02", HWLAB_RUNTIME_LANE: "v02", HWLAB_RUNTIME_ENDPOINT_LOCKED: "1", HWLAB_CODE_AGENT_ASSEMBLED_RUNTIME: "1" }, fetchImpl: async (url, init) => { assert.equal(String(url), "http://74.48.78.17:19667/v1/device-pods"); assert.equal(init?.headers?.cookie, "hwlab_session=session-from-web"); assert.equal(init?.headers?.["x-hwlab-session-token"], undefined); return jsonResponse(200, { ok: true, devicePods: [] }); }, now: () => "2026-06-02T00:00:00.000Z" }); assert.equal(result.exitCode, 0); assert.equal(result.payload.runtimeEndpoint.source, "runtime-namespace"); assert.equal(result.payload.runtimeEndpoint.explicitOverride, false); } finally { process.chdir(originalCwd); } }); test("device-pod-cli ignores stale hwlab-cli web session from another runtime scope", async () => { const root = await mkdtemp(path.join(os.tmpdir(), "hwlab-device-pod-cli-stale-session-")); await mkdir(path.join(root, ".state/hwlab-cli"), { recursive: true }); await writeFile(path.join(root, ".state/hwlab-cli/session.json"), JSON.stringify({ baseUrl: "http://74.48.78.17:17666", cookie: "hwlab_session=dev-session" }), "utf8"); const originalCwd = process.cwd(); process.chdir(root); try { const result = await runDevicePodCli(["profile", "list"], { env: { HWLAB_RUNTIME_NAMESPACE: "hwlab-v02", HWLAB_RUNTIME_LANE: "v02", HWLAB_RUNTIME_ENDPOINT_LOCKED: "1", HWLAB_CODE_AGENT_ASSEMBLED_RUNTIME: "1" }, fetchImpl: async (_url, init) => { assert.equal(init?.headers?.cookie, undefined); return jsonResponse(401, { ok: false, error: { code: "auth_required" } }); }, now: () => "2026-06-02T00:00:00.000Z" }); assert.equal(result.exitCode, 1); assert.equal(result.payload.httpStatus, 401); } finally { process.chdir(originalCwd); } }); test("device-pod-cli keeps manual API URL only as unlocked local debug override", async () => { const seen: string[] = []; const result = await runDevicePodCli(["--api-base-url", "http://debug.test", "--session-token", "debug-session", "profile", "list"], { fetchImpl: async (url, init) => { seen.push(String(url)); assert.equal(init?.headers?.["x-hwlab-session-token"], "debug-session"); return jsonResponse(200, { ok: true, devicePods: [] }); }, now: () => "2026-06-01T00:00:00.000Z" }); assert.equal(result.exitCode, 0); assert.equal(seen[0], "http://debug.test/v1/device-pods"); assert.equal(result.payload.runtimeEndpoint.source, "explicit-override"); }); test("device-pod-cli selector creates REST job instead of gateway RPC", async () => { const seen: any[] = []; const result = await runAssembledDevicePodCli(["device-pod-71-freq:workspace:/src", "ls"], { fetchImpl: async (url, init) => { seen.push({ url: String(url), init, body: JSON.parse(String(init?.body ?? "{}")) }); return jsonResponse(202, { ok: true, status: "running", job: { id: "job_1", status: "running" }, profileHash: "sha256:abc" }); }, now: () => "2026-05-29T00:00:00.000Z" }); assert.equal(result.exitCode, 0); assert.equal(seen[0].url, "http://cloud.test/v1/device-pods/device-pod-71-freq/jobs"); assert.deepEqual(seen[0].body, { intent: "workspace.ls", args: { path: "src" } }); assert.equal(JSON.stringify(seen[0].body).includes("gatewaySessionId"), false); }); test("device-pod-cli bootsharp creates a formal workspace bootsharp REST job", async () => { const seen: any[] = []; const result = await runAssembledDevicePodCli(["bootsharp", "--pod-id", "D601-F103-V2", "--depth", "2"], { fetchImpl: async (url, init) => { seen.push({ url: String(url), init, body: JSON.parse(String(init?.body ?? "{}")) }); return jsonResponse(202, { ok: true, status: "running", job: { id: "job_bootsharp", status: "running" }, profileHash: "sha256:abc" }); }, now: () => "2026-05-31T00:00:00.000Z" }); assert.equal(result.exitCode, 0); assert.equal(seen[0].url, "http://cloud.test/v1/device-pods/D601-F103-V2/jobs"); assert.deepEqual(seen[0].body, { intent: "workspace.bootsharp", args: { path: ".", depth: 2 } }); assert.equal(result.payload.localProfileAuthority, false); }); test("device-pod-cli job output is compact by default and full with --full", async () => { const longText = `${"x".repeat(30000)}FREQ_Controller_FW.axf${"y".repeat(30000)}FREQ_Controller_FW.hex${"z".repeat(30000)}`; const payload = { serviceId: "hwlab-cloud-api", contractVersion: "device-pod-job-v1", status: "completed", devicePodId: "D601-F103-V2", traceId: "trc_job", operationId: "op_job", job: { id: "job_1", status: "completed" }, output: { text: longText, dispatch: { command: "verbose command that should stay out of compact output" }, bytes: Buffer.byteLength(longText, "utf8"), summary: "gateway/device-host-cli dispatch completed" } }; const fetchImpl = async () => jsonResponse(200, payload); const compact = await runAssembledDevicePodCli([ "job", "output", "--pod-id", "D601-F103-V2", "job_1" ], { fetchImpl, now: () => "2026-05-31T00:00:00.000Z" }); assert.equal(compact.exitCode, 0); assert.equal(compact.payload.body.compacted, true); assert.equal(compact.payload.body.text.length, 64000); assert.equal(compact.payload.body.text.includes("device-pod job output compacted"), true); assert.equal(compact.payload.body.text.includes("FREQ_Controller_FW.hex"), true); assert.equal(compact.payload.body.text.includes("FREQ_Controller_FW.axf"), true); assert.equal(JSON.stringify(compact.payload).includes("verbose command"), false); const full = await runAssembledDevicePodCli([ "--full", "job", "output", "--pod-id", "D601-F103-V2", "job_1" ], { fetchImpl, now: () => "2026-05-31T00:00:00.000Z" }); assert.equal(full.exitCode, 0); assert.equal(full.payload.full, true); assert.equal(full.payload.body.output.dispatch.command, "verbose command that should stay out of compact output"); }); test("device-pod-cli sends reason without extra token for mutating jobs", async () => { const seen: any[] = []; const result = await runAssembledDevicePodCli(["--reason", "reset smoke", "device-pod-71-freq:debug-probe", "reset"], { 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_reset", status: "running" } }); }, now: () => "2026-05-29T00:00:00.000Z" }); assert.equal(result.exitCode, 0); assert.deepEqual(seen[0].body, { intent: "debug.reset", args: {}, reason: "reset smoke" }); }); test("device-pod-cli rejects standalone lease command with standard path guidance", async () => { let called = false; const result = await runAssembledDevicePodCli(["lease", "current", "--pod-id", "D601-F103-V2"], { fetchImpl: async () => { called = true; return jsonResponse(500, { ok: false }); }, now: () => "2026-06-02T00:00:00.000Z" }); assert.equal(result.exitCode, 1); assert.equal(called, false); assert.equal(result.payload.error.code, "standalone_lease_command_removed"); assert.match(result.payload.error.message, /Device Pod jobs through cloud-api/); assert.equal(JSON.stringify(result.payload.error.details.next).includes("hwpod lease acquire/current/release"), true); }); test("device-pod-cli preserves common v0.1 hardware options in REST job args", async () => { const seen: any[] = []; const result = await runAssembledDevicePodCli([ "--reason", "boot log", "--capture-uart", "uart/1", "--capture-duration-ms", "8000", "--port", "COM4", "--baud-rate", "921600", "device-pod-71-freq:debug-probe", "download", "start" ], { 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_download", status: "running" } }); }, now: () => "2026-05-29T00:00:00.000Z" }); assert.equal(result.exitCode, 0); assert.deepEqual(seen[0].body, { intent: "debug.download", args: { action: "start", captureUart: "uart/1", captureDurationMs: 8000, port: "COM4", baudRate: 921600 }, reason: "boot log" }); }); test("device-pod-cli exposes workspace writes, keil source edits, and UART JSON-RPC as REST jobs", 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 put = await runAssembledDevicePodCli([ "--reason", "write file", "device-pod-71-freq:workspace:/projects/app", "put", "User/new.c", "--create-dirs" ], { fetchImpl, stdinText: "int main(void) { return 0; }\n", now: () => "2026-05-29T00:00:00.000Z" }); assert.equal(put.exitCode, 0); assert.equal(seen[0].body.intent, "workspace.put"); assert.equal(seen[0].body.args.path, "projects/app/User/new.c"); assert.equal(seen[0].body.args.createDirs, true); assert.equal(Buffer.from(seen[0].body.args.contentB64, "base64").toString("utf8"), "int main(void) { return 0; }\n"); const keil = await runAssembledDevicePodCli([ "--reason", "add source", "device-pod-71-freq:workspace:/projects/app", "keil", "add-source", "User/new.c", "--group", "User" ], { fetchImpl, now: () => "2026-05-29T00:00:00.000Z" }); assert.equal(keil.exitCode, 0); assert.deepEqual(seen[1].body.args, { action: "add-source", base: "projects/app", path: "User/new.c", group: "User" }); const jsonrpc = await runAssembledDevicePodCli([ "--reason", "gpio read", "device-pod-71-freq:io-probe:/uart/1", "jsonrpc", "gpio.read", "--params-json", "{\"pin\":\"PB5\"}", "--require-jsonrpc-result", "--expect-result-field", "pin", "--expect-result-field", "value" ], { fetchImpl, now: () => "2026-05-29T00:00:00.000Z" }); assert.equal(jsonrpc.exitCode, 0); assert.equal(seen[2].body.intent, "io.uart.jsonrpc"); assert.deepEqual(seen[2].body.args.expectResultField, ["pin", "value"]); assert.equal(seen[2].body.args.params, "{\"pin\":\"PB5\"}"); }); test("device-pod-cli rejects legacy local profile create in formal mode", async () => { const result = await runDevicePodCli(["profile", "create", "--pod-id", "device-pod-71-freq"], { now: () => "2026-05-29T00:00:00.000Z" }); assert.equal(result.exitCode, 1); assert.equal(result.payload.error.code, "legacy_profile_create_removed"); }); test("device-pod-cli keeps debug-probe status as a probe operation, not job status", async () => { const seen: any[] = []; const result = await runAssembledDevicePodCli(["D601-F103-V2:debug-probe", "status"], { fetchImpl: async (url, init) => { seen.push({ url: String(url), init }); return jsonResponse(200, { ok: true, devicePodId: "D601-F103-V2", status: "ok" }); }, now: () => "2026-05-31T00:00:00.000Z" }); assert.equal(result.exitCode, 0); assert.equal(seen[0].url, "http://cloud.test/v1/device-pods/D601-F103-V2/status"); assert.equal(result.payload.action, "debug.status"); }); test("device-pod-cli forwards host build and download job ids as named device-pod jobs", 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:/", "build", "status", "20260531101208650-keil-build-030aab" ], { fetchImpl, now: () => "2026-05-31T00:00:00.000Z" }); assert.equal(build.exitCode, 0); assert.deepEqual(seen[0].body, { intent: "workspace.build", args: { action: "status", jobId: "20260531101208650-keil-build-030aab" } }); const download = await runAssembledDevicePodCli([ "D601-F103-V2:debug-probe", "download", "status", "20260531103014862-keil-download-177dd9" ], { fetchImpl, now: () => "2026-05-31T00:00:00.000Z" }); assert.equal(download.exitCode, 0); assert.deepEqual(seen[1].body, { intent: "debug.download", args: { action: "status", jobId: "20260531103014862-keil-download-177dd9" } }); }); 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 maps build verify to read-only workspace.evidence verify intent (HWLAB #821)", 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 result = await runAssembledDevicePodCli([ "D601-71-FREQ:workspace:/", "build", "verify", "--target", "FREQ_Controller_FW", "--expected", "axf,hex", "--head-bytes", "16" ], { fetchImpl, now: () => "2026-06-04T00:00:00.000Z" }); assert.equal(result.exitCode, 0); assert.deepEqual(seen[0].body, { intent: "workspace.evidence", args: { kind: "verify", target: "FREQ_Controller_FW", expected: "axf,hex", headBytes: 16 } }); assert.equal(JSON.stringify(seen[0].body).includes("reason"), false); }); test("device-host-cli build verify returns artifact stats, missing artifacts, and no-job blocker (HWLAB #821)", async () => { const root = await mkdtemp(path.join(os.tmpdir(), "hwlab-device-host-verify-")); const projectRel = path.join("projects", "71-00075-11", "FirmWare", "MDK-ARM", "FREQ_Controller_FW.uvprojx"); const project = path.join(root, projectRel); const outputDir = path.join(path.dirname(project), "FREQ_Controller_FW"); await mkdir(outputDir, { recursive: true }); await writeFile(project, [ "", " ", " ", " FREQ_Controller_FW", " ", " ", " .\\FREQ_Controller_FW\\", " FREQ_Controller_FW", " ", " ", " ", " ", "" ].join("\n")); await writeFile(path.join(outputDir, "FREQ_Controller_FW.axf"), Buffer.from([0x7f, 0x45, 0x4c, 0x46, 1, 1, 1, 0])); await writeFile(path.join(outputDir, "FREQ_Controller_FW.bin"), Buffer.from([1, 2, 3, 4])); await writeFile(path.join(outputDir, "FREQ_Controller_FW.hex"), ":020000040800F2\n"); const jobsDir = path.join(root, ".device-pod", ".state", "device-host-cli", "jobs"); await mkdir(jobsDir, { recursive: true }); await writeFile(path.join(jobsDir, "20260604053140522-keil-build-09e72a.json"), JSON.stringify({ jobId: "20260604053140522-keil-build-09e72a", kind: "keil-build", status: "completed", createdAt: "2026-06-04T05:31:40.522Z", updatedAt: "2026-06-04T05:31:56.266Z", result: { success: true, exitCode: 0, elapsedMs: 15744, project, target: "FREQ_Controller_FW", buildSummary: { hasSummary: true, errors: 0, warnings: 0, hasErrors: false } } }, null, 2)); const profile = { devicePodId: "D601-71-FREQ", workspaceRoot: root, projectWorkspace: { projectPath: projectRel, targetName: "FREQ_Controller_FW", hexPath: path.join("projects", "71-00075-11", "FirmWare", "MDK-ARM", "FREQ_Controller_FW", "FREQ_Controller_FW.hex") } }; const happy = await runHostCli(root, profile, ["workspace", "build", "verify", "--expected", "axf,bin,hex"]); assert.equal(happy.exitCode, 0); assert.equal(happy.payload.ok, true); assert.equal(happy.payload.action, "workspace.build.verify"); assert.equal(happy.payload.data.buildJob.jobId, "20260604053140522-keil-build-09e72a"); assert.deepEqual(happy.payload.data.missing, []); assert.deepEqual(happy.payload.data.artifacts.map((artifact) => artifact.kind), ["axf", "bin", "hex"]); assert.equal(Buffer.from(happy.payload.data.artifacts[0].headMagicB64, "base64")[0], 0x7f); await writeFile(path.join(outputDir, "FREQ_Controller_FW.bin"), Buffer.alloc(0)); const missing = await runHostCli(root, profile, ["workspace", "build", "verify", "--expected", "bin"]); assert.equal(missing.exitCode, 1); assert.equal(missing.payload.ok, false); assert.deepEqual(missing.payload.data.missing, [{ kind: "bin", path: path.join(outputDir, "FREQ_Controller_FW.bin"), reason: "zero-bytes" }]); const emptyRoot = await mkdtemp(path.join(os.tmpdir(), "hwlab-device-host-verify-no-job-")); const emptyProject = path.join(emptyRoot, projectRel); const emptyOutputDir = path.join(path.dirname(emptyProject), "FREQ_Controller_FW"); await mkdir(emptyOutputDir, { recursive: true }); await writeFile(emptyProject, await Bun.file(project).text()); await writeFile(path.join(emptyOutputDir, "FREQ_Controller_FW.axf"), Buffer.from([0x7f, 0x45, 0x4c, 0x46])); const noJob = await runHostCli(emptyRoot, { ...profile, workspaceRoot: emptyRoot }, ["workspace", "build", "verify", "--expected", "axf"]); assert.equal(noJob.exitCode, 1); assert.equal(noJob.payload.ok, false); assert.equal(noJob.payload.data.buildJob, null); assert.equal(noJob.payload.data.blockers[0].code, "keil_build_job_not_found"); }); 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) => { seen.push({ url: String(url), method: init?.method }); return jsonResponse(200, { ok: true, status: "completed", output: { text: "done" } }); }; const build = await runAssembledDevicePodCli([ "D601-F103-V2:workspace:/", "build", "status", "job_devicepod_build" ], { fetchImpl, now: () => "2026-05-31T00:00:00.000Z" }); assert.equal(build.exitCode, 0); assert.equal(build.payload.action, "job.status"); assert.equal(seen[0].url, "http://cloud.test/v1/device-pods/D601-F103-V2/jobs/job_devicepod_build"); const download = await runAssembledDevicePodCli([ "D601-F103-V2:debug-probe", "download", "output", "job_devicepod_download" ], { fetchImpl, now: () => "2026-05-31T00:00:00.000Z" }); assert.equal(download.exitCode, 0); assert.equal(download.payload.action, "job.output"); assert.equal(seen[1].url, "http://cloud.test/v1/device-pods/D601-F103-V2/jobs/job_devicepod_download/output"); }); test("device-pod-cli accepts explicit workspace rg pattern and path options", async () => { const seen: any[] = []; const result = await runAssembledDevicePodCli([ "D601-F103-V2:workspace:/", "rg", "--pattern", "Programming Done|Verify OK|Application running|Error", "--path", "projects/01_baseline/captures" ], { 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_rg", status: "running" } }); }, now: () => "2026-05-31T00:00:00.000Z" }); assert.equal(result.exitCode, 0); assert.deepEqual(seen[0].body, { intent: "workspace.rg", args: { pattern: "Programming Done|Verify OK|Application running|Error", path: "projects/01_baseline/captures" } }); }); test("device-pod-cli forwards workspace rg filename search and scan limit options", async () => { const seen: any[] = []; const result = await runAssembledDevicePodCli([ "D601-71-FREQ:workspace:/", "rg", "--pattern", "FREQ_Controller_FW\\.(axf|bin|hex)$", "--path", "projects/71-00075-11/FirmWare/MDK-ARM", "--name-only", "--files-with-matches", "--limit", "25000" ], { 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_rg_names", status: "running" } }); }, now: () => "2026-06-04T00:00:00.000Z" }); assert.equal(result.exitCode, 0); assert.deepEqual(seen[0].body, { intent: "workspace.rg", args: { pattern: "FREQ_Controller_FW\\.(axf|bin|hex)$", path: "projects/71-00075-11/FirmWare/MDK-ARM", nameOnly: true, filesWithMatches: true, limit: 25000 } }); }); test("device-pod-cli forwards workspace rg base64 patterns with explicit path", async () => { const seen: any[] = []; const pattern = Buffer.from("Error\\(s\\)|Warning\\(s\\)", "utf8").toString("base64"); const result = await runAssembledDevicePodCli([ "D601-71-FREQ:workspace:/", "rg", "--pattern-b64", pattern, "--path", "projects/71-00075-11/FirmWare/MDK-ARM" ], { 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_rg_b64", status: "running" } }); }, now: () => "2026-06-04T00:00:00.000Z" }); assert.equal(result.exitCode, 0); assert.deepEqual(seen[0].body, { intent: "workspace.rg", args: { pattern: "Error\\(s\\)|Warning\\(s\\)", path: "projects/71-00075-11/FirmWare/MDK-ARM" } }); }); function jsonResponse(status: number, body: unknown) { return new Response(JSON.stringify(body), { status, headers: { "content-type": "application/json" } }); } function listen(server: ReturnType) { return new Promise((resolve, reject) => { server.once("error", reject); server.listen(0, "127.0.0.1", resolve); }); } function close(server: ReturnType) { return new Promise((resolve, reject) => { server.close((error) => error ? reject(error) : resolve()); }); } test("device-pod-cli fails fast on debug-probe flash with selector hint", async () => { const result = await runAssembledDevicePodCli([ "device-pod-71-freq:debug-probe", "flash" ], { fetchImpl: async () => { throw new Error("should not reach cloud-api"); } }); assert.equal(result.exitCode, 1); const payload = result.payload; assert.equal(payload.ok, false); assert.equal(payload.error.code, "unsupported_debug_operation"); assert.match(payload.error.message, /flash/); assert.match(payload.error.message, /debug-probe download start/); assert.match(payload.error.message, /Selector Cheat Sheet/); assert.deepEqual(payload.error.details.allowed, ["status", "chip-id", "download", "reset"]); assert.match(payload.error.details.hint, /debug-probe download start/); }); test("device-pod-cli fails fast on workspace.keil download with selector hint", async () => { const result = await runAssembledDevicePodCli([ "device-pod-71-freq:workspace:/", "keil", "download", "FREQ_Controller_FW.hex" ], { fetchImpl: async () => { throw new Error("should not reach cloud-api"); } }); assert.equal(result.exitCode, 1); const payload = result.payload; assert.equal(payload.ok, false); assert.equal(payload.error.code, "unsupported_workspace_keil_action"); assert.match(payload.error.message, /download/); assert.match(payload.error.message, /add-source/); assert.match(payload.error.message, /remove-source/); assert.match(payload.error.message, /debug-probe download start/); assert.deepEqual(payload.error.details.allowed, ["add-source", "remove-source"]); }); test("device-pod-cli fails fast on workspace.keil flash with selector hint", async () => { const result = await runAssembledDevicePodCli([ "device-pod-71-freq:workspace:/", "keil", "flash" ], { fetchImpl: async () => { throw new Error("should not reach cloud-api"); } }); assert.equal(result.exitCode, 1); const payload = result.payload; assert.equal(payload.ok, false); assert.equal(payload.error.code, "unsupported_workspace_keil_action"); assert.match(payload.error.message, /flash/); }); test("device-pod-cli still accepts workspace.keil add-source and remove-source", async () => { const seen: any[] = []; const result = await runAssembledDevicePodCli([ "device-pod-71-freq:workspace:/", "keil", "add-source", "User/new.c", "--group", "User", "--reason", "DEV smoke" ], { 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_keil_add", status: "running" } }); } }); assert.equal(result.exitCode, 0); assert.equal(seen[0].body.intent, "workspace.keil"); assert.equal(seen[0].body.args.action, "add-source"); assert.equal(seen[0].body.args.path, "User/new.c"); }); test("device-pod-cli rejects unknown debug-probe operation with allowed list", async () => { const result = await runAssembledDevicePodCli([ "device-pod-71-freq:debug-probe", "wiggle" ], { fetchImpl: async () => { throw new Error("should not reach cloud-api"); } }); assert.equal(result.exitCode, 1); const payload = result.payload; assert.equal(payload.error.code, "unsupported_debug_operation"); assert.match(payload.error.message, /wiggle/); assert.match(payload.error.message, /status, chip-id, download, reset/); assert.deepEqual(payload.error.details.allowed, ["status", "chip-id", "download", "reset"]); }); test("device-pod-cli help surfaces selectorCheatSheet with SOP table and common-mistake list", async () => { const result = await runAssembledDevicePodCli(["--help"], { fetchImpl: async () => { throw new Error("help should not hit network"); } }); assert.equal(result.exitCode, 0); const payload = result.payload; assert.ok(Array.isArray(payload.selectorCheatSheet), "selectorCheatSheet array should be present"); const joined = payload.selectorCheatSheet.join("\n"); assert.match(joined, /workspace: ls \| cat/); assert.match(joined, /debug-probe: status \| chip-id \| download/); assert.match(joined, /io-probe: ports/); assert.match(joined, /no flash op/); assert.match(joined, /Selector Cheat Sheet/); }); function serverUrl(server: ReturnType) { const address = server.address(); if (!address || typeof address === "string") throw new Error("server address is unavailable"); return `http://127.0.0.1:${address.port}`; } test("device-pod-cli prefers HWLAB_API_KEY over HWLAB_DEVICE_POD_API_KEY when both are set", async () => { const seen: any[] = []; const server = createServer((request, response) => { let chunks: Buffer[] = []; request.on("data", (chunk) => chunks.push(chunk)); request.on("end", () => { const body = JSON.parse(Buffer.concat(chunks).toString("utf8") || "{}"); seen.push({ url: request.url, method: request.method, devicePodKey: request.headers["x-hwlab-device-pod-api-key"] ?? null, authorization: request.headers["authorization"] ?? null }); response.writeHead(200, { "content-type": "application/json" }); response.end(JSON.stringify({ ok: true, status: "ok", devicePods: [], count: 0 })); }); }); await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve)); try { const port = server.address().port; const result = await runDevicePodCli(["profile", "list"], { env: { HWLAB_RUNTIME_API_URL: `http://127.0.0.1:${port}`, HWLAB_RUNTIME_NAMESPACE: "hwlab-v02", HWLAB_RUNTIME_LANE: "v02", HWLAB_RUNTIME_ENDPOINT_LOCKED: "1", HWLAB_CODE_AGENT_ASSEMBLED_RUNTIME: "1", HWLAB_DEVICE_POD_API_KEY: "device-pod-api-key-a", HWLAB_API_KEY: "hwl_live_user-default-key-1234567890" }, now: () => "2026-06-03T00:00:00.000Z" }); assert.equal(result.exitCode, 0); assert.equal(seen.length, 1); assert.equal(seen[0].devicePodKey, null); assert.equal(seen[0].authorization, "Bearer hwl_live_user-default-key-1234567890"); } finally { await new Promise((resolve, reject) => server.close((error) => (error ? reject(error) : resolve()))); } });