case-run: refresh registry artifacts from result

This commit is contained in:
Codex Agent
2026-06-06 16:39:12 +08:00
parent 7d7642dfce
commit 21b66b28cc
2 changed files with 102 additions and 3 deletions
+79
View File
@@ -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";
}
+23 -3
View File
@@ -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") },