Files
pikasTech-HWLAB/tools/hwlab-caserun.test.ts
T

153 lines
7.6 KiB
TypeScript

import assert from "node:assert/strict";
import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { test } from "bun:test";
import { prepareCaseRun } from "./src/hwlab-caserun-lib.ts";
import { subjectWorktreeFailureDetails } from "./src/hwlab-caserun-subject.ts";
const NOW = "2026-06-06T00:00:00.000Z";
const SUBJECT_COMMIT = "df7a4e6e551fa90d64bde5537cc000f89d63dd20";
const TEST_RUNTIME_API_URL = "http://api.test";
test("subject worktree failures preserve common Windows operation identity without run-specific constants", () => {
for (const repoLocalPath of ["F:\\Work\\HWLAB-CASE-F103", "F:\\Work\\ConStart"]) {
const plan = {
contractVersion: "hwpod-node-ops-v1",
planId: "case_subject_test",
hwpodId: "d601-f103-v2",
nodeId: "node-d601-f103-v2",
intent: "cmd.run",
ops: [
{ opId: "op_01_subject_commit", op: "cmd.run", args: { workspacePath: repoLocalPath } },
{ opId: "op_02_subject_worktree_add", op: "cmd.run", args: { workspacePath: repoLocalPath } },
{ opId: "op_03_subject_worktree_head", op: "cmd.run", args: { workspacePath: repoLocalPath } }
]
};
const body = {
contractVersion: "hwpod-node-ops-v1",
planId: plan.planId,
hwpodId: plan.hwpodId,
nodeId: plan.nodeId,
status: "blocked",
results: plan.ops.map((op) => ({ opId: op.opId, op: op.op, ok: false, status: "blocked", blocker: { code: "hwpod_node_unavailable", layer: "hwpod-node", retryable: true, summary: "node unavailable token=hwl_live_should_not_leak" } })),
blocker: { code: "hwpod_node_unavailable", layer: "hwpod-node", retryable: true, summary: "node unavailable token=hwl_live_should_not_leak", details: { dispatchMode: "none" } }
};
const failure = subjectWorktreeFailureDetails(body, plan, { stderr: "" });
assert.equal(failure.partialWrite, false);
assert.equal(failure.dispatched, false);
assert.equal(failure.blocker.details.dispatched, false);
assert.equal(failure.operation.nodeId, "node-d601-f103-v2");
assert.deepEqual(failure.operation.ops.map((op: any) => op.opId), plan.ops.map((op) => op.opId));
assert.doesNotMatch(failure.errorSummary, /hwl_live_should_not_leak/u);
}
});
test("subject worktree failure facts remain null unless dispatch or writes are authoritative", () => {
const plan = {
contractVersion: "hwpod-node-ops-v1",
planId: "case_subject_facts",
hwpodId: "d601-f103-v2",
nodeId: "node-d601-f103-v2",
intent: "cmd.run",
ops: [{ opId: "op_02_subject_worktree_add", op: "cmd.run", args: {} }]
};
const unknown = subjectWorktreeFailureDetails({ status: "blocked", results: [{ opId: "op_02_subject_worktree_add", ok: false, status: "blocked" }] }, plan, { stderr: "blocked" });
assert.equal(unknown.dispatched, null);
assert.equal(unknown.partialWrite, null);
const dispatched = subjectWorktreeFailureDetails({ status: "failed", blocker: { details: { dispatchMode: "websocket" } }, results: [{ opId: "op_02_subject_worktree_add", ok: false, status: "failed" }] }, plan, { stderr: "failed" });
assert.equal(dispatched.dispatched, true);
assert.equal(dispatched.partialWrite, null);
const written = subjectWorktreeFailureDetails({ status: "failed", dispatched: true, results: [{ opId: "op_02_subject_worktree_add", ok: true, status: "completed" }] }, plan, { stderr: "later operation failed" });
assert.equal(written.dispatched, true);
assert.equal(written.partialWrite, true);
});
test("CaseRun records source root baseline so pre-existing dirty files are not attributed to the run", async () => {
const root = await mkdtemp(path.join(os.tmpdir(), "hwlab-caserun-source-root-"));
try {
const caseRepo = path.join(root, "case-registry");
const caseDir = path.join(caseRepo, "cases", "dirty-source-root");
await mkdir(path.join(caseRepo, ".git"), { recursive: true });
await mkdir(caseDir, { recursive: true });
await writeFile(path.join(caseRepo, ".git", "HEAD"), "ref: refs/heads/main\n", "utf8");
await writeFile(path.join(caseDir, "case.json"), JSON.stringify({
contractVersion: "hwpod-case-v1",
caseId: "dirty-source-root",
hwpodSpec: "hwpod-spec.yaml",
subject: {
repoLocalPath: "F:\\Work\\HWLAB-CASE-F103",
commitId: SUBJECT_COMMIT,
subdir: "projects/01_baseline"
},
agentTask: { workspace: "subjectWorktree", prompt: "record source root baseline", constraints: [] }
}, null, 2), "utf8");
await writeFile(path.join(caseDir, "hwpod-spec.yaml"), [
"apiVersion: hwlab.dev/v0alpha1",
"kind: Hwpod",
"metadata:",
" name: d601-f103-v2",
"spec:",
" targetDevice:",
" board: D601-F103-V2",
" mcu: STM32F103",
" workspace:",
" path: \"F:\\\\Work\\\\HWLAB-CASE-F103\"",
" toolchain: keil-mdk",
" keilProject: projects/01_baseline/Projects/MDK-ARM/atk_f103.uvprojx",
" debugProbe:",
" type: daplink",
" adapter: keil",
" ioProbe:",
" uart:",
" id: uart/1",
" port: COM9",
" nodeBinding:",
" nodeId: node-d601-f103-v2",
""
].join("\n"), "utf8");
const dirtyStatus = " M projects/01_baseline/Projects/MDK-ARM/atk_f103.uvprojx\n";
const dirtyDiff = "diff --git a/projects/01_baseline/Projects/MDK-ARM/atk_f103.uvprojx b/projects/01_baseline/Projects/MDK-ARM/atk_f103.uvprojx\n+<PackID>Keil.STM32F1xx_DFP.2.4.1</PackID>\n";
const fetchImpl = async (_url: string, init: RequestInit) => {
const plan = JSON.parse(String(init.body));
const results = plan.ops.map((op: any) => ({ opId: op.opId, op: op.op, ok: true, status: "completed", output: outputForGit(op.args.argv, dirtyStatus, dirtyDiff) }));
return new Response(JSON.stringify({ ok: true, status: "completed", body: { results } }), { status: 200, headers: { "content-type": "application/json" } });
};
const result = await prepareCaseRun({
parsed: { _: [], caseRepo, runId: "source-root-baseline" },
env: { HWLAB_RUNTIME_API_URL: TEST_RUNTIME_API_URL },
fetchImpl: fetchImpl as typeof fetch,
cwd: root,
now: () => NOW,
sleep: async () => {},
rest: ["prepare", "dirty-source-root"]
});
assert.equal(result.status, "succeeded");
assert.equal(result.summary.status, "prepared");
assert.equal(result.run.subject.sourceRootBaseline.statusShort, dirtyStatus.trim());
assert.equal(result.run.subject.sourceRootAfterPrepare.statusShort, dirtyStatus.trim());
assert.equal(result.run.subject.sourceRootBaseline.diffSha256, result.run.subject.sourceRootAfterPrepare.diffSha256);
assert.match(await readFile(result.specPath, "utf8"), /caserun-source-root-baseline/u);
} finally {
await rm(root, { recursive: true, force: true });
}
});
function outputForGit(argv: string[], dirtyStatus: string, dirtyDiff: string) {
const command = argv.join(" ");
if (command.includes("cat-file -e")) return { exitCode: 0, stdout: "", stderr: "" };
if (command.includes("worktree add")) return { exitCode: 0, stdout: "Preparing worktree\n", stderr: "" };
if (command.includes("rev-parse HEAD")) return { exitCode: 0, stdout: `${SUBJECT_COMMIT}\n`, stderr: "" };
if (command.endsWith("status --short")) return { exitCode: 0, stdout: dirtyStatus, stderr: "" };
if (command.endsWith("diff --stat")) return { exitCode: 0, stdout: " projects/01_baseline/Projects/MDK-ARM/atk_f103.uvprojx | 8 ++++----\n", stderr: "" };
if (command.endsWith("diff --binary")) return { exitCode: 0, stdout: dirtyDiff, stderr: "" };
return { exitCode: 0, stdout: "", stderr: "" };
}