feat: add hwpod harness node-ops loop
This commit is contained in:
@@ -0,0 +1,152 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { test } from "bun:test";
|
||||
|
||||
import { createCloudApiServer } from "./server.ts";
|
||||
import { 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.ok(payload.hwpod.supportedOps.includes("workspace.ls"));
|
||||
} finally {
|
||||
await close(server);
|
||||
}
|
||||
});
|
||||
|
||||
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();
|
||||
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 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: "." } }
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
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}`;
|
||||
}
|
||||
Reference in New Issue
Block a user