Files
pikasTech-HWLAB/tools/device-pod-cli.test.ts
T

383 lines
16 KiB
TypeScript

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" } });
}