import assert from "node:assert/strict"; import { mkdtemp, mkdir, writeFile } from "node:fs/promises"; import os from "node:os"; import path from "node:path"; import { test } from "bun:test"; import { runDevicePodCli } from "./src/device-pod-cli-lib.ts"; 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 runDevicePodCli([ "--api-base-url", "http://cloud.test", "--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 upsert = await runDevicePodCli([ "--api-base-url", "http://cloud.test", "--session-token", "session-a", "--pod-id", "device-pod-71-freq", "--profile-json", JSON.stringify(profile), "admin", "device-pod", "upsert" ], { 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.deepEqual(seen[0].body, { devicePodId: "device-pod-71-freq", profile }); const grant = await runDevicePodCli([ "--api-base-url", "http://cloud.test", "--session-token", "session-a", "--pod-id", "device-pod-71-freq", "--user-id", "usr_alice", "admin", "grant" ], { 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 runDevicePodCli(["--api-base-url", "http://cloud.test", "--session-token", "session-a", "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"], "session-a"); assert.equal(JSON.stringify(result.payload).includes("gws_local_should_not_be_read"), false); }); test("device-pod-cli selector creates REST job instead of gateway RPC", async () => { const seen: any[] = []; const result = await runDevicePodCli(["--api-base-url", "http://cloud.test", "--session-token", "session-a", "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 runDevicePodCli(["--api-base-url", "http://cloud.test", "--session-token", "session-a", "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 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: "x".repeat(5000), dispatch: { command: "verbose command that should stay out of compact output" }, bytes: 5000, summary: "gateway/device-host-cli dispatch completed" } }; const fetchImpl = async () => jsonResponse(200, payload); const compact = await runDevicePodCli([ "--api-base-url", "http://cloud.test", "--session-token", "session-a", "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, 4000); assert.equal(JSON.stringify(compact.payload).includes("verbose command"), false); const full = await runDevicePodCli([ "--api-base-url", "http://cloud.test", "--session-token", "session-a", "--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 and lease token for mutating jobs", async () => { const seen: any[] = []; const result = await runDevicePodCli(["--api-base-url", "http://cloud.test", "--session-token", "session-a", "--lease-token", "lease-a", "--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", leaseToken: "lease-a" }); }); test("device-pod-cli preserves common v0.1 hardware options in REST job args", async () => { const seen: any[] = []; const result = await runDevicePodCli([ "--api-base-url", "http://cloud.test", "--session-token", "session-a", "--reason", "boot log", "--lease-token", "lease-a", "--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", leaseToken: "lease-a" }); }); 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 runDevicePodCli([ "--api-base-url", "http://cloud.test", "--session-token", "session-a", "--reason", "write file", "--lease-token", "lease-a", "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 runDevicePodCli([ "--api-base-url", "http://cloud.test", "--session-token", "session-a", "--reason", "add source", "--lease-token", "lease-a", "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 runDevicePodCli([ "--api-base-url", "http://cloud.test", "--session-token", "session-a", "--reason", "gpio read", "--lease-token", "lease-a", "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 runDevicePodCli(["--api-base-url", "http://cloud.test", "--session-token", "session-a", "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 runDevicePodCli([ "--api-base-url", "http://cloud.test", "--session-token", "session-a", "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 runDevicePodCli([ "--api-base-url", "http://cloud.test", "--session-token", "session-a", "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 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 runDevicePodCli([ "--api-base-url", "http://cloud.test", "--session-token", "session-a", "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 runDevicePodCli([ "--api-base-url", "http://cloud.test", "--session-token", "session-a", "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 runDevicePodCli([ "--api-base-url", "http://cloud.test", "--session-token", "session-a", "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" } }); }); function jsonResponse(status: number, body: unknown) { return new Response(JSON.stringify(body), { status, headers: { "content-type": "application/json" } }); }