diff --git a/tools/hwlab-cli/caserun.test.ts b/tools/hwlab-cli/caserun.test.ts index c66bf3cf..2e2179ef 100644 --- a/tools/hwlab-cli/caserun.test.ts +++ b/tools/hwlab-cli/caserun.test.ts @@ -185,6 +185,80 @@ test("case run start returns immediately and status/result/logs are short pollin } }); +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"); @@ -419,6 +493,11 @@ async function createCaseRepo(caseRepo: string, caseId: string) { 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"; } diff --git a/tools/src/hwlab-caserun-lib.ts b/tools/src/hwlab-caserun-lib.ts index fd33acd7..64b11a8f 100644 --- a/tools/src/hwlab-caserun-lib.ts +++ b/tools/src/hwlab-caserun-lib.ts @@ -314,17 +314,19 @@ export async function statusCaseRun(context: CaseContext) { const runId = requiredText(context.parsed.runId ?? context.rest[2], "runId"); const run = await readRunById(context, runId); const control = controlFromRun(run); + const status = text(control.status) || text(run.status) || "unknown"; const stage = statusStage(run, control); const stdoutPath = text(control.stdoutPath) || path.join(run.runDir, "worker.stdout.log"); const stderrPath = text(control.stderrPath) || path.join(run.runDir, "worker.stderr.log"); const stdoutBytes = await fileSize(stdoutPath); const stderrBytes = await fileSize(stderrPath); + const registryArchive = status === "completed" ? await refreshCompletedRunArchive(context, run) : null; return ok("case.run.status", { caseId: run.caseId, runId: run.runId, runDir: run.runDir, stateFile: path.join(run.runDir, "run.json"), - status: text(control.status) || text(run.status) || "unknown", + status, stage: stage.stage, staleControlWarning: stage.warning, pid: control.pid ?? null, @@ -336,6 +338,7 @@ export async function statusCaseRun(context: CaseContext) { stderrPath, stdoutBytes, stderrBytes, + registryArchive: registryArchive?.summary ?? null, prepare: run.subject?.worktreePath ? prepareSummary(run) : null, agent: run.agent ? agentSummary(run.agent) : null, agentDiff: run.agentDiff ? diffSummary(run.agentDiff) : null, @@ -367,7 +370,14 @@ export async function resultCaseRun(context: CaseContext) { } const evidencePath = text(run.evidencePath) || path.join(run.runDir, "evidence.json"); const evidence = await readJsonIfExists(evidencePath); - const result = await readJsonIfExists(path.join(run.runDir, "result.json")); + const resultPath = path.join(run.runDir, "result.json"); + let result = await readJsonIfExists(resultPath); + let registryArchive = evidence ? await refreshCompletedRunArchive(context, run, evidence) : null; + if (registryArchive && result && typeof result === "object") { + result = { ...result, collect: registryArchive.summary }; + await writeJson(resultPath, result); + registryArchive = await refreshCompletedRunArchive(context, run, evidence); + } return ok("case.run.result", { caseId: run.caseId, runId: run.runId, @@ -375,7 +385,8 @@ export async function resultCaseRun(context: CaseContext) { status, ready: true, evidencePath, - resultPath: path.join(run.runDir, "result.json"), + resultPath, + registryArchive: registryArchive?.summary ?? null, summary: evidence ? buildSummary(evidence) : null, agent: run.agent ? agentSummary(run.agent) : null, agentDiff: run.agentDiff ? diffSummary(run.agentDiff) : null, @@ -1409,6 +1420,15 @@ function collectSummaryFromArchive(run: PreparedCaseRun, evidence: any, archive: }; } +async function refreshCompletedRunArchive(context: CaseContext, run: PreparedCaseRun, evidence?: any) { + if (context.parsed.noCaseRepoRecord === true) return null; + if (!text(run.caseRepo)) return null; + const loadedEvidence = evidence ?? await readJsonIfExists(path.join(run.runDir, "evidence.json")); + if (!loadedEvidence) return null; + const archive = await archiveCaseRunArtifacts(context, run, loadedEvidence); + return { evidence: loadedEvidence, archive, summary: collectSummaryFromArchive(run, loadedEvidence, archive) }; +} + function caseRunArtifactEntries(run: PreparedCaseRun) { return [ { rel: "run.json", source: path.join(run.runDir, "run.json") },