import assert from "node:assert/strict"; import { mkdir, mkdtemp, rm, symlink, writeFile } from "node:fs/promises"; import os from "node:os"; import path from "node:path"; import { test } from "bun:test"; import { createCloudApiBunServer } from "./bun-server.ts"; import { createCloudApiServer } from "./server.ts"; import { connectHwpodNodeWs, createHwpodNodeServer } from "../../tools/src/hwpod-node-lib.ts"; test("cloud-api exposes hwpod-node-ops contract on /v1", async () => { const server = createCloudApiServer(); await listen(server); try { const response = await fetch(`${serverUrl(server)}/v1`); const payload = await response.json(); assert.equal(response.status, 200); assert.equal(payload.hwpod.contractVersion, "hwpod-node-ops-v1"); assert.equal(payload.hwpod.apiRole, "node-ops-forwarder"); assert.equal(payload.hwpod.specDiscoveryRoute, "/v1/hwpod/specs"); assert.ok(payload.hwpod.supportedOps.includes("node.diagnostics")); assert.ok(payload.hwpod.supportedOps.includes("workspace.ls")); assert.ok(payload.hwpod.supportedOps.includes("workspace.insert-after")); assert.equal(payload.hwlabNode.updateRoute, "/v1/hwlab-node/update"); assert.equal(payload.hwlabNode.downloadRoute, "/v1/hwlab-node/download/hwlab-node.py"); assert.equal(payload.hwlabNode.diagnosticsOp, "node.diagnostics"); assert.equal(payload.hwlabNode.updateContractVersion, "hwlab-node-update-v1"); } finally { await close(server); } }); test("cloud-api exposes hwlab-node Python update metadata", async () => { const server = createCloudApiServer({ env: { PATH: process.env.PATH, HWLAB_PUBLIC_ENDPOINT: "https://hwlab.pikapython.com", HWLAB_NODE_PY_LATEST_VERSION: "0.1.1", HWLAB_NODE_PY_DOWNLOAD_PATH: "/downloads/hwlab-node.py", HWLAB_NODE_PY_SHA256: "abc123" } }); await listen(server); try { const response = await fetch(`${serverUrl(server)}/v1/hwlab-node/update?current=0.1.0&channel=stable&platform=windows`); const payload = await response.json(); assert.equal(response.status, 200); assert.equal(payload.ok, true); assert.equal(payload.contractVersion, "hwlab-node-update-v1"); assert.equal(payload.updateAvailable, true); assert.equal(payload.latestVersion, "0.1.1"); assert.equal(payload.downloadUrl, "https://hwlab.pikapython.com/downloads/hwlab-node.py"); assert.equal(payload.sha256, "abc123"); const currentResponse = await fetch(`${serverUrl(server)}/v1/hwlab-node/update?current=0.1.1&channel=stable&platform=windows`); const currentPayload = await currentResponse.json(); assert.equal(currentResponse.status, 200); assert.equal(currentPayload.updateAvailable, false); assert.equal(currentPayload.downloadUrl, null); } finally { await close(server); } }); test("cloud-api serves bundled hwlab-node Python updater artifact by default", async () => { const server = createCloudApiServer({ env: { PATH: process.env.PATH, HWLAB_PUBLIC_ENDPOINT: "https://hwlab.pikapython.com" } }); await listen(server); try { const response = await fetch(`${serverUrl(server)}/v1/hwlab-node/update?current=0.1.0&channel=stable&platform=windows`); const payload = await response.json(); assert.equal(response.status, 200); assert.equal(payload.updateAvailable, true); assert.equal(payload.latestVersion, "0.1.1"); assert.equal(payload.downloadUrl, "https://hwlab.pikapython.com/v1/hwlab-node/download/hwlab-node.py"); assert.match(payload.sha256, /^[a-f0-9]{64}$/u); const download = await fetch(`${serverUrl(server)}/v1/hwlab-node/download/hwlab-node.py`); const text = await download.text(); assert.equal(download.status, 200); assert.equal(download.headers.get("x-hwlab-node-version"), "0.1.1"); assert.ok(text.includes('APP_VERSION = "0.1.1"')); } finally { await close(server); } }); test("cloud-api discovers preinstalled workspace hwpod-specs without hardcoded device constants", async () => { const root = await mkdtemp(path.join(os.tmpdir(), "hwlab-hwpod-spec-discovery-")); const specDir = path.join(root, ".hwlab"); await mkdir(specDir, { recursive: true }); await writeFile(path.join(specDir, "hwpod-spec.yaml"), sampleSpecYaml(), "utf8"); await writeFile(path.join(specDir, "hwpod-spec.meta.json"), JSON.stringify({ source: { kind: "preinstalled-verified-spec", caseRepo: "pikasTech/hwlab-case-registry", caseId: "d601-f103-v2-compile" } }), "utf8"); const server = createCloudApiServer({ env: { PATH: process.env.PATH, HWLAB_CODE_AGENT_WORKSPACE: root } }); await listen(server); try { const response = await fetch(`${serverUrl(server)}/v1/hwpod/specs`); const payload = await response.json(); assert.equal(response.status, 200); assert.equal(payload.contractVersion, "hwpod-spec-discovery-v1"); assert.equal(payload.count, 1); assert.equal(payload.specs[0].name, "d601-f103-v2"); assert.equal(payload.specs[0].uid, "D601-F103-V2"); assert.equal(payload.specs[0].nodeId, "node-d601-f103-v2"); assert.equal(payload.specs[0].source.caseRepo, "pikasTech/hwlab-case-registry"); assert.equal(payload.specs[0].source.caseId, "d601-f103-v2-compile"); } finally { await close(server); await rm(root, { recursive: true, force: true }); } }); test("cloud-api discovers configmap-mounted registry hwpod-spec symlinks", async () => { const root = await mkdtemp(path.join(os.tmpdir(), "hwlab-hwpod-spec-registry-")); const registryDir = path.join(root, "registry"); const dataDir = path.join(root, "data"); await mkdir(registryDir, { recursive: true }); await mkdir(dataDir, { recursive: true }); const targetPath = path.join(dataDir, "constart-71freq-c.yaml"); await writeFile(targetPath, sampleConstartSpecYaml(), "utf8"); await symlink(targetPath, path.join(registryDir, "constart-71freq-c.yaml")); const server = createCloudApiServer({ env: { PATH: process.env.PATH, HWLAB_CODE_AGENT_WORKSPACE: root, HWLAB_HWPOD_SPEC_REGISTRY_DIRS: registryDir } }); await listen(server); try { const response = await fetch(`${serverUrl(server)}/v1/hwpod/specs`); const payload = await response.json(); assert.equal(response.status, 200); assert.equal(payload.contractVersion, "hwpod-spec-discovery-v1"); const spec = payload.specs.find((item: any) => item.name === "constart-71freq-c"); assert.ok(spec); assert.equal(spec.uid, "CONSTART-71FREQ-C"); assert.equal(spec.nodeId, "node-d601-f103-v2"); assert.equal(spec.workspacePath, "F:\\Work\\ConStart"); } finally { await close(server); await rm(root, { recursive: true, force: true }); } }); test("cloud-api probes discovered hwpod-spec availability through node-ops", async () => { const root = await mkdtemp(path.join(os.tmpdir(), "hwlab-hwpod-spec-probe-")); const specDir = path.join(root, ".hwlab"); await mkdir(specDir, { recursive: true }); await writeFile(path.join(specDir, "hwpod-spec.yaml"), sampleSpecYaml(), "utf8"); const seen: any[] = []; const server = createCloudApiServer({ env: { PATH: process.env.PATH, HWLAB_CODE_AGENT_WORKSPACE: root }, hwpodNodeOpsHandler: async (plan: any) => { seen.push(plan); return { ok: true, status: "completed", results: plan.ops.map((op: any) => ({ opId: op.opId, op: op.op, ok: true, status: "completed", output: { path: op.args.workspacePath } })) }; } }); await listen(server); try { const response = await fetch(`${serverUrl(server)}/v1/hwpod/specs?probe=1`); const payload = await response.json(); assert.equal(response.status, 200); assert.equal(payload.count, 1); assert.equal(payload.availableCount, 1); assert.equal(payload.specs[0].availability.status, "available"); assert.equal(seen[0].hwpodId, "d601-f103-v2"); assert.equal(seen[0].nodeId, "node-d601-f103-v2"); assert.equal(seen[0].ops[0].op, "workspace.ls"); } finally { await close(server); await rm(root, { recursive: true, force: true }); } }); test("cloud-api forwards valid hwpod-node-ops plans to injected node handler", async () => { const seen: any[] = []; const server = createCloudApiServer({ hwpodNodeOpsHandler: async (plan: any, context: any) => { seen.push({ plan, context }); return { ok: true, status: "completed", results: plan.ops.map((op: any) => ({ opId: op.opId, op: op.op, ok: true, status: "completed", output: { sample: true } })) }; } }); await listen(server); try { const response = await fetch(`${serverUrl(server)}/v1/hwpod-node-ops`, { method: "POST", headers: { "content-type": "application/json", "x-trace-id": "trc_hwpod_test" }, body: JSON.stringify(samplePlan()) }); const payload = await response.json(); assert.equal(response.status, 200); assert.equal(payload.ok, true); assert.equal(payload.status, "completed"); assert.equal(payload.contractVersion, "hwpod-node-ops-v1"); assert.equal(payload.results[0].op, "workspace.ls"); assert.equal(seen.length, 1); assert.equal(seen[0].plan.nodeId, "pc-host-1"); assert.equal(seen[0].context.requestMeta.traceId, "trc_hwpod_test"); } finally { await close(server); } }); test("cloud-api returns blocked payload when hwpod-node is not wired yet", async () => { const server = createCloudApiServer(); await listen(server); try { const response = await fetch(`${serverUrl(server)}/v1/hwpod-node-ops`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(samplePlan()) }); const payload = await response.json(); assert.equal(response.status, 200); assert.equal(payload.ok, false); assert.equal(payload.status, "blocked"); assert.equal(payload.blocker.code, "hwpod_node_unavailable"); assert.equal(payload.results[0].blocker.layer, "hwpod-node"); } finally { await close(server); } }); test("cloud-api forwards hwpod-node-ops to configured thin hwpod-node URL", async () => { const node = createHwpodNodeServer({ nodeId: "pc-host-1" }); await listen(node); const server = createCloudApiServer({ env: { PATH: process.env.PATH, HWLAB_HWPOD_NODE_OPS_URL: `${serverUrl(node)}/v1/hwpod-node-ops` } }); await listen(server); try { const plan = samplePlan(); plan.ops[0].args.workspacePath = process.cwd(); const response = await fetch(`${serverUrl(server)}/v1/hwpod-node-ops`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(plan) }); const payload = await response.json(); assert.equal(response.status, 200); assert.equal(payload.ok, true); assert.equal(payload.status, "completed"); assert.equal(payload.results[0].op, "workspace.ls"); assert.equal(payload.results[0].output.entries.some((entry: any) => entry.name === "package.json"), true); } finally { await close(server); await close(node); } }); test("cloud-api blocks direct hwpod-node URL when target node id does not match", async () => { const node = createHwpodNodeServer({ nodeId: "g14-host-hwpod-node" }); await listen(node); const server = createCloudApiServer({ env: { PATH: process.env.PATH, HWPOD_NODE_ID: "g14-host-hwpod-node", HWLAB_HWPOD_NODE_OPS_URL: `${serverUrl(node)}/v1/hwpod-node-ops` } }); await listen(server); try { const plan = samplePlan(); plan.nodeId = "node-d601-f103-v2"; plan.ops[0].args.workspacePath = "F:\\Work\\HWLAB-CASE-F103"; const response = await fetch(`${serverUrl(server)}/v1/hwpod-node-ops`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(plan) }); const payload = await response.json(); assert.equal(response.status, 200); assert.equal(payload.ok, false); assert.equal(payload.status, "blocked"); assert.equal(payload.blocker.code, "hwpod_node_id_mismatch"); assert.equal(payload.blocker.details.requestedNodeId, "node-d601-f103-v2"); assert.equal(payload.blocker.details.targetNodeId, "g14-host-hwpod-node"); assert.equal(payload.results[0].blocker.code, "hwpod_node_id_mismatch"); } finally { await close(server); await close(node); } }); test("cloud-api blocks direct hwpod-node URL when node identity is not visible", async () => { const target = createCloudApiServer({ hwpodNodeOpsHandler: async (plan: any) => ({ ok: true, status: "completed", results: plan.ops.map((op: any) => ({ opId: op.opId, op: op.op, ok: true, status: "completed", output: { shouldNotRun: true } })) }) }); await listen(target); const server = createCloudApiServer({ env: { PATH: process.env.PATH, HWLAB_HWPOD_NODE_OPS_URL: `${serverUrl(target)}/v1/hwpod-node-ops` } }); await listen(server); try { const response = await fetch(`${serverUrl(server)}/v1/hwpod-node-ops`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(samplePlan()) }); const payload = await response.json(); assert.equal(response.status, 200); assert.equal(payload.ok, false); assert.equal(payload.status, "blocked"); assert.equal(payload.blocker.code, "hwpod_node_identity_unverified"); assert.equal(payload.results[0].output, undefined); } finally { await close(server); await close(target); } }); test("cloud-api dispatches hwpod-node-ops to outbound WebSocket hwpod-node by nodeId", async () => { const root = await mkdtemp(path.join(os.tmpdir(), "hwlab-hwpod-node-ws-")); const runtime = await createCloudApiBunServer({ host: "127.0.0.1", port: 0, env: { PATH: process.env.PATH } }); const connector = connectHwpodNodeWs({ cloudUrl: runtime.url, nodeId: "pc-host-1", reconnect: false, heartbeatIntervalMs: 1000 }); try { await writeFile(path.join(root, "main.c"), "int main(void) { return 0; }\n", "utf8"); await withTimeout(connector.ready, 3000, "hwpod-node WebSocket registration timed out"); const plan = samplePlan(); plan.ops[0].args.workspacePath = root; const response = await fetch(`${runtime.url}/v1/hwpod-node-ops`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(plan) }); const payload = await response.json(); assert.equal(response.status, 200); assert.equal(payload.ok, true); assert.equal(payload.status, "completed"); assert.equal(payload.nodeId, "pc-host-1"); assert.equal(payload.results[0].op, "workspace.ls"); assert.equal(payload.results[0].output.entries.some((entry: any) => entry.name === "main.c"), true); } finally { connector.close(); runtime.stop(); await rm(root, { recursive: true, force: true }); } }); test("cloud-api rejects unsupported hwpod-node ops before forwarding", async () => { const server = createCloudApiServer({ hwpodNodeOpsHandler: async () => { throw new Error("must not be called"); } }); await listen(server); try { const plan = samplePlan(); plan.ops[0].op = "debug.flash"; const response = await fetch(`${serverUrl(server)}/v1/hwpod-node-ops`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(plan) }); const payload = await response.json(); assert.equal(response.status, 400); assert.equal(payload.ok, false); assert.equal(payload.error.code, "unsupported_hwpod_node_op"); } finally { await close(server); } }); function samplePlan() { return { contractVersion: "hwpod-node-ops-v1", planId: "hwpod_plan_test", hwpodId: "hwpod-local", nodeId: "pc-host-1", intent: "workspace.ls", ops: [ { opId: "op_01_workspace_ls", op: "workspace.ls", args: { workspacePath: "/workspace/fw", path: "." } } ] }; } function sampleSpecYaml() { return `apiVersion: hwlab.dev/v0alpha1 kind: Hwpod metadata: uid: D601-F103-V2 name: d601-f103-v2 spec: targetDevice: board: D601-F103-V2 mcu: STM32F103 workspace: path: "F:\\\\Work\\\\HWLAB-CASE-F103" toolchain: keil-mdk debugProbe: type: daplink ioProbe: uart: port: COM9 nodeBinding: nodeId: node-d601-f103-v2 nodeType: pc-host `; } function sampleConstartSpecYaml() { return `apiVersion: hwlab.dev/v0alpha1 kind: Hwpod metadata: uid: CONSTART-71FREQ-C name: constart-71freq-c spec: targetDevice: board: ConStart 71-FREQ Controller mcu: STM32F103 workspace: path: 'F:\\Work\\ConStart' toolchain: keil-mdk debugProbe: type: daplink serialNumber: 3FD750C63E342E24 ioProbe: uart: port: COM4 baudRate: 921600 nodeBinding: nodeId: node-d601-f103-v2 nodeType: pc-host `; } async function listen(server: any) { await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve)); } async function close(server: any) { await new Promise((resolve, reject) => server.close((error: Error | undefined) => (error ? reject(error) : resolve(undefined)))); } function serverUrl(server: any) { return `http://127.0.0.1:${server.address().port}`; } async function withTimeout(promise: Promise, timeoutMs: number, message: string) { let timer: ReturnType | null = null; try { return await Promise.race([ promise, new Promise((_resolve, reject) => { timer = setTimeout(() => reject(new Error(message)), timeoutMs); }) ]); } finally { if (timer !== null) clearTimeout(timer); } }