import assert from "node:assert/strict"; import { mkdir, readFile, rm, stat, 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"; const SUBJECT_COMMIT_ID = "df7a4e6e551fa90d64bde5537cc000f89d63dd20"; const SUBJECT_REPO_LOCAL_PATH = "F:\\Work\\HWLAB-CASE-F103"; 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: { repoLocalPath: SUBJECT_REPO_LOCAL_PATH, commitId: SUBJECT_COMMIT_ID, subdir: "projects/01_baseline" } }, null, 2), "utf8"); await writeFile(path.join(caseDir, "hwpod-spec.yaml"), hwpodSpecText(), "utf8"); const requests: any[] = []; 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", fetchImpl: subjectWorktreeFetch(requests) }); 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.equal(result.payload.subject.repoLocalPath, SUBJECT_REPO_LOCAL_PATH); assert.equal(result.payload.subject.commitId, SUBJECT_COMMIT_ID); assert.equal(result.payload.subject.worktreePath, "F:\\Work\\HWLAB-CASE-F103\\.worktree\\caserun-run-test"); const runLocalSpec = await readFile(path.join(cwd, ".state", "hwlab-cli", "caserun", "run-test", ".hwlab", "hwpod-spec.yaml"), "utf8"); assert.match(runLocalSpec, /node-d601-f103-v2/u); assert.match(runLocalSpec, /path: "F:\\\\Work\\\\HWLAB-CASE-F103\\\\\.worktree\\\\caserun-run-test"/u); assert.equal(requests.length, 1); assert.equal(requests[0].body.ops.length, 3); assert.equal(requests[0].body.ops[0].args.workspacePath, SUBJECT_REPO_LOCAL_PATH); assert.deepEqual(requests[0].body.ops[0].args.argv, ["cat-file", "-e", SUBJECT_COMMIT_ID]); assert.deepEqual(requests[0].body.ops[1].args.argv, ["worktree", "add", "--detach", "--force", ".worktree\\caserun-run-test", SUBJECT_COMMIT_ID]); assert.deepEqual(requests[0].body.ops[2].args.argv, ["-C", ".worktree\\caserun-run-test", "rev-parse", "HEAD"]); } finally { await rm(root, { recursive: true, force: true }); } }); test("case prepare rejects legacy subject repo/ref without a local repo path and commit id", 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", "legacy-case"); 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: "legacy-case", hwpodSpec: "hwpod-spec.yaml", subject: { repo: "git@github.com:pikasTech/D601-HWLAB.git", ref: "main" } }, null, 2), "utf8"); await writeFile(path.join(caseDir, "hwpod-spec.yaml"), hwpodSpecText(), "utf8"); const result = await runHwlabCli(["case", "prepare", "legacy-case", "--case-repo", caseRepo], { cwd, now: () => "2026-06-05T00:00:00.000Z", fetchImpl: subjectWorktreeFetch([]) }); assert.equal(result.exitCode, 1); assert.equal(result.payload.error.code, "subject_repo_local_path_required"); } 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", fetchImpl: subjectWorktreeFetch([]) }); 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.equal(command.command, "cmd.exe"); assert.deepEqual(command.argv.slice(0, 3), ["/d", "/s", "/c"]); assert.match(command.argv[3], /^py -3 C:\\Users\\liang\\\.agents\\skills\\keil\\keil-cli\.py job-status job-1$/u); }); 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"]); }); test("case run start returns immediately and status/result/logs are short polling commands", async () => { const root = await mkdtempCaseRoot(); const caseRepo = path.join(root, "hwlab-case-registry"); const cwd = path.join(root, "hwlab"); const caseId = "d601-f103-v2-compile"; try { await createCaseRepo(caseRepo, caseId); await mkdir(cwd, { recursive: true }); let workerArgs: string[] = []; const started = await runHwlabCli([ "case", "run", "start", caseId, "--case-repo", caseRepo, "--run-id", "run-async", "--agent-timeout-ms", "180000", "--job-timeout-ms", "70000" ], { cwd, now: () => "2026-06-05T00:00:00.000Z", startProcess: async (input) => { workerArgs = input.args; await writeFile(input.stdoutPath, "worker accepted\n", "utf8"); await writeFile(input.stderrPath, "", "utf8"); return { pid: 4242 }; } }); assert.equal(started.exitCode, 0); assert.equal(started.payload.action, "case.run.start"); assert.equal(started.payload.status, "submitted"); assert.equal(started.payload.startReturned, true); assert.equal(started.payload.pid, 4242); assert.match(started.payload.statusCommand, /case run status run-async/u); assert.match(started.payload.resultCommand, /case run result run-async/u); assert.match(started.payload.logsCommand, /case run logs run-async/u); assert.equal(workerArgs.includes("--agent-timeout-ms"), true); assert.equal(workerArgs[workerArgs.indexOf("--agent-timeout-ms") + 1], "180000"); assert.equal(workerArgs.includes("--job-timeout-ms"), true); assert.equal(workerArgs[workerArgs.indexOf("--job-timeout-ms") + 1], "70000"); const status = await runHwlabCli(["case", "run", "status", "run-async"], { cwd, now: () => "2026-06-05T00:00:01.000Z" }); assert.equal(status.exitCode, 0); assert.equal(status.payload.action, "case.run.status"); assert.equal(status.payload.status, "running"); assert.equal(status.payload.stage, "started"); assert.equal(status.payload.pid, 4242); assert.equal(status.payload.stdoutBytes, Buffer.byteLength("worker accepted\n")); assert.equal(status.payload.stderrBytes, 0); assert.match(status.payload.nextPollCommand, /case run status run-async/u); const result = await runHwlabCli(["case", "run", "result", "run-async"], { cwd, now: () => "2026-06-05T00:00:02.000Z" }); assert.equal(result.exitCode, 0); assert.equal(result.payload.action, "case.run.result"); assert.equal(result.payload.ready, false); assert.equal(result.payload.status, "running"); assert.match(result.payload.nextPollCommand, /case run status run-async/u); const logs = await runHwlabCli(["case", "run", "logs", "run-async", "--tail", "200"], { cwd, now: () => "2026-06-05T00:00:03.000Z" }); assert.equal(logs.exitCode, 0); assert.equal(logs.payload.action, "case.run.logs"); assert.equal(logs.payload.stdout, "worker accepted\n"); } finally { await rm(root, { recursive: true, force: true }); } }); test("case run result refreshes registry artifacts after worker stdout is finalized", async () => { const root = await mkdtempCaseRoot(); const caseRepo = path.join(root, "hwlab-case-registry"); const cwd = path.join(root, "hwlab"); const runId = "run-refresh-registry"; const runDir = path.join(cwd, ".state", "hwlab-cli", "caserun", runId); const registryRunDir = path.join(caseRepo, "runs", "d601-f103-v2-compile", runId); try { await mkdir(path.join(caseRepo, ".git"), { recursive: true }); await writeFile(path.join(caseRepo, ".git", "HEAD"), "ref: refs/heads/main\n", "utf8"); await mkdir(path.join(runDir, ".hwlab"), { recursive: true }); await writeFile(path.join(runDir, ".hwlab", "hwpod-spec.yaml"), hwpodSpecText(), "utf8"); await writeFile(path.join(runDir, "agent-prompt.md"), "prompt\n", "utf8"); await writeFile(path.join(runDir, "agent-diff.patch"), "", "utf8"); await writeFile(path.join(runDir, "worker.stdout.log"), "final worker payload\n", "utf8"); await writeFile(path.join(runDir, "worker.stderr.log"), "", "utf8"); const run = { caseId: "d601-f103-v2-compile", runId, runDir, caseRepo, caseDir: "", caseFile: "", sourceSpecPath: "", specPath: path.join(runDir, ".hwlab", "hwpod-spec.yaml"), apiUrl: "http://api.test", compileOnly: true, createdAt: "2026-06-05T00:00:00.000Z", updatedAt: "2026-06-05T00:00:10.000Z", status: "completed", stage: "completed", definition: {}, subject: { repoLocalPath: SUBJECT_REPO_LOCAL_PATH, commitId: SUBJECT_COMMIT_ID, subdir: "projects/01_baseline", worktreePath: "F:\\Work\\HWLAB-CASE-F103\\.worktree\\caserun-run-refresh-registry", workspacePath: "", setup: {} }, evidencePath: path.join(runDir, "evidence.json"), _control: { status: "completed", stage: "completed", stdoutPath: path.join(runDir, "worker.stdout.log"), stderrPath: path.join(runDir, "worker.stderr.log"), completedAt: "2026-06-05T00:00:10.000Z", resultPath: path.join(runDir, "result.json") } }; const evidence = { contractVersion: "hwpod-case-run-evidence-v1", caseId: run.caseId, runId, compileOnly: true, autoEvaluation: false, status: "recorded", subject: run.subject, agent: { traceId: "trc_refresh", sessionId: "ses_refresh", conversationId: "cnv_refresh", threadId: "thread-refresh", providerProfile: "deepseek", projectId: "prj_hwpod_workbench", promptSha256: "prompt-sha" }, agentTrace: { traceId: "trc_refresh", status: "recorded", terminalStatus: "completed", commandCount: 1, hwpodCommandCount: 1, hwpodBuildCommandCount: 1, agentRun: { runId: "run_agent_refresh" } }, agentDiff: { statusShort: "", diffStat: "", diffPatchSha256: "diff-sha" }, hwpod: { source: "case-run-runner-post-agent-compile-check", exitCode: 0 }, keilJob: { jobId: "job-refresh", status: "completed", returnCode: 0 }, artifacts: [], decisions: { downloadSkipped: true } }; await writeJsonFile(path.join(runDir, "run.json"), run); await writeJsonFile(path.join(runDir, "evidence.json"), evidence); await writeJsonFile(path.join(runDir, "result.json"), { ok: true, action: "case.run", status: "completed", caseId: run.caseId, runId, runDir, collect: { caseRepoFiles: [] }, evidence: {} }); await mkdir(registryRunDir, { recursive: true }); await writeFile(path.join(registryRunDir, "worker.stdout.log"), "", "utf8"); const result = await runHwlabCli(["case", "run", "result", runId], { cwd, now: () => "2026-06-05T00:00:11.000Z" }); assert.equal(result.exitCode, 0); assert.equal(result.payload.ready, true); assert.equal(result.payload.registryArchive.artifactManifestPath, path.join(registryRunDir, "artifact-manifest.json")); assert.equal(await readFile(path.join(registryRunDir, "worker.stdout.log"), "utf8"), "final worker payload\n"); const manifest = JSON.parse(await readFile(path.join(registryRunDir, "artifact-manifest.json"), "utf8")); assert.equal(manifest.trace.traceId, "trc_refresh"); assert.equal(manifest.files.some((item: any) => item.path === "worker.stdout.log" && item.bytes === Buffer.byteLength("final worker payload\n")), true); const registryResult = JSON.parse(await readFile(path.join(registryRunDir, "result.json"), "utf8")); assert.equal(registryResult.collect.artifactManifestPath, path.join(registryRunDir, "artifact-manifest.json")); } finally { await rm(root, { recursive: true, force: true }); } }); test("case run orchestrates agent prompt, diff capture, and compile evidence without auto evaluation", 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", title: "D601-F103-V2 Keil compile-only CaseRun", hwpodSpec: "hwpod-spec.yaml", subject: { repoLocalPath: SUBJECT_REPO_LOCAL_PATH, commitId: SUBJECT_COMMIT_ID, subdir: "projects/01_baseline" }, runtime: { apiUrl: "http://api.test", webUrl: "http://web.test" } }, null, 2), "utf8"); await writeFile(path.join(caseDir, "hwpod-spec.yaml"), hwpodSpecText(), "utf8"); const requests: any[] = []; let sawAgentRunningState = false; const result = await runHwlabCli([ "case", "run", "d601-f103-v2-compile", "--case-repo", caseRepo, "--run-id", "run-agent-flow", "--poll-interval-ms", "10", "--agent-poll-interval-ms", "10" ], { cwd, now: () => "2026-06-05T00:00:00.000Z", sleep: async () => { const runDir = path.join(cwd, ".state", "hwlab-cli", "caserun", "run-agent-flow"); const runState = JSON.parse(await readFile(path.join(runDir, "run.json"), "utf8")); assert.equal(runState.stage, "agent-running"); assert.equal(runState.agent.stageStatus, "running"); assert.equal(runState.agent.sessionId, "ses_case_run"); assert.equal(runState.agent.conversationId, "cnv_case_d601-f103-v2-compile_run-agent-flow"); assert.equal(runState.agent.threadId, "thread-case"); assert.match(runState.agent.traceId, /^trc_case_/u); assert.match(runState.agent.nextPollCommand, /client agent result trc_case_/u); assert.equal(runState._control.stage, "agent-running"); await writeFile(path.join(runDir, "worker.stdout.log"), "worker accepted\n", "utf8"); await writeFile(path.join(runDir, "worker.stderr.log"), "", "utf8"); sawAgentRunningState = true; }, env: { HWLAB_API_KEY: "hwl_live_test_case_agent_key" }, runProcess: async () => ({ command: [], exitCode: 0, stdout: JSON.stringify({ body: { results: [{ output: { stdout: JSON.stringify({ job_id: "job-compile-1" }) } }] } }), stderr: "" }), fetchImpl: caseRunFlowFetch(requests) }); assert.equal(result.exitCode, 0); assert.equal(result.payload.action, "case.run"); assert.equal(result.payload.status, "completed"); assert.equal(result.payload.evidence.status, "recorded"); assert.equal(result.payload.evidence.autoEvaluation, false); assert.equal(result.payload.evidence.ok, undefined); assert.equal(result.payload.agent.stageStatus, "completed"); assert.equal(result.payload.agent.traceId.startsWith("trc_case_"), true); assert.ok(result.payload.agentTrace.hwpodBuildCommandCount >= 1); assert.ok(result.payload.evidence.agentTrace.commandCount > 0); assert.ok(result.payload.evidence.agentTrace.commandCount <= 30); assert.ok(result.payload.evidence.agentTrace.hwpodCommandCount >= 3); assert.equal(result.payload.evidence.agentTrace.keilJobCandidates.includes("20260605_203835_798515c0"), true); assert.equal(result.payload.evidence.agentTrace.commands.some((item: any) => /^hwpod-ctl spec validate --spec \.hwlab\/hwpod-spec\.yaml/u.test(item.normalizedCommand ?? "")), true); assert.equal(result.payload.evidence.agentTrace.commands.some((item: any) => /^hwpod inspect --spec \.hwlab\/hwpod-spec\.yaml/u.test(item.normalizedCommand ?? "")), true); assert.equal(result.payload.evidence.agentTrace.commands.some((item: any) => /^hwpod build --spec \.hwlab\/hwpod-spec\.yaml/u.test(item.normalizedCommand ?? "")), true); assert.equal(result.payload.evidence.agentTrace.commands.some((item: any) => /cat .*hwpod-cli\/SKILL\.md/u.test(item.command ?? "") && /hwpod build/u.test(item.bodyPreview ?? "")), true); assert.equal(result.payload.evidence.agentTrace.commands.some((item: any) => /^hwpod build --spec \.hwlab\/hwpod-spec\.yaml/u.test(item.normalizedCommand ?? "")), true); assert.equal(result.payload.evidence.hwpod.source, "case-run-runner-post-agent-compile-check"); assert.match(result.payload.agentDiff.statusShort, /projects\/01_baseline\/main\.c/u); assert.equal(result.payload.build.autoEvaluation, false); assert.ok(result.payload.build.agentTraceHwpodCommandCount >= 3); assert.equal(result.payload.build.hwpodExitCode, 0); assert.equal(result.payload.build.hwpodSource, "case-run-runner-post-agent-compile-check"); assert.equal(result.payload.build.keilStatus, "completed"); assert.equal(result.payload.collect.autoEvaluation, false); assert.equal(sawAgentRunningState, true); const registryRunDir = path.join(caseRepo, "runs", "d601-f103-v2-compile", "run-agent-flow"); const registryFiles = [ "evidence.json", "summary.md", "run.json", "result.json", "agent-trace.json", "agent-trace.md", "agent-messages.json", "agent-transcript.md", "final-response.md", "agent-prompt.md", "agent-diff.patch", path.join(".hwlab", "hwpod-spec.yaml"), "worker.stdout.log", "worker.stderr.log", "artifact-manifest.json" ]; for (const file of registryFiles) await stat(path.join(registryRunDir, file)); const manifest = JSON.parse(await readFile(path.join(registryRunDir, "artifact-manifest.json"), "utf8")); assert.equal(manifest.contractVersion, "hwpod-case-run-artifact-manifest-v1"); assert.equal(manifest.trace.traceId, result.payload.agent.traceId); assert.equal(manifest.trace.sessionId, "ses_case_run"); assert.equal(manifest.trace.conversationId, "cnv_case_d601-f103-v2-compile_run-agent-flow"); assert.equal(manifest.trace.agentRun.runId, "run_agent_trace"); assert.equal(manifest.prompt.sha256, result.payload.evidence.agent.promptSha256); assert.equal(manifest.diff.sha256, result.payload.evidence.agentDiff.diffPatchSha256); assert.equal(manifest.readableAgent.renderer, "tools/src/hwlab-cli/trace-renderer:traceDisplayRows"); assert.equal(manifest.readableAgent.messagesPath, "agent-messages.json"); assert.equal(manifest.readableAgent.tracePath, "agent-trace.md"); assert.equal(manifest.readableAgent.transcriptPath, "agent-transcript.md"); assert.equal(manifest.readableAgent.finalResponsePath, "final-response.md"); assert.equal(manifest.readableAgent.finalResponse.present, true); assert.equal(manifest.readableAgent.finalResponse.text, "done"); assert.deepEqual(manifest.agentStage.kinds, ["spec-validate", "inspect", "workspace-edit", "build", "download", "uart-read"]); assert.equal(manifest.agentStage.autoEvaluation, false); assert.equal(manifest.agentStage.commands.some((item: any) => item.kind === "download" && /uvoptx file not found/u.test(item.detail)), true); assert.equal(manifest.agentStage.commands.some((item: any) => item.kind === "uart-read" && /hwpod_uart_monitor_not_active/u.test(item.detail)), true); assert.equal(manifest.decisions.autoEvaluation, false); assert.equal(manifest.decisions.downloadSkipped, undefined); assert.equal(manifest.decisions.runnerPostAgentCompileCheck, "recorded"); assert.equal(manifest.files.some((item: any) => item.path === "agent-messages.json"), true); assert.equal(manifest.files.some((item: any) => item.path === "agent-trace.md"), true); assert.equal(manifest.files.some((item: any) => item.path === "final-response.md"), true); assert.equal(manifest.files.some((item: any) => item.path === "worker.stdout.log"), true); assert.equal(manifest.files.some((item: any) => item.path === ".hwlab/hwpod-spec.yaml"), true); const messages = JSON.parse(await readFile(path.join(registryRunDir, "agent-messages.json"), "utf8")); assert.equal(messages.renderer, "tools/src/hwlab-cli/trace-renderer:traceDisplayRows"); assert.equal(messages.finalResponse.present, true); assert.match(JSON.stringify(messages.rows), /hwpod build --spec \.hwlab\/hwpod-spec\.yaml/u); const traceMarkdown = await readFile(path.join(registryRunDir, "agent-trace.md"), "utf8"); assert.match(traceMarkdown, /renderer: tools\/src\/hwlab-cli\/trace-renderer:traceDisplayRows/u); assert.match(traceMarkdown, /## Subject Diff/u); assert.match(traceMarkdown, /\+printf\("hello"\);/u); const finalResponse = await readFile(path.join(registryRunDir, "final-response.md"), "utf8"); assert.match(finalResponse, /present: true/u); assert.match(finalResponse, /done/u); assert.equal(result.payload.collect.caseRepoRunDir, registryRunDir); assert.equal(result.payload.collect.artifactManifestPath, path.join(registryRunDir, "artifact-manifest.json")); assert.equal(result.payload.collect.agentStage.commands.some((item: any) => item.kind === "download"), true); assert.equal(result.payload.collect.caseRepoFiles.some((item: string) => item.endsWith("artifact-manifest.json")), true); const registryResult = JSON.parse(await readFile(path.join(registryRunDir, "result.json"), "utf8")); assert.equal(registryResult.collect.artifactManifestPath, path.join(registryRunDir, "artifact-manifest.json")); const summaryMarkdown = await readFile(path.join(registryRunDir, "summary.md"), "utf8"); assert.match(summaryMarkdown, /## Agent HWPOD Raw Steps/u); assert.match(summaryMarkdown, /download/u); assert.match(summaryMarkdown, /uvoptx file not found/u); assert.match(summaryMarkdown, /uart-read/u); assert.match(summaryMarkdown, /hwpod_uart_monitor_not_active/u); assert.doesNotMatch(summaryMarkdown, /downloadSkipped/u); const prompt = await readFile(path.join(cwd, ".state", "hwlab-cli", "caserun", "run-agent-flow", "agent-prompt.md"), "utf8"); assert.match(prompt, /without auto-grading/u); assert.match(prompt, /Run-local HWPOD spec/u); assert.match(prompt, /verificationMode: compile-only build check/u); assert.doesNotMatch(prompt, /compile-only 阶段不要修改 subject 源码/u); assert.doesNotMatch(prompt, /Write exactly this YAML/u); assert.doesNotMatch(prompt, /```yaml/u); assert.doesNotMatch(prompt, /apiVersion: hwlab\.pikastech\.com/u); assert.doesNotMatch(prompt, /path: "F:\\\\Work\\\\HWLAB-CASE-F103/u); assert.match(prompt, /CaseRun has already placed the run-local spec/u); assert.match(prompt, /hwpod-ctl spec validate --spec \.hwlab\/hwpod-spec\.yaml/u); assert.match(prompt, /standard hwpod\/hwpod-ctl commands/u); const diff = await readFile(path.join(cwd, ".state", "hwlab-cli", "caserun", "run-agent-flow", "agent-diff.patch"), "utf8"); assert.match(diff, /printf\("hello"\)/u); assert.equal(requests.some((item) => item.url === "http://web.test/v1/agent/sessions"), true); assert.equal(requests.some((item) => item.url === "http://web.test/v1/agent/chat"), true); const agentChatRequest = requests.find((item) => item.url === "http://web.test/v1/agent/chat"); assert.equal(agentChatRequest.body.workspaceFiles[0].path, ".hwlab/hwpod-spec.yaml"); assert.equal(agentChatRequest.body.workspaceFiles[0].encoding, "utf8"); assert.match(agentChatRequest.body.workspaceFiles[0].content, /HWLAB-CASE-F103.*caserun-run-agent-flow/u); assert.doesNotMatch(agentChatRequest.body.message, /path: "F:\\Work\\HWLAB-CASE-F103\\\.worktree\\caserun-run-agent-flow"/u); assert.equal(requests.some((item) => item.url === "http://web.test/v1/agent/chat/result/" + result.payload.agent.traceId), true); assert.equal(requests.some((item) => item.url === "http://web.test/v1/agent/chat/trace/" + result.payload.agent.traceId), true); } finally { await rm(root, { recursive: true, force: true }); } }); test("case run archives rendered trace transcript when finalResponse is null", async () => { const root = await mkdtempCaseRoot(); const caseRepo = path.join(root, "hwlab-case-registry"); const cwd = path.join(root, "hwlab"); const runId = "run-final-null"; const runDir = path.join(cwd, ".state", "hwlab-cli", "caserun", runId); const registryRunDir = path.join(caseRepo, "runs", "d601-f103-v2-compile", runId); try { await mkdir(path.join(caseRepo, ".git"), { recursive: true }); await writeFile(path.join(caseRepo, ".git", "HEAD"), "ref: refs/heads/main\n", "utf8"); await mkdir(path.join(runDir, ".hwlab"), { recursive: true }); await writeFile(path.join(runDir, ".hwlab", "hwpod-spec.yaml"), hwpodSpecText(), "utf8"); await writeFile(path.join(runDir, "agent-prompt.md"), "prompt\n", "utf8"); await writeFile(path.join(runDir, "agent-diff.patch"), "diff --git a/main.c b/main.c\n+printf(\"hello\");\n", "utf8"); await writeFile(path.join(runDir, "worker.stdout.log"), "worker complete\n", "utf8"); await writeFile(path.join(runDir, "worker.stderr.log"), "", "utf8"); const run = { caseId: "d601-f103-v2-compile", runId, runDir, caseRepo, caseDir: "", caseFile: "", sourceSpecPath: "", specPath: path.join(runDir, ".hwlab", "hwpod-spec.yaml"), apiUrl: "http://api.test", compileOnly: true, createdAt: "2026-06-06T00:00:00.000Z", updatedAt: "2026-06-06T00:00:10.000Z", status: "completed", stage: "completed", definition: {}, subject: { repoLocalPath: SUBJECT_REPO_LOCAL_PATH, commitId: SUBJECT_COMMIT_ID, subdir: "projects/01_baseline", worktreePath: "F:\\Work\\HWLAB-CASE-F103\\.worktree\\caserun-run-final-null", workspacePath: "", setup: {} }, agent: { stageStatus: "failed", baseUrl: "http://web.test", projectId: "prj_hwpod_workbench", providerProfile: "deepseek", conversationId: "cnv_final_null", sessionId: "ses_final_null", threadId: "thread-final-null", traceId: "trc_final_null", promptPath: path.join(runDir, "agent-prompt.md"), promptSha256: "prompt-sha", result: { ok: true, status: "failed", finalResponse: null }, polls: 2, timedOut: false, sessionResponse: null }, agentTrace: { traceId: "trc_final_null", status: "failed", httpStatus: 200, sourceEventCount: 3, renderedRowCount: 0, commandCount: 0, hwpodCommandCount: 0, hwpodBuildCommandCount: 0, keilJobCandidates: [], terminalStatus: "failed", agentRun: { runId: "run_agent_final_null", commandId: "cmd_final_null" }, commands: [] }, agentDiff: { statusShort: " M main.c\n", diffStat: " main.c | 1 +\n", diffPatchPath: path.join(runDir, "agent-diff.patch"), diffPatchSha256: "diff-sha", sourceRootStatusShort: "", requests: [] }, evidencePath: path.join(runDir, "evidence.json"), _control: { status: "completed", stage: "completed", stdoutPath: path.join(runDir, "worker.stdout.log"), stderrPath: path.join(runDir, "worker.stderr.log"), completedAt: "2026-06-06T00:00:10.000Z", resultPath: path.join(runDir, "result.json") } }; const trace = { ok: true, status: "failed", traceId: "trc_final_null", finalResponse: null, events: [ { traceId: "trc_final_null", seq: 1, label: "agentrun:assistant:message", type: "assistant", status: "running", message: "我先检查当前 HWPOD。", createdAt: "2026-06-06T00:00:01.000Z" }, { traceId: "trc_final_null", seq: 2, label: "item/commandExecution:completed", type: "tool_call", toolName: "commandExecution", status: "completed", command: "/bin/sh -lc 'hwpod build --spec .hwlab/hwpod-spec.yaml'", stdoutSummary: "build completed", exitCode: 0, createdAt: "2026-06-06T00:00:02.000Z" }, { traceId: "trc_final_null", seq: 3, label: "agentrun:terminal:failed", type: "result", terminal: true, status: "failed", message: "Error running remote compact task: unexpected status 404 Not Found", createdAt: "2026-06-06T00:00:03.000Z" } ], error: { message: "Error running remote compact task: unexpected status 404 Not Found" } }; const evidence = { contractVersion: "hwpod-case-run-evidence-v1", caseId: run.caseId, runId, compileOnly: true, autoEvaluation: false, status: "recorded", subject: run.subject, agent: { traceId: "trc_final_null", sessionId: "ses_final_null", conversationId: "cnv_final_null", threadId: "thread-final-null", providerProfile: "deepseek", projectId: "prj_hwpod_workbench", promptSha256: "prompt-sha" }, agentTrace: run.agentTrace, agentDiff: run.agentDiff, hwpod: { source: "case-run-runner-post-agent-compile-check", exitCode: 0 }, artifacts: [], decisions: { downloadSkipped: true } }; await writeJsonFile(path.join(runDir, "run.json"), run); await writeJsonFile(path.join(runDir, "agent-trace.json"), trace); await writeJsonFile(path.join(runDir, "evidence.json"), evidence); await writeJsonFile(path.join(runDir, "result.json"), { ok: true, action: "case.run", status: "completed", caseId: run.caseId, runId, runDir, collect: { caseRepoFiles: [] }, evidence: {} }); const result = await runHwlabCli(["case", "run", "result", runId], { cwd, now: () => "2026-06-06T00:00:11.000Z" }); assert.equal(result.exitCode, 0); const manifest = JSON.parse(await readFile(path.join(registryRunDir, "artifact-manifest.json"), "utf8")); assert.equal(manifest.readableAgent.finalResponse.present, false); assert.match(manifest.readableAgent.finalResponse.reason, /404 Not Found/u); const messages = JSON.parse(await readFile(path.join(registryRunDir, "agent-messages.json"), "utf8")); assert.equal(messages.finalResponse.present, false); assert.match(JSON.stringify(messages.rows), /我先检查当前 HWPOD/u); assert.match(JSON.stringify(messages.rows), /hwpod build --spec \.hwlab\/hwpod-spec\.yaml/u); const traceMarkdown = await readFile(path.join(registryRunDir, "agent-trace.md"), "utf8"); assert.match(traceMarkdown, /finalResponse=null/u); assert.match(traceMarkdown, /Error running remote compact task/u); assert.match(traceMarkdown, /## Subject Diff/u); assert.match(traceMarkdown, /\+printf\("hello"\);/u); const finalMarkdown = await readFile(path.join(registryRunDir, "final-response.md"), "utf8"); assert.match(finalMarkdown, /present: false/u); assert.match(finalMarkdown, /404 Not Found/u); } finally { await rm(root, { recursive: true, force: true }); } }); test("case run status reports latest run stage when async control stage is stale", async () => { const root = await mkdtempCaseRoot(); const cwd = path.join(root, "hwlab"); const runDir = path.join(cwd, ".state", "hwlab-cli", "caserun", "run-stale-stage"); try { await mkdir(runDir, { recursive: true }); await writeFile(path.join(runDir, "worker.stdout.log"), "agent finished\n", "utf8"); await writeFile(path.join(runDir, "worker.stderr.log"), "", "utf8"); await writeFile(path.join(runDir, "run.json"), JSON.stringify({ caseId: "d601-f103-v2-compile", runId: "run-stale-stage", runDir, caseRepo: "", caseDir: "", caseFile: "", sourceSpecPath: "", specPath: path.join(runDir, ".hwlab", "hwpod-spec.yaml"), apiUrl: "http://api.test", compileOnly: true, createdAt: "2026-06-05T00:00:00.000Z", updatedAt: "2026-06-05T00:00:10.000Z", status: "running", stage: "agent-running", definition: {}, subject: { repoLocalPath: SUBJECT_REPO_LOCAL_PATH, commitId: SUBJECT_COMMIT_ID, subdir: "projects/01_baseline", worktreePath: "F:\\Work\\HWLAB-CASE-F103\\.worktree\\caserun-run-stale-stage", workspacePath: "", setup: {} }, agent: { stageStatus: "completed", baseUrl: "http://web.test", projectId: "prj_hwpod_workbench", providerProfile: "deepseek", conversationId: "cnv_case", sessionId: "ses_case", threadId: "thread-case", traceId: "trc_case", promptPath: path.join(runDir, "agent-prompt.md"), promptSha256: "sha", polls: 3, lastHttpStatus: 200 }, agentDiff: { statusShort: " M projects/01_baseline/main.c\n", diffStat: " projects/01_baseline/main.c | 1 +\n", diffPatchPath: path.join(runDir, "agent-diff.patch"), diffPatchSha256: "sha" }, _control: { status: "running", stage: "prepared", pid: 1234, stdoutPath: path.join(runDir, "worker.stdout.log"), stderrPath: path.join(runDir, "worker.stderr.log"), startedAt: "2026-06-05T00:00:00.000Z" } }, null, 2), "utf8"); const status = await runHwlabCli(["case", "run", "status", "run-stale-stage"], { cwd, now: () => "2026-06-05T00:00:12.000Z" }); assert.equal(status.exitCode, 0); assert.equal(status.payload.stage, "agent-diff-collected"); assert.equal(status.payload.staleControlWarning.code, "stale_control_stage"); assert.equal(status.payload.staleControlWarning.controlStage, "prepared"); assert.equal(status.payload.staleControlWarning.selectedStage, "agent-diff-collected"); assert.equal(status.payload.agent.stageStatus, "completed"); assert.match(status.payload.agentDiff.statusShort, /projects\/01_baseline\/main\.c/u); const result = await runHwlabCli(["case", "run", "result", "run-stale-stage"], { cwd, now: () => "2026-06-05T00:00:13.000Z" }); assert.equal(result.exitCode, 0); assert.equal(result.payload.ready, false); assert.equal(result.payload.stage, "agent-diff-collected"); assert.equal(result.payload.staleControlWarning.code, "stale_control_stage"); } finally { await rm(root, { recursive: true, force: true }); } }); test("case run status keeps worker control stage ahead of prepared worktree evidence", async () => { const root = await mkdtempCaseRoot(); const cwd = path.join(root, "hwlab"); const runDir = path.join(cwd, ".state", "hwlab-cli", "caserun", "run-worker-started"); try { await mkdir(runDir, { recursive: true }); await writeFile(path.join(runDir, "worker.stdout.log"), "", "utf8"); await writeFile(path.join(runDir, "worker.stderr.log"), "", "utf8"); await writeFile(path.join(runDir, "run.json"), JSON.stringify({ caseId: "d601-f103-v2-compile", runId: "run-worker-started", runDir, caseRepo: "", caseDir: "", caseFile: "", sourceSpecPath: "", specPath: path.join(runDir, ".hwlab", "hwpod-spec.yaml"), apiUrl: "http://api.test", compileOnly: true, createdAt: "2026-06-05T00:00:00.000Z", updatedAt: "2026-06-05T00:00:10.000Z", status: "running", stage: "queued", definition: {}, subject: { repoLocalPath: SUBJECT_REPO_LOCAL_PATH, commitId: SUBJECT_COMMIT_ID, subdir: "projects/01_baseline", worktreePath: "F:\\Work\\HWLAB-CASE-F103\\.worktree\\caserun-run-worker-started", workspacePath: "", setup: {} }, _control: { status: "running", stage: "started", pid: 1234, stdoutPath: path.join(runDir, "worker.stdout.log"), stderrPath: path.join(runDir, "worker.stderr.log"), startedAt: "2026-06-05T00:00:00.000Z" } }, null, 2), "utf8"); const status = await runHwlabCli(["case", "run", "status", "run-worker-started"], { cwd, now: () => "2026-06-05T00:00:12.000Z" }); assert.equal(status.exitCode, 0); assert.equal(status.payload.stage, "started"); assert.equal(status.payload.staleControlWarning, undefined); } finally { await rm(root, { recursive: true, force: true }); } }); 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: { repoLocalPath: SUBJECT_REPO_LOCAL_PATH, commitId: SUBJECT_COMMIT_ID, subdir: "projects/01_baseline" } }, null, 2), "utf8"); await writeFile(path.join(caseDir, "hwpod-spec.yaml"), hwpodSpecText(), "utf8"); } async function writeJsonFile(file: string, value: any) { await mkdir(path.dirname(file), { recursive: true }); await writeFile(file, `${JSON.stringify(value, null, 2)}\n`, "utf8"); } function hwpodSpecText() { return "kind: Hwpod\nmetadata:\n name: d601-f103-v2\nspec:\n nodeBinding:\n nodeId: node-d601-f103-v2\n workspace:\n path: F:\\\\Work\\\\HWLAB-CASE-F103\n targetDevice:\n board: D601-F103-V2\n debugProbe:\n type: daplink\n ioProbe:\n uart:\n port: COM9\n"; } function subjectWorktreeFetch(requests: any[]) { return async (url: RequestInfo | URL, init?: RequestInit) => { const body = JSON.parse(String(init?.body ?? "{}")); requests.push({ url: String(url), body }); return new Response(JSON.stringify({ body: { results: [ { ok: true, output: { exitCode: 0, stdout: "" } }, { ok: true, output: { exitCode: 0, stdout: "Preparing worktree\n" } }, { ok: true, output: { exitCode: 0, stdout: `${SUBJECT_COMMIT_ID}\n` } } ] } }), { status: 200, headers: { "content-type": "application/json" } }); }; } function caseRunFlowFetch(requests: any[]) { let resultPolls = 0; return async (url: RequestInfo | URL, init?: RequestInit) => { const rawBody = String(init?.body ?? "{}"); const body = rawBody ? JSON.parse(rawBody) : {}; requests.push({ url: String(url), body }); if (String(url) === "http://web.test/v1/agent/sessions") { return new Response(JSON.stringify({ ok: true, session: { sessionId: "ses_case_run", conversationId: body.conversationId, threadId: "thread-case", providerProfile: body.providerProfile } }), { status: 200, headers: { "content-type": "application/json" } }); } if (String(url) === "http://web.test/v1/agent/chat") { return new Response(JSON.stringify({ ok: true, accepted: true, traceId: body.traceId, resultUrl: `/v1/agent/chat/result/${body.traceId}` }), { status: 202, headers: { "content-type": "application/json" } }); } if (String(url).startsWith("http://web.test/v1/agent/chat/result/")) { resultPolls += 1; return new Response(JSON.stringify({ ok: true, status: resultPolls > 1 ? "completed" : "running", reply: { content: "done" } }), { status: 200, headers: { "content-type": "application/json" } }); } if (String(url).startsWith("http://web.test/v1/agent/chat/trace/")) { const events = caseRunTraceEvents(); return new Response(JSON.stringify({ ok: true, status: "completed", sourceEventCount: events.length, renderedRowCount: 0, traceSummary: { terminalStatus: "completed", agentRun: { runId: "run_agent_trace", commandId: "cmd_agent_trace" } }, events }), { status: 200, headers: { "content-type": "application/json" } }); } if (body.ops?.[0]?.args?.argv?.includes("status") && body.ops?.length === 1) { return hwpodResponse([{ exitCode: 0, stdout: body.ops[0].args.argv.includes("--short") ? " M projects/01_baseline/main.c\n" : "" }]); } if (body.ops?.[0]?.args?.argv?.includes("--stat")) { return hwpodResponse([{ exitCode: 0, stdout: " projects/01_baseline/main.c | 1 +\n" }]); } if (body.ops?.[0]?.args?.argv?.includes("--binary")) { return hwpodResponse([{ exitCode: 0, stdout: "+printf(\"hello\");\n" }]); } if (body.ops?.[0]?.args?.argv?.includes("job-status")) { return hwpodResponse([{ exitCode: 0, stdout: JSON.stringify({ status: "completed", return_code: 0, result: { hex_file: "F:\\out.hex", axf_file: "F:\\out.axf" } }) }]); } return subjectWorktreeFetch(requests)(url, init); }; } function caseRunTraceEvents() { const events: any[] = [ { seq: 1, label: "item/commandExecution:completed", type: "tool_call", toolName: "commandExecution", status: "completed", command: "cat .agents/skills/hwpod-cli/SKILL.md", message: "use `hwpod build` for compile", exitCode: 0 }, { seq: 248, label: "item/commandExecution:completed", type: "tool_call", toolName: "commandExecution", status: "completed", command: "/bin/sh -lc 'hwpod-ctl spec validate --spec .hwlab/hwpod-spec.yaml'", message: "validated", exitCode: 0 }, { seq: 323, label: "item/commandExecution:completed", type: "tool_call", toolName: "commandExecution", status: "completed", command: "/bin/sh -lc 'hwpod inspect --spec .hwlab/hwpod-spec.yaml'", message: "inspected", exitCode: 0 }, { seq: 360, label: "item/commandExecution:completed", type: "tool_call", toolName: "commandExecution", status: "completed", command: "/bin/sh -lc 'hwpod workspace insert-after --spec .hwlab/hwpod-spec.yaml --path projects/01_baseline/User/main.c --anchor ready --line marker'", message: "inserted marker", exitCode: 0 }, { seq: 480, label: "item/commandExecution:completed", type: "tool_call", toolName: "commandExecution", status: "completed", command: "/bin/sh -lc 'hwpod download --spec .hwlab/hwpod-spec.yaml'", message: "uvoptx file not found; program accepted job 20260605_213000_aaaaaaaa", exitCode: 1 }, { seq: 520, label: "item/commandExecution:completed", type: "tool_call", toolName: "commandExecution", status: "completed", command: "/bin/sh -lc 'hwpod uart read --spec .hwlab/hwpod-spec.yaml --port uart1 --max-bytes 4096'", message: "hwpod_uart_monitor_not_active on COM9", exitCode: 1 } ]; for (let index = 0; index < 31; index += 1) { const seq = 400 + index * 2; events.push({ seq, label: "item/commandExecution:started", type: "tool_call", toolName: "commandExecution", status: "started", command: "/bin/sh -lc 'hwpod build --spec .hwlab/hwpod-spec.yaml'", message: "commandExecution inProgress: /bin/sh -lc 'hwpod build --spec .hwlab/hwpod-spec.yaml'" }); events.push({ seq: seq + 1, label: "item/commandExecution:completed", type: "tool_call", toolName: "commandExecution", status: "completed", command: "/bin/sh -lc 'hwpod build --spec .hwlab/hwpod-spec.yaml'", message: index === 0 ? "{\"job_id\":\"20260605_203835_798515c0\"}" : "build completed", exitCode: 0 }); } return events; } function hwpodResponse(outputs: Array<{ exitCode: number; stdout?: string; stderr?: string }>) { return new Response(JSON.stringify({ body: { results: outputs.map((output) => ({ ok: output.exitCode === 0, output })) } }), { status: 200, headers: { "content-type": "application/json" } }); }