119 lines
5.3 KiB
TypeScript
119 lines
5.3 KiB
TypeScript
import assert from "node:assert/strict";
|
|
import { mkdtemp, readFile, rm } from "node:fs/promises";
|
|
import os from "node:os";
|
|
import path from "node:path";
|
|
import { test } from "bun:test";
|
|
|
|
import { runHwpodCli, runHwpodCompilerCli, runHwpodCtl } from "./src/hwpod-harness-lib.ts";
|
|
|
|
const NOW = "2026-06-05T00:00:00.000Z";
|
|
|
|
test("hwpod-ctl initializes, validates, edits, and binds workspace-local hwpod-spec", async () => {
|
|
const root = await mkdtemp(path.join(os.tmpdir(), "hwlab-hwpod-ctl-"));
|
|
const specPath = path.join(root, ".hwlab", "hwpod-spec.yaml");
|
|
try {
|
|
const init = await runHwpodCtl([
|
|
"spec",
|
|
"init",
|
|
"--spec",
|
|
specPath,
|
|
"--name",
|
|
"stm32-local",
|
|
"--node",
|
|
"pc-host-1",
|
|
"--workspace",
|
|
"/workspace/fw"
|
|
], { now: () => NOW });
|
|
assert.equal(init.exitCode, 0);
|
|
assert.equal(init.payload.document.kind, "Hwpod");
|
|
assert.equal(init.payload.document.spec.nodeBinding.nodeId, "pc-host-1");
|
|
|
|
const validate = await runHwpodCtl(["spec", "validate", "--spec", specPath], { now: () => NOW });
|
|
assert.equal(validate.exitCode, 0);
|
|
assert.deepEqual(validate.payload.fourElements, ["targetDevice", "workspace", "debugProbe", "ioProbe"]);
|
|
|
|
const set = await runHwpodCtl(["spec", "set", "spec.targetDevice.id", "stm32f103", "--spec", specPath], { now: () => NOW });
|
|
assert.equal(set.exitCode, 0);
|
|
assert.equal(set.payload.document.spec.targetDevice.id, "stm32f103");
|
|
|
|
const bind = await runHwpodCtl(["bind", "--spec", specPath, "--node", "edge-gateway-1"], { now: () => NOW });
|
|
assert.equal(bind.exitCode, 0);
|
|
assert.equal(bind.payload.nodeId, "edge-gateway-1");
|
|
assert.match(await readFile(specPath, "utf8"), /nodeId: edge-gateway-1/u);
|
|
} finally {
|
|
await rm(root, { recursive: true, force: true });
|
|
}
|
|
});
|
|
test("hwpod-compiler-cli compiles workspace-local spec into node ops", async () => {
|
|
const root = await mkdtemp(path.join(os.tmpdir(), "hwlab-hwpod-compiler-"));
|
|
const specPath = path.join(root, ".hwlab", "hwpod-spec.yaml");
|
|
try {
|
|
await runHwpodCtl(["spec", "init", "--spec", specPath, "--node", "pc-host-1", "--workspace", "/workspace/fw"], { now: () => NOW });
|
|
const result = await runHwpodCompilerCli([
|
|
"compile",
|
|
"--spec",
|
|
specPath,
|
|
"--intent",
|
|
"workspace.ls",
|
|
"--args",
|
|
"{\"path\":\"src\"}"
|
|
], { now: () => NOW });
|
|
assert.equal(result.exitCode, 0);
|
|
assert.equal(result.payload.contractVersion, "hwpod-node-ops-v1");
|
|
assert.equal(result.payload.plan.nodeId, "pc-host-1");
|
|
assert.equal(result.payload.plan.ops.length, 1);
|
|
assert.equal(result.payload.plan.ops[0].op, "workspace.ls");
|
|
assert.equal(result.payload.plan.ops[0].args.path, "src");
|
|
assert.equal(result.payload.plan.ops[0].args.workspacePath, "/workspace/fw");
|
|
} finally {
|
|
await rm(root, { recursive: true, force: true });
|
|
}
|
|
});
|
|
test("hwpod-cli dry-run keeps high-level intent out of hwlab-api and exposes hwpod-node-ops plan", async () => {
|
|
const root = await mkdtemp(path.join(os.tmpdir(), "hwlab-hwpod-cli-"));
|
|
const specPath = path.join(root, ".hwlab", "hwpod-spec.yaml");
|
|
try {
|
|
await runHwpodCtl(["spec", "init", "--spec", specPath, "--node", "pc-host-1"], { now: () => NOW });
|
|
const inspect = await runHwpodCli(["inspect", "--spec", specPath, "--dry-run"], { now: () => NOW });
|
|
assert.equal(inspect.exitCode, 0);
|
|
assert.equal(inspect.payload.dryRun, true);
|
|
assert.deepEqual(inspect.payload.plan.ops.map((op: any) => op.op), ["node.health", "node.inventory"]);
|
|
|
|
const build = await runHwpodCli(["build", "--spec", specPath, "--dry-run", "--target", "Debug"], { now: () => NOW });
|
|
assert.equal(build.exitCode, 0);
|
|
assert.equal(build.payload.plan.ops[0].op, "debug.build");
|
|
assert.equal(build.payload.plan.ops[0].args.target, "Debug");
|
|
} finally {
|
|
await rm(root, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test("hwpod-cli submits compiled node ops to hwlab-api when not dry-run", async () => {
|
|
const root = await mkdtemp(path.join(os.tmpdir(), "hwlab-hwpod-cli-submit-"));
|
|
const specPath = path.join(root, ".hwlab", "hwpod-spec.yaml");
|
|
const seen: any[] = [];
|
|
try {
|
|
await runHwpodCtl(["spec", "init", "--spec", specPath, "--node", "pc-host-1"], { now: () => NOW });
|
|
const result = await runHwpodCli(["workspace", "ls", "src", "--spec", specPath], {
|
|
env: {
|
|
HWLAB_RUNTIME_API_URL: "http://cloud.test",
|
|
HWLAB_RUNTIME_NAMESPACE: "hwlab-v02",
|
|
HWLAB_RUNTIME_ENDPOINT_LOCKED: "1",
|
|
HWLAB_API_KEY: "hwl_live_test"
|
|
},
|
|
fetchImpl: async (url, init) => {
|
|
seen.push({ url: String(url), init, body: JSON.parse(String(init?.body ?? "{}")) });
|
|
return new Response(JSON.stringify({ ok: true, status: "completed", contractVersion: "hwpod-node-ops-v1", results: [{ op: "workspace.ls", ok: true }] }), { status: 200, headers: { "content-type": "application/json" } });
|
|
},
|
|
now: () => NOW
|
|
});
|
|
assert.equal(result.exitCode, 0);
|
|
assert.equal(seen[0].url, "http://cloud.test/v1/hwpod-node-ops");
|
|
assert.equal(seen[0].init.headers["x-hwlab-device-pod-api-key"], "hwl_live_test");
|
|
assert.equal(seen[0].body.contractVersion, "hwpod-node-ops-v1");
|
|
assert.equal(seen[0].body.ops[0].op, "workspace.ls");
|
|
} finally {
|
|
await rm(root, { recursive: true, force: true });
|
|
}
|
|
});
|