import assert from "node:assert/strict"; import { mkdir, readFile, rm, writeFile } from "node:fs/promises"; import os from "node:os"; import path from "node:path"; import { test } from "bun:test"; import { runHwlabCli } from "../src/hwlab-cli-lib.ts"; import { artifactsFromKeilStatusForTest, extractKeilJobId, jobStatusCommandForTest } from "../src/hwlab-caserun-lib.ts"; test("hwlab-cli case prepare copies a case hwpod-spec into isolated run state", async () => { const root = await mkdtempCaseRoot(); const caseRepo = path.join(root, "hwlab-case-registry"); const cwd = path.join(root, "hwlab"); const caseDir = path.join(caseRepo, "cases", "d601-f103-v2-compile"); try { await mkdir(path.join(caseRepo, ".git"), { recursive: true }); await writeFile(path.join(caseRepo, ".git", "HEAD"), "ref: refs/heads/main\n", "utf8"); await mkdir(caseDir, { recursive: true }); await mkdir(cwd, { recursive: true }); await writeFile(path.join(caseDir, "case.json"), JSON.stringify({ contractVersion: "hwpod-case-v1", caseId: "d601-f103-v2-compile", hwpodSpec: "hwpod-spec.yaml", subject: { repo: "git@github.com:pikasTech/D601-HWLAB.git", ref: "main", subdir: "projects/01_baseline" } }, null, 2), "utf8"); await writeFile(path.join(caseDir, "hwpod-spec.yaml"), "kind: Hwpod\nspec:\n nodeBinding:\n nodeId: node-d601-f103-v2\n workspace:\n path: F:\\\\Work\\\\D601-HWLAB\n targetDevice:\n board: D601-F103-V2\n debugProbe:\n type: daplink\n ioProbe:\n uart:\n port: COM9\n", "utf8"); const result = await runHwlabCli(["case", "prepare", "d601-f103-v2-compile", "--case-repo", caseRepo, "--run-id", "run-test"], { cwd, now: () => "2026-06-05T00:00:00.000Z" }); assert.equal(result.exitCode, 0); assert.equal(result.payload.action, "case.prepare.prepare"); assert.equal(result.payload.runId, "run-test"); assert.equal(result.payload.caseRepo, caseRepo); assert.equal(result.payload.compileOnly, true); assert.match(await readFile(path.join(cwd, ".state", "hwlab-cli", "caserun", "run-test", ".hwlab", "hwpod-spec.yaml"), "utf8"), /node-d601-f103-v2/u); } finally { await rm(root, { recursive: true, force: true }); } }); test("case prepare uses the new registry by default and refuses the removed hwpod-cases path", async () => { const root = await mkdtempCaseRoot(); const cwd = path.join(root, "hwlab"); const newRepo = path.join(root, "hwlab-case-registry"); const removedRepo = path.join(root, "hwpod-cases"); const caseId = "d601-f103-v2-compile"; try { await createCaseRepo(newRepo, caseId); await createCaseRepo(removedRepo, caseId); await mkdir(cwd, { recursive: true }); const defaultResult = await runHwlabCli(["case", "prepare", caseId, "--run-id", "run-default"], { cwd, now: () => "2026-06-05T00:00:00.000Z" }); assert.equal(defaultResult.exitCode, 0); assert.equal(defaultResult.payload.caseRepo, newRepo); const removedResult = await runHwlabCli(["case", "prepare", caseId, "--case-repo", removedRepo], { cwd, now: () => "2026-06-05T00:00:00.000Z" }); assert.equal(removedResult.exitCode, 1); assert.equal(removedResult.payload.error.code, "removed_case_repo_path"); assert.equal(removedResult.payload.error.details.standardCaseRepo, "/root/hwlab-case-registry"); } finally { await rm(root, { recursive: true, force: true }); } }); test("case run extracts Keil async job ids from hwpod-node output", () => { const jobId = extractKeilJobId({ body: { results: [{ stdout: JSON.stringify({ ok: true, job_id: "20260605_203835_798515c0" }) }] } }); assert.equal(jobId, "20260605_203835_798515c0"); const nestedJobId = extractKeilJobId({ body: { results: [{ output: { stdout: JSON.stringify({ accepted: true, job_id: "20260605_210457_bdf3292a" }) } }] } }); assert.equal(nestedJobId, "20260605_210457_bdf3292a"); }); test("case run uses cmd.run command plus argv for Keil job-status", () => { const command = jobStatusCommandForTest("py -3", "C:\\Users\\liang\\.agents\\skills\\keil\\keil-cli.py", "job-1"); assert.deepEqual(command, { command: "py", argv: ["-3", "C:\\Users\\liang\\.agents\\skills\\keil\\keil-cli.py", "job-status", "job-1"] }); }); test("case run exposes Keil hex and axf artifacts in evidence summary", () => { assert.deepEqual(artifactsFromKeilStatusForTest({ result: { hex_file: "F:\\out.hex", axf_file: "F:\\out.axf" } }), ["F:\\out.hex", "F:\\out.axf"]); }); async function mkdtempCaseRoot() { return await import("node:fs/promises").then(({ mkdtemp }) => mkdtemp(path.join(os.tmpdir(), "hwlab-caserun-"))); } async function createCaseRepo(caseRepo: string, caseId: string) { const caseDir = path.join(caseRepo, "cases", caseId); await mkdir(path.join(caseRepo, ".git"), { recursive: true }); await writeFile(path.join(caseRepo, ".git", "HEAD"), "ref: refs/heads/main\n", "utf8"); await mkdir(caseDir, { recursive: true }); await writeFile(path.join(caseDir, "case.json"), JSON.stringify({ contractVersion: "hwpod-case-v1", caseId, hwpodSpec: "hwpod-spec.yaml", subject: { repo: "git@github.com:pikasTech/D601-HWLAB.git", ref: "main", subdir: "projects/01_baseline" } }, null, 2), "utf8"); await writeFile(path.join(caseDir, "hwpod-spec.yaml"), "kind: Hwpod\nspec:\n nodeBinding:\n nodeId: node-d601-f103-v2\n workspace:\n path: F:\\\\Work\\\\D601-HWLAB\n targetDevice:\n board: D601-F103-V2\n debugProbe:\n type: daplink\n ioProbe:\n uart:\n port: COM9\n", "utf8"); }