161 lines
7.7 KiB
TypeScript
161 lines
7.7 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 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 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");
|
|
});
|
|
|
|
function jsonResponse(status: number, body: unknown) {
|
|
return new Response(JSON.stringify(body), { status, headers: { "content-type": "application/json" } });
|
|
}
|