1242 lines
57 KiB
TypeScript
1242 lines
57 KiB
TypeScript
import assert from "node:assert/strict";
|
|
import { spawnSync } from "node:child_process";
|
|
import { createHash } from "node:crypto";
|
|
import { mkdir, mkdtemp, readdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
import os from "node:os";
|
|
import path from "node:path";
|
|
import { pathToFileURL } from "node:url";
|
|
import { test } from "bun:test";
|
|
|
|
import { createHwpodNodeServer, executeHwpodNodeOpsPlan, resolveHwpodNodeCommand } from "./src/hwpod-node-lib.ts";
|
|
|
|
test("hwpod-node executes minimal workspace node ops", async () => {
|
|
const root = await mkdtemp(path.join(os.tmpdir(), "hwlab-hwpod-node-"));
|
|
try {
|
|
await writeFile(path.join(root, "main.c"), "int main(void) { return 0; }\n", "utf8");
|
|
const result = await executeHwpodNodeOpsPlan({
|
|
contractVersion: "hwpod-node-ops-v1",
|
|
planId: "hwpod_plan_test",
|
|
hwpodId: "hwpod-local",
|
|
nodeId: "pc-host-1",
|
|
ops: [
|
|
{ opId: "op_01", op: "node.health", args: { workspacePath: root } },
|
|
{ opId: "op_02", op: "workspace.ls", args: { workspacePath: root, path: "." } },
|
|
{ opId: "op_03", op: "workspace.cat", args: { workspacePath: root, path: "main.c" } }
|
|
]
|
|
}, { now: () => "2026-06-05T00:00:00.000Z" });
|
|
assert.equal(result.ok, true);
|
|
assert.equal(result.status, "completed");
|
|
assert.equal(result.results[1].output.entries[0].name, "main.c");
|
|
assert.match(result.results[2].output.content, /int main/u);
|
|
} finally {
|
|
await rm(root, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test("hwpod-node materializes a verified Git resource atomically and reuses matching provenance", async () => {
|
|
const root = await mkdtemp(path.join(os.tmpdir(), "hwlab-hwpod-node-git-materialize-"));
|
|
try {
|
|
const source = path.join(root, "source");
|
|
await mkdir(source, { recursive: true });
|
|
for (const args of [["init", source], ["-C", source, "config", "user.email", "hwpod-test@example.invalid"], ["-C", source, "config", "user.name", "HWPOD Test"]]) {
|
|
const command = spawnSync("git", args, { encoding: "utf8" });
|
|
assert.equal(command.status, 0, command.stderr);
|
|
}
|
|
await writeFile(path.join(source, "resource.txt"), "verified resource\n", "utf8");
|
|
const added = spawnSync("git", ["-C", source, "add", "resource.txt"], { encoding: "utf8" });
|
|
assert.equal(added.status, 0, added.stderr);
|
|
const committed = spawnSync("git", ["-C", source, "commit", "-m", "fixture"], { encoding: "utf8" });
|
|
assert.equal(committed.status, 0, committed.stderr);
|
|
const head = spawnSync("git", ["-C", source, "rev-parse", "HEAD"], { encoding: "utf8" });
|
|
assert.equal(head.status, 0, head.stderr);
|
|
const commitId = head.stdout.trim();
|
|
const basePlan = {
|
|
contractVersion: "hwpod-node-ops-v1",
|
|
planId: "hwpod_plan_git_materialize",
|
|
hwpodId: "hwpod-local",
|
|
nodeId: "pc-host-1",
|
|
ops: [{ opId: "op_git", op: "workspace.git-materialize", args: { workspacePath: root, destination: "third_party/resource", repoUrl: pathToFileURL(source).href, ref: "HEAD", commitId } }]
|
|
};
|
|
const first = await executeHwpodNodeOpsPlan(basePlan, { now: () => "2026-07-25T00:00:00.000Z" });
|
|
assert.equal(first.ok, true);
|
|
assert.equal(first.results[0].output.materialized, true);
|
|
assert.equal(first.results[0].output.reused, false);
|
|
assert.equal(await readFile(path.join(root, "third_party", "resource", "resource.txt"), "utf8"), "verified resource\n");
|
|
|
|
const second = await executeHwpodNodeOpsPlan(basePlan, { now: () => "2026-07-25T00:00:00.000Z" });
|
|
assert.equal(second.ok, true);
|
|
assert.equal(second.results[0].output.reused, true);
|
|
|
|
const bad = await executeHwpodNodeOpsPlan({ ...basePlan, planId: "hwpod_plan_git_materialize_bad", ops: [{ opId: "op_bad", op: "workspace.git-materialize", args: { ...basePlan.ops[0].args, destination: "third_party/bad", commitId: "0".repeat(40) } }] }, { now: () => "2026-07-25T00:00:00.000Z" });
|
|
assert.equal(bad.ok, false);
|
|
assert.equal(bad.results[0].blocker.code, "hwpod_git_materialize_failed");
|
|
assert.equal((await readdir(path.join(root, "third_party"))).some((entry) => entry.startsWith(".bad.hwpod-")), false);
|
|
|
|
await mkdir(path.join(root, "third_party", "conflict"), { recursive: true });
|
|
const conflict = await executeHwpodNodeOpsPlan({ ...basePlan, planId: "hwpod_plan_git_materialize_conflict", ops: [{ opId: "op_conflict", op: "workspace.git-materialize", args: { ...basePlan.ops[0].args, destination: "third_party/conflict" } }] }, { now: () => "2026-07-25T00:00:00.000Z" });
|
|
assert.equal(conflict.ok, false);
|
|
assert.equal(conflict.results[0].blocker.code, "hwpod_git_destination_conflict");
|
|
} finally {
|
|
await rm(root, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test("python hwpod-node materializes Git resources with the same atomic contract", () => {
|
|
const script = String.raw`
|
|
import importlib.util
|
|
import json
|
|
import logging
|
|
import pathlib
|
|
import subprocess
|
|
import tempfile
|
|
|
|
spec = importlib.util.spec_from_file_location("hwlab_node", "tools/hwlab-node.py")
|
|
module = importlib.util.module_from_spec(spec)
|
|
spec.loader.exec_module(module)
|
|
root = pathlib.Path(tempfile.mkdtemp()).resolve()
|
|
source = root / "source"
|
|
source.mkdir()
|
|
subprocess.run(["git", "init", str(source)], check=True, capture_output=True)
|
|
subprocess.run(["git", "-C", str(source), "config", "user.email", "hwpod-test@example.invalid"], check=True)
|
|
subprocess.run(["git", "-C", str(source), "config", "user.name", "HWPOD Test"], check=True)
|
|
(source / "resource.txt").write_text("verified resource\n", encoding="utf-8")
|
|
subprocess.run(["git", "-C", str(source), "add", "resource.txt"], check=True)
|
|
subprocess.run(["git", "-C", str(source), "commit", "-m", "fixture"], check=True, capture_output=True)
|
|
commit_id = subprocess.check_output(["git", "-C", str(source), "rev-parse", "HEAD"], text=True).strip()
|
|
executor = module.NodeOpsExecutor({"nodeId": "node-test", "allowedWorkspaceRoots": [str(root)]}, logging.getLogger("test"))
|
|
args = {"workspacePath": str(root), "destination": "third_party/resource", "repoUrl": source.as_uri(), "ref": "HEAD", "commitId": commit_id}
|
|
first = executor._execute_op({"opId": "op_first", "op": "workspace.git-materialize", "args": args})
|
|
second = executor._execute_op({"opId": "op_second", "op": "workspace.git-materialize", "args": args})
|
|
bad_args = {**args, "destination": "third_party/bad", "commitId": "0" * 40}
|
|
bad = executor._execute_op({"opId": "op_bad", "op": "workspace.git-materialize", "args": bad_args})
|
|
outside = pathlib.Path(tempfile.mkdtemp()).resolve()
|
|
outside_args = {**args, "destination": "third_party/outside", "repoUrl": outside.as_uri()}
|
|
outside_result = executor._execute_op({"opId": "op_outside", "op": "workspace.git-materialize", "args": outside_args})
|
|
print(json.dumps({
|
|
"first": first,
|
|
"second": second,
|
|
"bad": bad,
|
|
"badTemporaryExists": any((root / "third_party").glob(".bad.hwpod-*")),
|
|
"outside": outside_result,
|
|
}))
|
|
`;
|
|
const completed = spawnSync("python3", ["-c", script], { cwd: process.cwd(), encoding: "utf8" });
|
|
assert.equal(completed.status, 0, completed.stderr || completed.stdout);
|
|
const result = JSON.parse(completed.stdout);
|
|
assert.equal(result.first.ok, true);
|
|
assert.equal(result.first.output.reused, false);
|
|
assert.equal(result.second.ok, true);
|
|
assert.equal(result.second.output.reused, true);
|
|
assert.equal(result.bad.ok, false);
|
|
assert.equal(result.bad.blocker.code, "hwpod_git_materialize_failed");
|
|
assert.equal(result.badTemporaryExists, false);
|
|
assert.equal(result.outside.ok, false);
|
|
assert.equal(result.outside.blocker.code, "hwpod_git_repo_url_invalid");
|
|
});
|
|
|
|
test("hwpod-node searches workspace text without requiring host rg", async () => {
|
|
const root = await mkdtemp(path.join(os.tmpdir(), "hwlab-hwpod-node-rg-"));
|
|
try {
|
|
await mkdir(path.join(root, "Middlewares", "Arm-2D", ".git", "objects"), { recursive: true });
|
|
await mkdir(path.join(root, "Objects"), { recursive: true });
|
|
await writeFile(path.join(root, "Middlewares", "Arm-2D", "arm_2d.c"), "static int before;\nvoid arm_2d_init(void) {\n}\nstatic int after;\n", "utf8");
|
|
await writeFile(path.join(root, "Middlewares", "Arm-2D", "arm_2d.h"), "void arm_2d_init(void);\n", "utf8");
|
|
await writeFile(path.join(root, "Middlewares", "Arm-2D", ".git", "objects", "ignored.c"), "arm_2d_init\n", "utf8");
|
|
await writeFile(path.join(root, "Objects", "ignored.txt"), "arm_2d_init should not be scanned\n", "utf8");
|
|
|
|
const result = await executeHwpodNodeOpsPlan({
|
|
contractVersion: "hwpod-node-ops-v1",
|
|
planId: "hwpod_plan_rg",
|
|
hwpodId: "hwpod-local",
|
|
nodeId: "pc-host-1",
|
|
ops: [{ opId: "op_rg", op: "workspace.rg", args: { workspacePath: root, pattern: "arm_2d_init", path: "Middlewares", glob: "*.c", context: 1 } }]
|
|
}, { now: () => "2026-06-05T00:00:00.000Z" });
|
|
|
|
assert.equal(result.ok, true);
|
|
assert.equal(result.results[0].ok, true);
|
|
assert.equal(result.results[0].output.engine, "node-recursive-search");
|
|
assert.equal(result.results[0].output.matchCount, 1);
|
|
assert.equal(result.results[0].output.scannedFiles, 1);
|
|
assert.equal(result.results[0].output.glob, "*.c");
|
|
assert.ok(result.results[0].output.stdout.includes("Middlewares/Arm-2D/arm_2d.c-1-static int before"));
|
|
assert.ok(result.results[0].output.stdout.includes("Middlewares/Arm-2D/arm_2d.c:2:void arm_2d_init"));
|
|
assert.ok(result.results[0].output.stdout.includes("Middlewares/Arm-2D/arm_2d.c-3-}"));
|
|
assert.equal(result.results[0].output.matches[0].line, 2);
|
|
assert.equal(result.results[0].output.matches[0].before[0].line, 1);
|
|
assert.equal(result.results[0].output.matches[0].after[0].line, 3);
|
|
assert.equal(result.results[0].output.limits.beforeContext, 1);
|
|
} finally {
|
|
await rm(root, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test("python hwpod-node searches a nested Git resource from a .worktree workspace", () => {
|
|
const script = String.raw`
|
|
import importlib.util, json, logging, pathlib, tempfile
|
|
spec = importlib.util.spec_from_file_location("hwlab_node", "tools/hwlab-node.py")
|
|
module = importlib.util.module_from_spec(spec)
|
|
spec.loader.exec_module(module)
|
|
root = pathlib.Path(tempfile.mkdtemp()) / ".worktree" / "case-run"
|
|
source = root / "third_party" / "Arm-2D"
|
|
(source / ".git" / "objects").mkdir(parents=True)
|
|
(source / "arm_2d.c").write_text("void arm_2d_init(void) {}\n", encoding="utf-8")
|
|
(source / "arm_2d.h").write_text("void arm_2d_init(void);\n", encoding="utf-8")
|
|
(source / ".git" / "objects" / "ignored.c").write_text("arm_2d_init\n", encoding="utf-8")
|
|
executor = module.NodeOpsExecutor({"nodeId": "node-test", "allowedWorkspaceRoots": [str(root)]}, logging.getLogger("test"))
|
|
result = executor._workspace_rg({"workspacePath": str(root), "path": "third_party/Arm-2D", "pattern": "arm_2d_init", "glob": "*.c"})
|
|
print(json.dumps(result))
|
|
`;
|
|
const completed = spawnSync("python3", ["-c", script], { cwd: process.cwd(), encoding: "utf8" });
|
|
assert.equal(completed.status, 0, completed.stderr || completed.stdout);
|
|
const result = JSON.parse(completed.stdout);
|
|
assert.equal(result.scannedFiles, 1);
|
|
assert.equal(result.matches.length, 1);
|
|
assert.equal(result.matches[0].path, "third_party/Arm-2D/arm_2d.c");
|
|
assert.ok(result.skippedDirectories >= 1);
|
|
});
|
|
|
|
test("hwpod-node HTTP endpoint accepts hwpod-node-ops plans", async () => {
|
|
const server = createHwpodNodeServer({ now: () => "2026-06-05T00:00:00.000Z", nodeId: "pc-host-1" });
|
|
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
|
try {
|
|
const response = await fetch(`http://127.0.0.1:${server.address().port}/v1/hwpod-node-ops`, {
|
|
method: "POST",
|
|
headers: { "content-type": "application/json" },
|
|
body: JSON.stringify({
|
|
contractVersion: "hwpod-node-ops-v1",
|
|
planId: "hwpod_plan_http",
|
|
hwpodId: "hwpod-local",
|
|
nodeId: "pc-host-1",
|
|
ops: [{ opId: "op_01", op: "node.version", args: {} }]
|
|
})
|
|
});
|
|
const payload = await response.json();
|
|
assert.equal(response.status, 200);
|
|
assert.equal(payload.ok, true);
|
|
assert.equal(payload.specAuthority, "none");
|
|
assert.equal(payload.results[0].op, "node.version");
|
|
} finally {
|
|
await new Promise((resolve, reject) => server.close((error: Error | undefined) => (error ? reject(error) : resolve(undefined))));
|
|
}
|
|
});
|
|
|
|
test("hwpod-node executes debug ops only through explicit node-side command bindings", async () => {
|
|
const root = await mkdtemp(path.join(os.tmpdir(), "hwlab-hwpod-node-debug-"));
|
|
try {
|
|
const result = await executeHwpodNodeOpsPlan({
|
|
contractVersion: "hwpod-node-ops-v1",
|
|
planId: "hwpod_plan_debug_bound",
|
|
hwpodId: "hwpod-local",
|
|
nodeId: "pc-host-1",
|
|
ops: [{ opId: "op_build", op: "debug.build", args: { workspacePath: root, command: "printf build-ok > build.ok" } }]
|
|
}, { now: () => "2026-06-05T00:00:00.000Z" });
|
|
|
|
assert.equal(result.ok, true);
|
|
assert.equal(result.results[0].ok, true);
|
|
assert.equal(result.results[0].output.bindingSource, "hwpod-node-ops.command");
|
|
assert.match(await readFile(path.join(root, "build.ok"), "utf8"), /build-ok/u);
|
|
} finally {
|
|
await rm(root, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test("hwpod-node executes debug commandRuns without shell splitting", async () => {
|
|
const root = await mkdtemp(path.join(os.tmpdir(), "hwlab-hwpod-node-debug-runs-"));
|
|
try {
|
|
const writer = path.join(root, "writer.js");
|
|
await writeFile(writer, "const { writeFileSync } = require('node:fs'); writeFileSync(process.argv[2], process.argv.slice(3).join('|'));\n", "utf8");
|
|
const result = await executeHwpodNodeOpsPlan({
|
|
contractVersion: "hwpod-node-ops-v1",
|
|
planId: "hwpod_plan_debug_runs",
|
|
hwpodId: "hwpod-local",
|
|
nodeId: "pc-host-1",
|
|
ops: [{ opId: "op_download", op: "debug.download", args: { workspacePath: root, commandRuns: [{ command: process.execPath, argv: [writer, path.join(root, "args.txt"), "CherryUSB MicroLink CMSIS-DAP"] }] } }]
|
|
}, { now: () => "2026-06-05T00:00:00.000Z" });
|
|
|
|
assert.equal(result.ok, true);
|
|
assert.equal(result.results[0].ok, true);
|
|
assert.equal(result.results[0].output.bindingSource, "hwpod-node-ops.commandRuns");
|
|
assert.equal(result.results[0].output.commands[0].command.at(-1), "CherryUSB MicroLink CMSIS-DAP");
|
|
assert.equal(await readFile(path.join(root, "args.txt"), "utf8"), "CherryUSB MicroLink CMSIS-DAP");
|
|
} finally {
|
|
await rm(root, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test("hwpod-node reports cmd.run non-zero exits as failed results", async () => {
|
|
const root = await mkdtemp(path.join(os.tmpdir(), "hwlab-hwpod-node-cmd-fail-"));
|
|
try {
|
|
const result = await executeHwpodNodeOpsPlan({
|
|
contractVersion: "hwpod-node-ops-v1",
|
|
planId: "hwpod_plan_cmd_fail",
|
|
hwpodId: "hwpod-local",
|
|
nodeId: "pc-host-1",
|
|
ops: [{ opId: "op_cmd", op: "cmd.run", args: { workspacePath: root, command: "sh", argv: ["-c", "echo no >&2; exit 7"] } }]
|
|
}, { now: () => "2026-06-05T00:00:00.000Z" });
|
|
|
|
assert.equal(result.ok, false);
|
|
assert.equal(result.results[0].ok, false);
|
|
assert.equal(result.results[0].status, "failed");
|
|
assert.equal(result.results[0].output.exitCode, 7);
|
|
assert.equal(result.results[0].blocker.code, "hwpod_node_command_failed");
|
|
} finally {
|
|
await rm(root, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test("hwpod-node projects bound command JSON into authoritative observation", async () => {
|
|
const root = await mkdtemp(path.join(os.tmpdir(), "hwlab-hwpod-node-observation-"));
|
|
try {
|
|
const result = await executeHwpodNodeOpsPlan({
|
|
contractVersion: "hwpod-node-ops-v1",
|
|
planId: "hwpod_plan_observation",
|
|
hwpodId: "hwpod-local",
|
|
nodeId: "pc-host-1",
|
|
ops: [{
|
|
opId: "op_observation",
|
|
op: "cmd.run",
|
|
args: {
|
|
workspacePath: root,
|
|
command: process.execPath,
|
|
argv: ["-e", "console.log(JSON.stringify({ observationId: 'obs-node', response: { ok: true } }))"],
|
|
commandBinding: { kind: "board-comm" }
|
|
}
|
|
}]
|
|
}, { now: () => "2026-07-16T00:00:00.000Z" });
|
|
|
|
assert.equal(result.ok, true);
|
|
assert.equal(result.results[0].output.observation.observationId, "obs-node");
|
|
assert.equal(result.results[0].output.observation.response.ok, true);
|
|
} finally {
|
|
await rm(root, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test("hwpod-node refuses plans targeting another node id", async () => {
|
|
const root = await mkdtemp(path.join(os.tmpdir(), "hwlab-hwpod-node-id-mismatch-"));
|
|
try {
|
|
const result = await executeHwpodNodeOpsPlan({
|
|
contractVersion: "hwpod-node-ops-v1",
|
|
planId: "hwpod_plan_node_mismatch",
|
|
hwpodId: "hwpod-local",
|
|
nodeId: "node-d601-f103-v2",
|
|
ops: [{ opId: "op_cmd", op: "cmd.run", args: { workspacePath: root, command: "sh", argv: ["-c", "echo must-not-run"] } }]
|
|
}, { now: () => "2026-06-05T00:00:00.000Z", nodeId: "g14-host-hwpod-node" });
|
|
|
|
assert.equal(result.ok, false);
|
|
assert.equal(result.status, "blocked");
|
|
assert.equal(result.localNodeId, "g14-host-hwpod-node");
|
|
assert.equal(result.results[0].blocker.code, "hwpod_node_id_mismatch");
|
|
assert.equal(result.results[0].blocker.details.requestedNodeId, "node-d601-f103-v2");
|
|
assert.equal(result.results[0].blocker.details.localNodeId, "g14-host-hwpod-node");
|
|
} finally {
|
|
await rm(root, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test("hwpod-node exposes command spawn diagnostics when executable is missing", async () => {
|
|
const root = await mkdtemp(path.join(os.tmpdir(), "hwlab-hwpod-node-spawn-missing-"));
|
|
try {
|
|
const result = await executeHwpodNodeOpsPlan({
|
|
contractVersion: "hwpod-node-ops-v1",
|
|
planId: "hwpod_plan_spawn_missing",
|
|
hwpodId: "hwpod-local",
|
|
nodeId: "pc-host-1",
|
|
ops: [{ opId: "op_cmd", op: "cmd.run", args: { workspacePath: root, command: "hwlab-command-that-does-not-exist-1018", argv: ["--version"] } }]
|
|
}, { now: () => "2026-06-05T00:00:00.000Z", nodeId: "pc-host-1" });
|
|
|
|
assert.equal(result.ok, false);
|
|
assert.equal(result.results[0].status, "blocked");
|
|
assert.equal(result.results[0].blocker.code, "hwpod_node_command_spawn_failed");
|
|
assert.equal(result.results[0].blocker.details.output.commandResolution.requested, "hwlab-command-that-does-not-exist-1018");
|
|
assert.equal(result.results[0].blocker.details.output.spawnDiagnostics.platform, process.platform);
|
|
} finally {
|
|
await rm(root, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test("hwpod-node includes structured UART diagnostics when platform blocks serial access", async () => {
|
|
const root = await mkdtemp(path.join(os.tmpdir(), "hwlab-hwpod-node-uart-"));
|
|
try {
|
|
const fixture = path.join(root, "serial-monitor-idle.mjs");
|
|
await writeFile(fixture, `
|
|
const args = process.argv.slice(2).join(" ");
|
|
if (args === "monitor status") console.log(JSON.stringify({ action: "monitor_status", success: true, data: { isMonitoring: false, port: null, baudRate: null, lastPort: "COM9", lastBaudRate: 115200 } }));
|
|
else if (args === "ports") console.log(JSON.stringify({ action: "list_ports", success: true, data: { ports: [{ path: "COM9" }] } }));
|
|
else { console.error("unsupported " + args); process.exit(2); }
|
|
`, "utf8");
|
|
const result = await executeHwpodNodeOpsPlan({
|
|
contractVersion: "hwpod-node-ops-v1",
|
|
planId: "hwpod_plan_uart_diag",
|
|
hwpodId: "hwpod-local",
|
|
nodeId: "pc-host-1",
|
|
ops: [{
|
|
opId: "op_uart",
|
|
op: "io.uart.read",
|
|
args: {
|
|
workspacePath: root,
|
|
port: "uart1",
|
|
serialMonitorDir: root,
|
|
serialMonitorCommand: [process.execPath, fixture],
|
|
ioProbe: { uart: { id: "uart1", port: "COM9", baudrate: 115200 } }
|
|
}
|
|
}]
|
|
}, { now: () => "2026-06-05T00:00:00.000Z" });
|
|
|
|
if (process.platform === "win32") return;
|
|
assert.equal(result.ok, false);
|
|
assert.equal(result.results[0].status, "blocked");
|
|
assert.equal(result.results[0].blocker.code, "hwpod_uart_monitor_start_failed");
|
|
assert.equal(result.results[0].blocker.details.requestedPort, "uart1");
|
|
assert.equal(result.results[0].blocker.details.resolvedPort, "COM9");
|
|
assert.equal(result.results[0].blocker.details.baudRate, 115200);
|
|
assert.equal(result.results[0].blocker.details.serialMonitor.monitorStatus.data.isMonitoring, false);
|
|
assert.equal(result.results[0].blocker.details.serialMonitor.start.success, undefined);
|
|
} finally {
|
|
await rm(root, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test("hwpod-node reads UART through serial-monitor binding", async () => {
|
|
const root = await mkdtemp(path.join(os.tmpdir(), "hwlab-hwpod-node-uart-active-"));
|
|
try {
|
|
const fixture = path.join(root, "serial-monitor-fixture.mjs");
|
|
await writeFile(fixture, `
|
|
import { existsSync, writeFileSync } from "node:fs";
|
|
const marker = ${JSON.stringify(path.join(root, "monitor-started"))};
|
|
const args = process.argv.slice(2).join(" ");
|
|
if (args === "monitor status") console.log(JSON.stringify({ action: "monitor_status", success: true, data: existsSync(marker) ? { isMonitoring: true, port: "COM9", baudRate: 115200 } : { isMonitoring: false, port: null, baudRate: null, lastPort: "COM9", lastBaudRate: 115200 } }));
|
|
else if (args === "monitor start -p COM9 -b 115200") { writeFileSync(marker, "started"); console.log(JSON.stringify({ action: "monitor_start", success: true, data: { port: "COM9", baudRate: 115200 } })); }
|
|
else if (args === "ports") console.log(JSON.stringify({ action: "list_ports", success: true, data: { ports: [{ path: "COM9" }] } }));
|
|
else if (args.startsWith("fetch")) console.log(JSON.stringify({ action: "fetch_data", success: true, data: [{ timestamp: "2026-06-06T10:00:01Z", port: "COM9", data: "UART OK" }], count: 1, totalCount: 1, hasMore: false, truncated: false, sourceFile: "fixture.jsonl" }));
|
|
else { console.error("unsupported " + args); process.exit(2); }
|
|
`, "utf8");
|
|
|
|
const result = await executeHwpodNodeOpsPlan({
|
|
contractVersion: "hwpod-node-ops-v1",
|
|
planId: "hwpod_plan_uart_read",
|
|
hwpodId: "hwpod-local",
|
|
nodeId: "pc-host-1",
|
|
ops: [{
|
|
opId: "op_uart",
|
|
op: "io.uart.read",
|
|
args: {
|
|
workspacePath: root,
|
|
port: "uart1",
|
|
maxBytes: 128,
|
|
serialMonitorDir: root,
|
|
serialMonitorCommand: [process.execPath, fixture],
|
|
ioProbe: { uart: { id: "uart1", port: "COM9", baudrate: 115200 } }
|
|
}
|
|
}]
|
|
}, { now: () => "2026-06-05T00:00:00.000Z" });
|
|
|
|
assert.equal(result.ok, true);
|
|
assert.equal(result.results[0].status, "completed");
|
|
assert.equal(result.results[0].output.bindingSource, "serial-monitor-cli");
|
|
assert.equal(result.results[0].output.resolvedPort, "COM9");
|
|
assert.equal(result.results[0].output.text, "UART OK");
|
|
assert.equal(result.results[0].output.monitorStarted, true);
|
|
assert.equal(result.results[0].output.monitorStart.success, true);
|
|
assert.equal(result.results[0].output.monitorStatus.data.isMonitoring, true);
|
|
assert.equal(result.results[0].output.monitorStatus.data.port, "COM9");
|
|
assert.equal(result.results[0].output.monitorStatus.data.baudRate, 115200);
|
|
assert.deepEqual(result.results[0].output.command.slice(0, 2), [process.execPath, fixture]);
|
|
} finally {
|
|
await rm(root, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test("hwpod-node blocks UART fetch when start does not establish requested binding", async () => {
|
|
const root = await mkdtemp(path.join(os.tmpdir(), "hwlab-hwpod-node-uart-mismatch-"));
|
|
try {
|
|
const fixture = path.join(root, "serial-monitor-mismatch.mjs");
|
|
await writeFile(fixture, `
|
|
const args = process.argv.slice(2).join(" ");
|
|
if (args === "monitor status") console.log(JSON.stringify({ success: true, data: { isMonitoring: false, port: null, baudRate: null } }));
|
|
else if (args === "monitor start -p COM9 -b 115200") console.log(JSON.stringify({ success: true, data: { port: "COM9", baudRate: 115200 } }));
|
|
else if (args.startsWith("fetch")) { console.error("fetch must not run"); process.exit(3); }
|
|
else { console.error("unsupported " + args); process.exit(2); }
|
|
`, "utf8");
|
|
const result = await executeHwpodNodeOpsPlan({
|
|
contractVersion: "hwpod-node-ops-v1",
|
|
planId: "hwpod_plan_uart_mismatch",
|
|
hwpodId: "hwpod-local",
|
|
nodeId: "pc-host-1",
|
|
ops: [{ opId: "op_uart", op: "io.uart.read", args: { workspacePath: root, port: "COM9", baudRate: 115200, serialMonitorDir: root, serialMonitorCommand: [process.execPath, fixture] } }]
|
|
}, { now: () => "2026-06-05T00:00:00.000Z" });
|
|
assert.equal(result.ok, false);
|
|
assert.equal(result.results[0].blocker.code, "hwpod_uart_monitor_binding_mismatch");
|
|
assert.equal(result.results[0].blocker.details.serialMonitor.confirmedStatus.data.isMonitoring, false);
|
|
} finally {
|
|
await rm(root, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test("python hwpod-node returns typed blocker for non-JSON UART start response", () => {
|
|
const script = `
|
|
import importlib.util, json, logging
|
|
spec = importlib.util.spec_from_file_location("hwlab_node", "tools/hwlab-node.py")
|
|
module = importlib.util.module_from_spec(spec)
|
|
spec.loader.exec_module(module)
|
|
class Executor(module.NodeOpsExecutor):
|
|
def _spawn_output(self, command, cwd, timeout_ms):
|
|
action = " ".join(command[-2:])
|
|
if command[-2:] == ["monitor", "status"]:
|
|
return {"ok": True, "stdout": json.dumps({"success": True, "data": {"isMonitoring": False}}), "stderr": "", "exitCode": 0}
|
|
if "start" in command:
|
|
return {"ok": True, "stdout": "not-json", "stderr": "", "exitCode": 0}
|
|
return {"ok": True, "stdout": json.dumps({"success": True, "data": {"ports": []}}), "stderr": "", "exitCode": 0}
|
|
executor = Executor({"nodeId": "node-test"}, logging.getLogger("test"))
|
|
print(json.dumps(executor._uart_read({"workspacePath": ".", "port": "COM9", "baudRate": 115200, "serialMonitorCommand": ["serial-monitor"]})))
|
|
`;
|
|
const completed = spawnSync("python3", ["-c", script], { cwd: process.cwd(), encoding: "utf8" });
|
|
assert.equal(completed.status, 0, completed.stderr);
|
|
const result = JSON.parse(completed.stdout);
|
|
assert.equal(result.ok, false);
|
|
assert.equal(result.blockerCode, "hwpod_uart_monitor_start_failed");
|
|
assert.equal(result.details.serialMonitor.start.stdout, "not-json");
|
|
});
|
|
|
|
test("python hwpod-node applies v2 workspace patches with typed diagnostics", async () => {
|
|
const root = await mkdtemp(path.join(os.tmpdir(), "hwlab-python-hwpod-patch-"));
|
|
try {
|
|
await writeFile(path.join(root, "main.c"), "int value = 1;\r\n", "utf8");
|
|
await writeFile(path.join(root, "remove.txt"), "remove me\n", "utf8");
|
|
const script = `
|
|
import base64, importlib.util, json, logging, pathlib, sys
|
|
spec = importlib.util.spec_from_file_location("hwlab_node", "tools/hwlab-node.py")
|
|
module = importlib.util.module_from_spec(spec)
|
|
spec.loader.exec_module(module)
|
|
root = pathlib.Path(sys.argv[1]).resolve()
|
|
executor = module.NodeOpsExecutor({"nodeId": "node-test", "allowedWorkspaceRoots": [str(root)]}, logging.getLogger("test"))
|
|
def execute(op_id, patch_arg):
|
|
return executor._execute_op({"opId": op_id, "op": "workspace.apply-patch", "args": {"workspacePath": str(root), **patch_arg}})
|
|
update = """*** Begin Patch
|
|
*** Update File: main.c
|
|
@@
|
|
-int value = 1;
|
|
+int value = 2;
|
|
*** End Patch
|
|
"""
|
|
combined = """*** Begin Patch
|
|
*** Add File: added.txt
|
|
+added
|
|
*** Update File: main.c
|
|
*** Move to: moved.c
|
|
@@
|
|
-int value = 2;
|
|
+int value = 3;
|
|
*** Delete File: remove.txt
|
|
*** End Patch
|
|
"""
|
|
raw = """--- a/main.c
|
|
+++ b/main.c
|
|
@@ -1 +1 @@
|
|
-old
|
|
+new
|
|
"""
|
|
stale = """*** Begin Patch
|
|
*** Update File: moved.c
|
|
@@
|
|
-int value = 99;
|
|
+int value = 4;
|
|
*** End Patch
|
|
"""
|
|
escape = """*** Begin Patch
|
|
*** Add File: ../outside.txt
|
|
+outside
|
|
*** End Patch
|
|
"""
|
|
results = {
|
|
"capabilities": executor.capabilities(),
|
|
"update": execute("update", {"patchBase64": base64.b64encode(update.encode()).decode()}),
|
|
"combined": execute("combined", {"patchContent": combined}),
|
|
"raw": execute("raw", {"patch": raw}),
|
|
"stale": execute("stale", {"patch": stale}),
|
|
"escape": execute("escape", {"patch": escape}),
|
|
}
|
|
print(json.dumps(results))
|
|
`;
|
|
const completed = spawnSync("python3", ["-c", script, root], { cwd: process.cwd(), encoding: "utf8" });
|
|
assert.equal(completed.status, 0, completed.stderr);
|
|
const results = JSON.parse(completed.stdout);
|
|
assert.ok(results.capabilities.includes("workspace.apply-patch"));
|
|
assert.equal(results.update.status, "completed");
|
|
assert.equal(results.update.output.engine, "codex-apply-patch-v2-compatible");
|
|
assert.equal(results.combined.status, "completed");
|
|
assert.deepEqual(results.combined.output.changes.map((item: any) => item.action), ["add", "move", "delete"]);
|
|
assert.equal(await readFile(path.join(root, "moved.c"), "utf8"), "int value = 3;\r\n");
|
|
assert.equal(await readFile(path.join(root, "added.txt"), "utf8"), "added\n");
|
|
await assert.rejects(readFile(path.join(root, "main.c"), "utf8"));
|
|
await assert.rejects(readFile(path.join(root, "remove.txt"), "utf8"));
|
|
assert.equal(results.raw.blocker.code, "unsupported_apply_patch_format");
|
|
assert.equal(results.raw.blocker.details.engine, "codex-apply-patch-v2-compatible");
|
|
assert.equal(results.stale.blocker.code, "apply_patch_context_not_found");
|
|
assert.equal(results.stale.blocker.details.lineEnding, "\r\n");
|
|
assert.match(results.stale.blocker.details.fileSha256, /^[a-f0-9]{64}$/u);
|
|
assert.equal(results.escape.status, "blocked");
|
|
assert.match(results.escape.blocker.summary, /outside allowedWorkspaceRoots/u);
|
|
} finally {
|
|
await rm(root, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test("python hwpod-node maps UART open, write, and close to serial-monitor", () => {
|
|
const script = `
|
|
import importlib.util, json, logging
|
|
spec = importlib.util.spec_from_file_location("hwlab_node", "tools/hwlab-node.py")
|
|
module = importlib.util.module_from_spec(spec)
|
|
spec.loader.exec_module(module)
|
|
class Executor(module.NodeOpsExecutor):
|
|
def __init__(self):
|
|
super().__init__({"nodeId": "node-test"}, logging.getLogger("test"))
|
|
self.commands = []
|
|
def _spawn_output(self, command, cwd, timeout_ms):
|
|
self.commands.append(command)
|
|
return {"ok": True, "stdout": json.dumps({"success": True}), "stderr": "", "exitCode": 0}
|
|
executor = Executor()
|
|
args = {"workspacePath": ".", "port": "COM9", "baudRate": 115200, "data": "status", "serialMonitorCommand": ["serial-monitor"]}
|
|
results = [executor._execute_op({"opId": "open", "op": "io.uart.open", "args": args}), executor._execute_op({"opId": "write", "op": "io.uart.write", "args": args}), executor._execute_op({"opId": "close", "op": "io.uart.close", "args": args})]
|
|
print(json.dumps({"results": results, "commands": executor.commands}))
|
|
`;
|
|
const completed = spawnSync("python3", ["-c", script], { cwd: process.cwd(), encoding: "utf8" });
|
|
assert.equal(completed.status, 0, completed.stderr);
|
|
const result = JSON.parse(completed.stdout);
|
|
assert.deepEqual(result.commands, [
|
|
["serial-monitor", "monitor", "start", "-p", "COM9", "-b", "115200"],
|
|
["serial-monitor", "send", "status"],
|
|
["serial-monitor", "monitor", "stop"],
|
|
]);
|
|
assert.deepEqual(result.results.map((item: any) => item.status), ["completed", "completed", "completed"]);
|
|
});
|
|
|
|
test("python hwpod-node starts an unreachable serial-monitor server before opening UART", () => {
|
|
const script = `
|
|
import importlib.util, json, logging
|
|
spec = importlib.util.spec_from_file_location("hwlab_node", "tools/hwlab-node.py")
|
|
module = importlib.util.module_from_spec(spec)
|
|
spec.loader.exec_module(module)
|
|
class Executor(module.NodeOpsExecutor):
|
|
def __init__(self):
|
|
super().__init__({"nodeId": "node-test"}, logging.getLogger("test"))
|
|
self.commands = []
|
|
def _spawn_output(self, command, cwd, timeout_ms):
|
|
self.commands.append(command)
|
|
if command[-2:] == ["server", "start"]:
|
|
body = {"success": True}
|
|
elif len(self.commands) == 1:
|
|
body = {"success": False, "data": {"request": {"code": "ECONNREFUSED"}, "diagnosis": {"status": "not_running"}}}
|
|
else:
|
|
body = {"success": True}
|
|
return {"ok": True, "stdout": json.dumps(body), "stderr": "", "exitCode": 0}
|
|
executor = Executor()
|
|
result = executor._execute_op({"opId": "open", "op": "io.uart.open", "args": {"workspacePath": ".", "port": "COM4", "baudRate": 921600, "serialMonitorCommand": ["serial-monitor"]}})
|
|
print(json.dumps({"result": result, "commands": executor.commands}))
|
|
`;
|
|
const completed = spawnSync("python3", ["-c", script], { cwd: process.cwd(), encoding: "utf8" });
|
|
assert.equal(completed.status, 0, completed.stderr);
|
|
const result = JSON.parse(completed.stdout);
|
|
assert.equal(result.result.status, "completed");
|
|
assert.deepEqual(result.commands, [
|
|
["serial-monitor", "monitor", "start", "-p", "COM4", "-b", "921600"],
|
|
["serial-monitor", "server", "start"],
|
|
["serial-monitor", "monitor", "start", "-p", "COM4", "-b", "921600"],
|
|
]);
|
|
});
|
|
|
|
test("python hwpod-node keeps a pyserial connection across UART operations", () => {
|
|
const script = `
|
|
import importlib.util, json, logging, sys, types
|
|
spec = importlib.util.spec_from_file_location("hwlab_node", "tools/hwlab-node.py")
|
|
module = importlib.util.module_from_spec(spec)
|
|
spec.loader.exec_module(module)
|
|
class Port:
|
|
def __init__(self, port, baudrate, timeout):
|
|
self.port = port
|
|
self.baudrate = baudrate
|
|
self.is_open = True
|
|
self.buffer = bytearray(b"ready")
|
|
self.sent = bytearray()
|
|
@property
|
|
def in_waiting(self): return len(self.buffer)
|
|
def read(self, size):
|
|
value = bytes(self.buffer[:size])
|
|
del self.buffer[:size]
|
|
return value
|
|
def write(self, value):
|
|
self.sent.extend(value)
|
|
return len(value)
|
|
def close(self): self.is_open = False
|
|
sys.modules["serial"] = types.SimpleNamespace(Serial=Port)
|
|
executor = module.NodeOpsExecutor({"nodeId": "node-test"}, logging.getLogger("test"))
|
|
common = {"workspacePath": ".", "port": "COM4", "baudRate": 921600, "uartBackend": "pyserial"}
|
|
results = [
|
|
executor._execute_op({"opId": "open", "op": "io.uart.open", "args": common}),
|
|
executor._execute_op({"opId": "write", "op": "io.uart.write", "args": {**common, "data": "status"}}),
|
|
executor._execute_op({"opId": "read", "op": "io.uart.read", "args": {**common, "maxBytes": 16}}),
|
|
executor._execute_op({"opId": "close", "op": "io.uart.close", "args": common}),
|
|
]
|
|
print(json.dumps(results))
|
|
`;
|
|
const completed = spawnSync("python3", ["-c", script], { cwd: process.cwd(), encoding: "utf8" });
|
|
assert.equal(completed.status, 0, completed.stderr);
|
|
const results = JSON.parse(completed.stdout);
|
|
assert.deepEqual(results.map((item: any) => item.status), ["completed", "completed", "completed", "completed"]);
|
|
assert.equal(results[1].output.bytes, 6);
|
|
assert.equal(results[2].output.text, "ready");
|
|
assert.equal(results[2].output.bindingSource, "pyserial");
|
|
});
|
|
|
|
test("python hwpod-node reconnects a stale pyserial PTY once", () => {
|
|
const script = `
|
|
import importlib.util, json, logging, sys, types
|
|
spec = importlib.util.spec_from_file_location("hwlab_node", "tools/hwlab-node.py")
|
|
module = importlib.util.module_from_spec(spec)
|
|
spec.loader.exec_module(module)
|
|
ports = []
|
|
class Port:
|
|
def __init__(self, port, baudrate, timeout):
|
|
self.port = port
|
|
self.baudrate = baudrate
|
|
self.is_open = True
|
|
self.stale = False
|
|
self.buffer = bytearray(b"reconnected")
|
|
ports.append(self)
|
|
@property
|
|
def in_waiting(self):
|
|
if self.stale: raise OSError(5, "Input/output error")
|
|
return len(self.buffer)
|
|
def read(self, size):
|
|
value = bytes(self.buffer[:size])
|
|
del self.buffer[:size]
|
|
return value
|
|
def close(self): self.is_open = False
|
|
sys.modules["serial"] = types.SimpleNamespace(Serial=Port)
|
|
executor = module.NodeOpsExecutor({"nodeId": "node-test"}, logging.getLogger("test"))
|
|
common = {"workspacePath": ".", "port": "/tmp/qemu-uart", "baudRate": 115200, "uartBackend": "pyserial"}
|
|
opened = executor._execute_op({"opId": "open-1", "op": "io.uart.open", "args": common})
|
|
ports[0].stale = True
|
|
reopened = executor._execute_op({"opId": "open-2", "op": "io.uart.open", "args": common})
|
|
read = executor._execute_op({"opId": "read", "op": "io.uart.read", "args": {**common, "maxBytes": 32}})
|
|
ports[-1].stale = True
|
|
read_recovered = executor._execute_op({"opId": "read-recovered", "op": "io.uart.read", "args": {**common, "maxBytes": 32}})
|
|
print(json.dumps({"opened": opened, "reopened": reopened, "read": read, "readRecovered": read_recovered, "portCount": len(ports)}))
|
|
`;
|
|
const completed = spawnSync("python3", ["-c", script], { cwd: process.cwd(), encoding: "utf8" });
|
|
assert.equal(completed.status, 0, completed.stderr);
|
|
const result = JSON.parse(completed.stdout);
|
|
assert.equal(result.opened.status, "completed");
|
|
assert.equal(result.reopened.output.reopened, true);
|
|
assert.equal(result.read.output.text, "reconnected");
|
|
assert.equal(result.readRecovered.output.reopened, true);
|
|
assert.equal(result.readRecovered.output.text, "reconnected");
|
|
assert.equal(result.portCount, 3);
|
|
});
|
|
|
|
test("hwpod-node resolves Git for Windows when service PATH omits git", async () => {
|
|
const result = await resolveHwpodNodeCommand("git", {
|
|
platform: "win32",
|
|
env: {},
|
|
fileExists: async (candidate: string) => candidate === "C:\\Program Files\\Git\\cmd\\git.exe"
|
|
});
|
|
|
|
assert.equal(result.command, "C:\\Program Files\\Git\\cmd\\git.exe");
|
|
assert.equal(result.resolved, true);
|
|
assert.equal(result.source, "windows-well-known-tool");
|
|
});
|
|
|
|
test("hwpod-node keeps explicit executable paths unchanged", async () => {
|
|
const result = await resolveHwpodNodeCommand("C:\\Tools\\Git\\cmd\\git.exe", {
|
|
platform: "win32",
|
|
env: {},
|
|
fileExists: async () => true
|
|
});
|
|
|
|
assert.equal(result.command, "C:\\Tools\\Git\\cmd\\git.exe");
|
|
assert.equal(result.resolved, false);
|
|
assert.equal(result.source, "explicit-path");
|
|
});
|
|
|
|
test("hwpod-node applies workspace patches through the stable node op", async () => {
|
|
const root = await mkdtemp(path.join(os.tmpdir(), "hwlab-hwpod-node-patch-"));
|
|
try {
|
|
await writeFile(path.join(root, "main.c"), "int value = 1;\n", "utf8");
|
|
const result = await executeHwpodNodeOpsPlan({
|
|
contractVersion: "hwpod-node-ops-v1",
|
|
planId: "hwpod_plan_patch",
|
|
hwpodId: "hwpod-local",
|
|
nodeId: "pc-host-1",
|
|
ops: [{
|
|
opId: "op_patch",
|
|
op: "workspace.apply-patch",
|
|
args: {
|
|
workspacePath: root,
|
|
patch: [
|
|
"*** Begin Patch",
|
|
"*** Update File: main.c",
|
|
"@@",
|
|
"-int value = 1;",
|
|
"+int value = 2;",
|
|
"*** End Patch",
|
|
""
|
|
].join("\n")
|
|
}
|
|
}]
|
|
}, { now: () => "2026-06-05T00:00:00.000Z" });
|
|
assert.equal(result.ok, true);
|
|
assert.equal(result.results[0].ok, true);
|
|
assert.match(await readFile(path.join(root, "main.c"), "utf8"), /value = 2/u);
|
|
} finally {
|
|
await rm(root, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test("hwpod-node preserves CRLF when applying workspace patches", async () => {
|
|
const root = await mkdtemp(path.join(os.tmpdir(), "hwlab-hwpod-node-patch-crlf-"));
|
|
try {
|
|
await writeFile(path.join(root, "main.c"), "int main(void) {\r\n while (1) {\r\n }\r\n}\r\n", "utf8");
|
|
const result = await executeHwpodNodeOpsPlan({
|
|
contractVersion: "hwpod-node-ops-v1",
|
|
planId: "hwpod_plan_patch_crlf",
|
|
hwpodId: "hwpod-local",
|
|
nodeId: "pc-host-1",
|
|
ops: [{
|
|
opId: "op_patch",
|
|
op: "workspace.apply-patch",
|
|
args: {
|
|
workspacePath: root,
|
|
patch: [
|
|
"*** Begin Patch",
|
|
"*** Update File: main.c",
|
|
"@@",
|
|
" while (1) {",
|
|
"+ /* MARKER_OK */",
|
|
" }",
|
|
"*** End Patch",
|
|
""
|
|
].join("\n")
|
|
}
|
|
}]
|
|
}, { now: () => "2026-06-05T00:00:00.000Z" });
|
|
const content = await readFile(path.join(root, "main.c"), "utf8");
|
|
assert.equal(result.ok, true);
|
|
assert.match(content, /\r\n while \(1\) \{\r\n \/\* MARKER_OK \*\/\r\n \}\r\n/u);
|
|
assert.doesNotMatch(content, /[^\r]\n/u);
|
|
} finally {
|
|
await rm(root, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test("hwpod-node apply-patch accepts UniDesk v2 compatible unified headers and unprefixed context", async () => {
|
|
const root = await mkdtemp(path.join(os.tmpdir(), "hwlab-hwpod-node-patch-v2-"));
|
|
try {
|
|
await writeFile(path.join(root, "main.c"), [
|
|
"#include <stdio.h>",
|
|
"int main(void) {",
|
|
" printf(\"OLD\\r\\n\");",
|
|
" return 0;",
|
|
"}",
|
|
""
|
|
].join("\n"), "utf8");
|
|
const result = await executeHwpodNodeOpsPlan({
|
|
contractVersion: "hwpod-node-ops-v1",
|
|
planId: "hwpod_plan_patch_v2",
|
|
hwpodId: "hwpod-local",
|
|
nodeId: "pc-host-1",
|
|
ops: [{
|
|
opId: "op_patch",
|
|
op: "workspace.apply-patch",
|
|
args: {
|
|
workspacePath: root,
|
|
patch: [
|
|
"*** Begin Patch",
|
|
"*** Update File: main.c",
|
|
"@@ -1,5 +1,6 @@",
|
|
"#include <stdio.h>",
|
|
"int main(void) {",
|
|
"- printf(\"OLD\\r\\n\");",
|
|
"+ printf(\"NEW\\r\\n\");",
|
|
"+ printf(\"TRACE\\r\\n\");",
|
|
" return 0;",
|
|
"}",
|
|
"*** End Patch",
|
|
""
|
|
].join("\n")
|
|
}
|
|
}]
|
|
}, { now: () => "2026-06-05T00:00:00.000Z" });
|
|
const content = await readFile(path.join(root, "main.c"), "utf8");
|
|
assert.equal(result.ok, true);
|
|
assert.equal(result.results[0].output.engine, "codex-apply-patch-v2-compatible");
|
|
assert.match(result.results[0].output.hints.join("\n"), /unified-diff hunk header/u);
|
|
assert.match(result.results[0].output.hints.join("\n"), /unprefixed Update File context line/u);
|
|
assert.match(content, /printf\("NEW\\r\\n"\);/u);
|
|
assert.match(content, /printf\("TRACE\\r\\n"\);/u);
|
|
assert.doesNotMatch(content, /printf\("NEW\r\n"\);/u);
|
|
} finally {
|
|
await rm(root, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test("hwpod-node apply-patch rejects raw unified diff with a precise format blocker", async () => {
|
|
const root = await mkdtemp(path.join(os.tmpdir(), "hwlab-hwpod-node-patch-raw-diff-"));
|
|
try {
|
|
await writeFile(path.join(root, "main.c"), "int value = 1;\n", "utf8");
|
|
const result = await executeHwpodNodeOpsPlan({
|
|
contractVersion: "hwpod-node-ops-v1",
|
|
planId: "hwpod_plan_patch_raw_diff",
|
|
hwpodId: "hwpod-local",
|
|
nodeId: "pc-host-1",
|
|
ops: [{
|
|
opId: "op_patch",
|
|
op: "workspace.apply-patch",
|
|
args: {
|
|
workspacePath: root,
|
|
patch: [
|
|
"--- a/main.c",
|
|
"+++ b/main.c",
|
|
"@@ -1 +1 @@",
|
|
"-int value = 1;",
|
|
"+int value = 2;",
|
|
""
|
|
].join("\n")
|
|
}
|
|
}]
|
|
}, { now: () => "2026-06-05T00:00:00.000Z" });
|
|
assert.equal(result.ok, false);
|
|
assert.equal(result.results[0].blocker.code, "unsupported_apply_patch_format");
|
|
assert.equal(result.results[0].blocker.details.engine, "codex-apply-patch-v2-compatible");
|
|
assert.equal(result.results[0].blocker.details.format, "unified-diff");
|
|
assert.match(result.results[0].blocker.summary, /Codex\/UniDesk apply_patch v2 envelope/u);
|
|
} finally {
|
|
await rm(root, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test("hwpod-node returns apply-patch diagnostics when context is missing", async () => {
|
|
const root = await mkdtemp(path.join(os.tmpdir(), "hwlab-hwpod-node-patch-diagnostics-"));
|
|
try {
|
|
const original = "int main(void) {\r\n while (1) {\r\n }\r\n}\r\n";
|
|
await writeFile(path.join(root, "main.c"), original, "utf8");
|
|
const result = await executeHwpodNodeOpsPlan({
|
|
contractVersion: "hwpod-node-ops-v1",
|
|
planId: "hwpod_plan_patch_diag",
|
|
hwpodId: "hwpod-local",
|
|
nodeId: "pc-host-1",
|
|
ops: [{
|
|
opId: "op_patch",
|
|
op: "workspace.apply-patch",
|
|
args: {
|
|
workspacePath: root,
|
|
patch: [
|
|
"*** Begin Patch",
|
|
"*** Update File: main.c",
|
|
"@@",
|
|
"- while (2) {",
|
|
"+ while (3) {",
|
|
"*** End Patch",
|
|
""
|
|
].join("\n")
|
|
}
|
|
}]
|
|
}, { now: () => "2026-06-05T00:00:00.000Z" });
|
|
assert.equal(result.ok, false);
|
|
assert.equal(result.results[0].blocker.code, "apply_patch_context_not_found");
|
|
assert.equal(result.results[0].blocker.details.fileSha256, sha256(original));
|
|
assert.equal(result.results[0].blocker.details.lineEnding, "\r\n");
|
|
assert.equal(result.results[0].blocker.details.lineEndingCounts.crlf, 4);
|
|
assert.equal(result.results[0].blocker.details.normalized, true);
|
|
assert.match(result.results[0].blocker.details.nodeVersion, /thin-node-ops/u);
|
|
} finally {
|
|
await rm(root, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test("hwpod-node edits CRLF workspaces with insert-after replace and write ops", async () => {
|
|
const root = await mkdtemp(path.join(os.tmpdir(), "hwlab-hwpod-node-text-edit-"));
|
|
try {
|
|
const filePath = path.join(root, "projects", "01_baseline", "User", "main.c");
|
|
await mkdir(path.dirname(filePath), { recursive: true });
|
|
await writeFile(filePath, "int main(void) {\r\n while (1) {\r\n }\r\n}\r\n", "utf8");
|
|
const before = await readFile(filePath, "utf8");
|
|
const insert = await executeHwpodNodeOpsPlan({
|
|
contractVersion: "hwpod-node-ops-v1",
|
|
planId: "hwpod_plan_insert",
|
|
hwpodId: "hwpod-local",
|
|
nodeId: "pc-host-1",
|
|
ops: [{
|
|
opId: "op_insert",
|
|
op: "workspace.insert-after",
|
|
args: { workspacePath: root, path: "projects/01_baseline/User/main.c", anchor: " while (1) {", line: " /* MARKER_OK */", expectedSha: sha256(before) }
|
|
}]
|
|
}, { now: () => "2026-06-05T00:00:00.000Z" });
|
|
assert.equal(insert.ok, true);
|
|
assert.equal(insert.results[0].output.before.sha256, sha256(before));
|
|
assert.equal(insert.results[0].output.after.lineEnding, "\r\n");
|
|
assert.equal(insert.results[0].output.anchorMatch, "line-sequence");
|
|
assert.deepEqual(insert.results[0].output.diff.preview, ["+ /* MARKER_OK */"]);
|
|
const inserted = await readFile(filePath, "utf8");
|
|
assert.match(inserted, /\r\n \/\* MARKER_OK \*\/\r\n/u);
|
|
assert.doesNotMatch(inserted, /[^\r]\n/u);
|
|
|
|
const replace = await executeHwpodNodeOpsPlan({
|
|
contractVersion: "hwpod-node-ops-v1",
|
|
planId: "hwpod_plan_replace",
|
|
hwpodId: "hwpod-local",
|
|
nodeId: "pc-host-1",
|
|
ops: [{
|
|
opId: "op_replace",
|
|
op: "workspace.replace",
|
|
args: { workspacePath: root, path: "projects/01_baseline/User/main.c", find: " /* MARKER_OK */", replace: " /* MARKER_REPLACED */", expectedSha: sha256(inserted) }
|
|
}]
|
|
}, { now: () => "2026-06-05T00:00:00.000Z" });
|
|
assert.equal(replace.ok, true);
|
|
assert.equal(replace.results[0].output.occurrences, 1);
|
|
assert.match(await readFile(filePath, "utf8"), /MARKER_REPLACED/u);
|
|
|
|
const dryRun = await executeHwpodNodeOpsPlan({
|
|
contractVersion: "hwpod-node-ops-v1",
|
|
planId: "hwpod_plan_write_dry",
|
|
hwpodId: "hwpod-local",
|
|
nodeId: "pc-host-1",
|
|
ops: [{
|
|
opId: "op_write",
|
|
op: "workspace.write",
|
|
args: { workspacePath: root, path: "new.txt", content: "alpha\nbeta", lineEnding: "crlf", finalNewline: true, dryRun: true }
|
|
}]
|
|
}, { now: () => "2026-06-05T00:00:00.000Z" });
|
|
assert.equal(dryRun.ok, true);
|
|
assert.equal(dryRun.results[0].output.dryRun, true);
|
|
assert.equal(dryRun.results[0].output.after.lineEnding, "\r\n");
|
|
await assert.rejects(() => readFile(path.join(root, "new.txt"), "utf8"), /ENOENT/u);
|
|
} finally {
|
|
await rm(root, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test("hwpod-node insert-after accepts a unique in-line source fragment anchor", async () => {
|
|
const root = await mkdtemp(path.join(os.tmpdir(), "hwlab-hwpod-node-fragment-anchor-"));
|
|
try {
|
|
const filePath = path.join(root, "projects", "01_baseline", "User", "main.c");
|
|
await mkdir(path.dirname(filePath), { recursive: true });
|
|
await writeFile(filePath, "void boot(void) {\r\n printf(\"\\r\\n[D601-F103] device-pod LCD bring-up\\r\\n\");\r\n lcd_init();\r\n}\r\n", "utf8");
|
|
const before = await readFile(filePath, "utf8");
|
|
const insert = await executeHwpodNodeOpsPlan({
|
|
contractVersion: "hwpod-node-ops-v1",
|
|
planId: "hwpod_plan_insert_fragment",
|
|
hwpodId: "hwpod-local",
|
|
nodeId: "pc-host-1",
|
|
ops: [{
|
|
opId: "op_insert",
|
|
op: "workspace.insert-after",
|
|
args: { workspacePath: root, path: "projects/01_baseline/User/main.c", anchor: "device-pod LCD bring-up", line: " printf(\"[D601-F103] marker\\r\\n\");", expectedSha: sha256(before) }
|
|
}]
|
|
}, { now: () => "2026-06-05T00:00:00.000Z" });
|
|
assert.equal(insert.ok, true);
|
|
assert.equal(insert.results[0].output.anchorLine, 2);
|
|
assert.equal(insert.results[0].output.anchorMatch, "line-fragment");
|
|
const inserted = await readFile(filePath, "utf8");
|
|
assert.match(inserted, /device-pod LCD bring-up\\r\\n"\);\r\n printf\("\[D601-F103\] marker\\r\\n"\);\r\n lcd_init/u);
|
|
assert.doesNotMatch(inserted, /[^\r]\n/u);
|
|
} finally {
|
|
await rm(root, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test("hwpod-node insert-after keeps fragment anchors safe when ambiguous", async () => {
|
|
const root = await mkdtemp(path.join(os.tmpdir(), "hwlab-hwpod-node-fragment-ambiguous-"));
|
|
try {
|
|
const filePath = path.join(root, "main.c");
|
|
await writeFile(filePath, "printf(\"device-pod LCD bring-up\");\nprintf(\"device-pod LCD bring-up again\");\n", "utf8");
|
|
const before = await readFile(filePath, "utf8");
|
|
const insert = await executeHwpodNodeOpsPlan({
|
|
contractVersion: "hwpod-node-ops-v1",
|
|
planId: "hwpod_plan_insert_ambiguous_fragment",
|
|
hwpodId: "hwpod-local",
|
|
nodeId: "pc-host-1",
|
|
ops: [{
|
|
opId: "op_insert",
|
|
op: "workspace.insert-after",
|
|
args: { workspacePath: root, path: "main.c", anchor: "device-pod LCD bring-up", line: "printf(\"marker\");", expectedSha: sha256(before) }
|
|
}]
|
|
}, { now: () => "2026-06-05T00:00:00.000Z" });
|
|
assert.equal(insert.ok, false);
|
|
assert.equal(insert.results[0].blocker.code, "workspace_insert_anchor_ambiguous");
|
|
assert.equal(insert.results[0].blocker.details.anchorMatch, "line-fragment");
|
|
} finally {
|
|
await rm(root, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test("hwpod-node preserves Windows absolute workspace paths for PC-host nodes", async () => {
|
|
const result = await executeHwpodNodeOpsPlan({
|
|
contractVersion: "hwpod-node-ops-v1",
|
|
planId: "hwpod_plan_windows_path",
|
|
hwpodId: "d601-f103-v2",
|
|
nodeId: "node-d601-f103-v2",
|
|
ops: [{ opId: "op_01", op: "workspace.ls", args: { workspacePath: "F:\\Work\\D601-HWLAB", path: "." } }]
|
|
}, { now: () => "2026-06-05T00:00:00.000Z" });
|
|
|
|
assert.equal(result.ok, false);
|
|
assert.equal(result.results[0].blocker.code, "ENOENT");
|
|
assert.match(result.results[0].blocker.summary, /scandir 'F:\\Work\\D601-HWLAB'/u);
|
|
assert.doesNotMatch(result.results[0].blocker.summary, /\/root\//u);
|
|
});
|
|
|
|
test("python hwpod-node dispatches 16 WebSocket operations concurrently without blocking intake", () => {
|
|
const script = String.raw`
|
|
import importlib.util
|
|
import json
|
|
import pathlib
|
|
import tempfile
|
|
import threading
|
|
import time
|
|
|
|
spec = importlib.util.spec_from_file_location("hwlab_node", "tools/hwlab-node.py")
|
|
module = importlib.util.module_from_spec(spec)
|
|
spec.loader.exec_module(module)
|
|
module.OPERATION_LEDGER_PATH = pathlib.Path(tempfile.mkdtemp()) / "operations.json"
|
|
|
|
class Executor:
|
|
node_id = "node-concurrency-test"
|
|
def __init__(self):
|
|
self.lock = threading.Lock()
|
|
self.release = threading.Event()
|
|
self.all_started = threading.Event()
|
|
self.active = 0
|
|
self.peak = 0
|
|
def execute(self, plan):
|
|
with self.lock:
|
|
self.active += 1
|
|
self.peak = max(self.peak, self.active)
|
|
if self.active == 16:
|
|
self.all_started.set()
|
|
self.release.wait(5)
|
|
with self.lock:
|
|
self.active -= 1
|
|
if plan["planId"] == "plan-00":
|
|
raise RuntimeError("isolated failure")
|
|
return {"ok": True, "status": "completed", "planId": plan["planId"]}
|
|
|
|
executor = Executor()
|
|
client = module.WebSocketNodeClient({"maxInFlight": 16}, executor, module.logging.getLogger("test"), module.UiEventBus())
|
|
messages = []
|
|
messages_lock = threading.Lock()
|
|
def capture(value, connection_generation=None):
|
|
with messages_lock:
|
|
messages.append(value)
|
|
client._send_json = capture
|
|
|
|
for index in range(16):
|
|
request_id = "req-%02d" % index
|
|
plan_id = "plan-%02d" % index
|
|
client._handle_message(json.dumps({"type": "hwpod-node-ops", "requestId": request_id, "plan": {"planId": plan_id}}))
|
|
|
|
if not executor.all_started.wait(3):
|
|
raise AssertionError("16 operations did not enter the executor concurrently")
|
|
client._handle_message(json.dumps({"type": "hwpod-node-ops", "requestId": "req-overflow", "plan": {"planId": "plan-overflow"}}))
|
|
executor.release.set()
|
|
deadline = time.time() + 5
|
|
while time.time() < deadline:
|
|
with messages_lock:
|
|
results = [value for value in messages if value.get("type") == "hwpod-node-ops-result"]
|
|
if len(results) == 17:
|
|
break
|
|
time.sleep(0.01)
|
|
client.worker_pool.shutdown(wait=True)
|
|
with messages_lock:
|
|
results = [value for value in messages if value.get("type") == "hwpod-node-ops-result"]
|
|
completed = [value for value in results if value["result"].get("status") == "completed"]
|
|
failed = [value for value in results if value["result"].get("status") == "failed"]
|
|
busy = [value for value in results if value["result"].get("blocker", {}).get("code") == "hwpod_node_busy"]
|
|
print(json.dumps({"peak": executor.peak, "completed": len(completed), "failed": len(failed), "busy": len(busy), "requestIds": sorted(value["requestId"] for value in completed + failed)}))
|
|
`;
|
|
const result = spawnSync("python3", ["-c", script], { cwd: process.cwd(), encoding: "utf8" });
|
|
assert.equal(result.status, 0, result.stderr || result.stdout);
|
|
const payload = JSON.parse(result.stdout.trim());
|
|
assert.equal(payload.peak, 16);
|
|
assert.equal(payload.completed, 15);
|
|
assert.equal(payload.failed, 1);
|
|
assert.equal(payload.busy, 1);
|
|
assert.deepEqual(payload.requestIds, Array.from({ length: 16 }, (_, index) => `req-${String(index).padStart(2, "0")}`));
|
|
});
|
|
|
|
test("python hwpod-node serializes concurrent WebSocket frame writes", () => {
|
|
const script = String.raw`
|
|
import importlib.util
|
|
import json
|
|
import threading
|
|
import time
|
|
|
|
spec = importlib.util.spec_from_file_location("hwlab_node", "tools/hwlab-node.py")
|
|
module = importlib.util.module_from_spec(spec)
|
|
spec.loader.exec_module(module)
|
|
|
|
class Executor:
|
|
node_id = "node-send-lock-test"
|
|
class Socket:
|
|
def __init__(self):
|
|
self.lock = threading.Lock()
|
|
self.active = 0
|
|
self.peak = 0
|
|
self.calls = 0
|
|
def sendall(self, payload):
|
|
with self.lock:
|
|
self.active += 1
|
|
self.peak = max(self.peak, self.active)
|
|
time.sleep(0.01)
|
|
with self.lock:
|
|
self.active -= 1
|
|
self.calls += 1
|
|
|
|
client = module.WebSocketNodeClient({"maxInFlight": 16}, Executor(), module.logging.getLogger("test"), module.UiEventBus())
|
|
client.sock = Socket()
|
|
client.connection_generation = 7
|
|
client._send_frame(b"stale", connection_generation=6)
|
|
threads = [threading.Thread(target=client._send_frame, args=(b"payload",), kwargs={"connection_generation": 7}) for _ in range(16)]
|
|
for thread in threads:
|
|
thread.start()
|
|
for thread in threads:
|
|
thread.join()
|
|
client.worker_pool.shutdown(wait=True)
|
|
print(json.dumps({"peak": client.sock.peak, "calls": client.sock.calls}))
|
|
`;
|
|
const result = spawnSync("python3", ["-c", script], { cwd: process.cwd(), encoding: "utf8" });
|
|
assert.equal(result.status, 0, result.stderr || result.stdout);
|
|
assert.deepEqual(JSON.parse(result.stdout.trim()), { peak: 1, calls: 16 });
|
|
});
|
|
|
|
test("python hwpod-node keeps timeout output JSON-safe", () => {
|
|
const script = String.raw`
|
|
import importlib.util
|
|
import json
|
|
import logging
|
|
import pathlib
|
|
import sys
|
|
|
|
spec = importlib.util.spec_from_file_location("hwlab_node", "tools/hwlab-node.py")
|
|
module = importlib.util.module_from_spec(spec)
|
|
spec.loader.exec_module(module)
|
|
executor = module.NodeOpsExecutor({}, logging.getLogger("test"))
|
|
result = executor._spawn_output(
|
|
[sys.executable, "-c", "import sys,time;sys.stdout.buffer.write(b'partial');sys.stdout.flush();time.sleep(1)"],
|
|
pathlib.Path.cwd(),
|
|
20,
|
|
)
|
|
encoded = json.dumps({"result": result, "residual": b"bytes"}, default=module.json_default)
|
|
print(json.dumps({"blockerCode": result["blockerCode"], "stdout": result["stdout"], "stdoutType": type(result["stdout"]).__name__, "encoded": encoded}))
|
|
`;
|
|
const result = spawnSync("python3", ["-c", script], { cwd: process.cwd(), encoding: "utf8" });
|
|
assert.equal(result.status, 0, result.stderr || result.stdout);
|
|
const payload = JSON.parse(result.stdout.trim());
|
|
assert.equal(payload.blockerCode, "hwpod_node_command_timeout");
|
|
assert.equal(payload.stdoutType, "str");
|
|
assert.match(payload.stdout, /partial/u);
|
|
assert.match(payload.encoded, /bytes/u);
|
|
});
|
|
|
|
function sha256(content: string) {
|
|
return createHash("sha256").update(content, "utf8").digest("hex");
|
|
}
|