fix: sync caserun registry artifacts

This commit is contained in:
Codex Agent
2026-06-06 21:35:21 +08:00
parent 64c13b5b0a
commit fc7ed2c3ab
3 changed files with 72 additions and 16 deletions
+16 -1
View File
@@ -281,6 +281,7 @@ test("case run orchestrates agent prompt, diff capture, and compile evidence wit
await writeFile(path.join(caseDir, "hwpod-spec.yaml"), hwpodSpecText(), "utf8");
const requests: any[] = [];
const runProcessCalls: string[][] = [];
let sawAgentRunningState = false;
const result = await runHwlabCli([
"case",
@@ -315,7 +316,16 @@ test("case run orchestrates agent prompt, diff capture, and compile evidence wit
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: "" }),
runProcess: async (command) => {
runProcessCalls.push(command);
const gitArgs = command.slice(1);
if (command[0] === "git" && gitArgs[0] === "status") return { command, exitCode: 0, stdout: "?? runs/d601-f103-v2-compile/run-agent-flow/\n", stderr: "" };
if (command[0] === "git" && gitArgs[0] === "add") return { command, exitCode: 0, stdout: "", stderr: "" };
if (command[0] === "git" && gitArgs[0] === "diff") return { command, exitCode: 1, stdout: "", stderr: "" };
if (command[0] === "git" && gitArgs[0] === "commit") return { command, exitCode: 0, stdout: "[main abc1234] data: archive caserun run-agent-flow\n", stderr: "" };
if (command[0] === "git" && gitArgs[0] === "push") return { command, exitCode: 0, stdout: "", stderr: "To github.com:pikasTech/hwlab-case-registry.git\n" };
return { command, exitCode: 0, stdout: JSON.stringify({ body: { results: [{ output: { stdout: JSON.stringify({ job_id: "job-compile-1" }) } }] } }), stderr: "" };
},
fetchImpl: caseRunFlowFetch(requests)
});
@@ -348,6 +358,11 @@ test("case run orchestrates agent prompt, diff capture, and compile evidence wit
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(result.payload.collect.registrySync.status, "pushed");
assert.equal(result.payload.collect.registrySync.path, "runs/d601-f103-v2-compile/run-agent-flow");
assert.equal(runProcessCalls.some((command) => command[0] === "git" && command[1] === "add" && command.at(-1) === "runs/d601-f103-v2-compile/run-agent-flow"), true);
assert.equal(runProcessCalls.some((command) => command[0] === "git" && command[1] === "commit" && command.includes("data: archive caserun run-agent-flow")), true);
assert.equal(runProcessCalls.some((command) => command[0] === "git" && command[1] === "push" && command.includes("origin") && command.includes("HEAD")), true);
assert.equal(sawAgentRunningState, true);
const registryRunDir = path.join(caseRepo, "runs", "d601-f103-v2-compile", "run-agent-flow");
+55 -15
View File
@@ -168,6 +168,7 @@ type CaseRegistryArchive = {
files: CaseRegistryArchiveFile[];
skippedFiles: string[];
manifest: Record<string, unknown>;
registrySync?: Record<string, unknown> | null;
};
type CaseReadableAgentArchive = {
@@ -241,10 +242,14 @@ export async function runCaseRun(context: CaseContext) {
const resultPath = path.join(collected.run.runDir, "result.json");
const completed = await writeRunControl(context, collected.run, { status: "completed", stage: "completed", completedAt: context.now(), resultPath });
await writeJson(resultPath, compactCaseRunResult({ ...collected, run: completed.run }));
const finalArchive = context.parsed.noCaseRepoRecord === true ? null : await archiveCaseRunArtifacts(context, completed.run, collected.evidence);
const finalCollect = finalArchive ? collectSummaryFromArchive(completed.run, collected.evidence, finalArchive) : collected.summary;
let finalArchive = context.parsed.noCaseRepoRecord === true ? null : await archiveCaseRunArtifacts(context, completed.run, collected.evidence, { sync: false });
let finalCollect = finalArchive ? collectSummaryFromArchive(completed.run, collected.evidence, finalArchive) : collected.summary;
await writeJson(resultPath, compactCaseRunResult({ ...collected, run: completed.run, summary: finalCollect }));
if (context.parsed.noCaseRepoRecord !== true) await archiveCaseRunArtifacts(context, completed.run, collected.evidence);
if (context.parsed.noCaseRepoRecord !== true) {
finalArchive = await archiveCaseRunArtifacts(context, completed.run, collected.evidence, { sync: true });
finalCollect = collectSummaryFromArchive(completed.run, collected.evidence, finalArchive);
await writeJson(resultPath, compactCaseRunResult({ ...collected, run: completed.run, summary: finalCollect }));
}
return ok("case.run", {
caseId: collected.run.caseId,
runId: collected.run.runId,
@@ -321,10 +326,12 @@ async function runCaseRunWorker(context: CaseContext) {
if (workerContext.parsed.noCaseRepoRecord !== true) {
const evidence = await readJsonIfExists(path.join(runDir, "evidence.json"));
if (evidence) {
const archive = await archiveCaseRunArtifacts(workerContext, completed.run, evidence);
let archive = await archiveCaseRunArtifacts(workerContext, completed.run, evidence, { sync: false });
payload = { ...payload, collect: collectSummaryFromArchive(completed.run, evidence, archive) };
await writeJson(path.join(runDir, "result.json"), payload);
archive = await archiveCaseRunArtifacts(workerContext, completed.run, evidence, { sync: true });
payload = { ...payload, collect: collectSummaryFromArchive(completed.run, evidence, archive) };
await writeJson(path.join(runDir, "result.json"), payload);
await archiveCaseRunArtifacts(workerContext, completed.run, evidence);
}
}
return payload;
@@ -400,11 +407,15 @@ export async function resultCaseRun(context: CaseContext) {
const evidence = await readJsonIfExists(evidencePath);
const resultPath = path.join(run.runDir, "result.json");
let result = await readJsonIfExists(resultPath);
let registryArchive = evidence ? await refreshCompletedRunArchive(context, run, evidence) : null;
let registryArchive = evidence ? await refreshCompletedRunArchive(context, run, evidence, { sync: false }) : null;
if (registryArchive && result && typeof result === "object") {
result = { ...result, collect: registryArchive.summary };
await writeJson(resultPath, result);
registryArchive = await refreshCompletedRunArchive(context, run, evidence);
registryArchive = await refreshCompletedRunArchive(context, run, evidence, { sync: true });
result = { ...result, collect: registryArchive.summary };
await writeJson(resultPath, result);
} else if (evidence) {
registryArchive = await refreshCompletedRunArchive(context, run, evidence, { sync: true });
}
return ok("case.run.result", {
caseId: run.caseId,
@@ -549,7 +560,7 @@ export async function buildCaseRun(context: CaseContext, prepared?: PreparedCase
export async function collectCaseRun(context: CaseContext, prepared?: PreparedCaseRun, knownEvidence?: any) {
const run = prepared ?? await loadRunFromDirOrFail(context);
const evidence = knownEvidence ?? JSON.parse(await readFile(path.join(run.runDir, "evidence.json"), "utf8"));
const archive = context.parsed.noCaseRepoRecord === true ? null : await archiveCaseRunArtifacts(context, run, evidence);
const archive = context.parsed.noCaseRepoRecord === true ? null : await archiveCaseRunArtifacts(context, run, evidence, { sync: !prepared });
const summary = archive ? collectSummaryFromArchive(run, evidence, archive) : {
caseRepo: run.caseRepo,
caseRepoRunDir: null,
@@ -1563,7 +1574,7 @@ function markdownTableText(value: unknown, maxBytes = 200) {
return clipText(value, maxBytes).replace(/\|/gu, "\\|").replace(/\r?\n/gu, " ").trim();
}
async function archiveCaseRunArtifacts(context: CaseContext, run: PreparedCaseRun, evidence: any): Promise<CaseRegistryArchive> {
async function archiveCaseRunArtifacts(context: CaseContext, run: PreparedCaseRun, evidence: any, options: { sync?: boolean } = {}): Promise<CaseRegistryArchive> {
const runRel = caseRegistryRunRel(run);
const caseRepoRunDir = path.join(run.caseRepo, runRel);
await mkdir(caseRepoRunDir, { recursive: true });
@@ -1586,11 +1597,39 @@ async function archiveCaseRunArtifacts(context: CaseContext, run: PreparedCaseRu
const files: CaseRegistryArchiveFile[] = [];
for (const [rel, source] of materialized) files.push(await registryArtifactFileRecord(run, caseRepoRunDir, rel, source));
const manifest = caseRunArtifactManifest(context, run, evidence, files, skippedFiles, readableAgent);
const artifactManifestPath = path.join(caseRepoRunDir, "artifact-manifest.json");
const existingManifest = await readJsonIfExists(artifactManifestPath);
const manifest = caseRunArtifactManifest(context, run, evidence, files, skippedFiles, readableAgent, existingManifest);
await writeJson(artifactManifestPath, manifest);
const caseRepoFiles = [...files.map((file) => artifactRel(path.join(runRel, file.path))), artifactRel(path.join(runRel, "artifact-manifest.json"))];
return { caseRepoRunDir, artifactManifestPath, caseRepoFiles, files, skippedFiles, manifest };
const registrySync = options.sync === true ? await syncCaseRegistryRun(context, run, runRel) : null;
return { caseRepoRunDir, artifactManifestPath, caseRepoFiles, files, skippedFiles, manifest, registrySync };
}
async function syncCaseRegistryRun(context: CaseContext, run: PreparedCaseRun, runRel: string) {
if (context.parsed.noCaseRepoGitSync === true || context.parsed.noRegistryGitSync === true) {
return { status: "skipped", reason: "disabled_by_cli_flag" };
}
const rel = artifactRel(runRel);
const before = await gitProcess(context, run.caseRepo, ["status", "--short", "--", rel]);
if (before.exitCode !== 0) return { status: "failed", stage: "status_before", command: before.command, exitCode: before.exitCode, stderr: clipText(before.stderr) };
if (!text(before.stdout)) return { status: "unchanged", path: rel };
const add = await gitProcess(context, run.caseRepo, ["add", "--", rel]);
if (add.exitCode !== 0) return { status: "failed", stage: "add", command: add.command, exitCode: add.exitCode, stderr: clipText(add.stderr) };
const staged = await gitProcess(context, run.caseRepo, ["diff", "--cached", "--quiet", "--", rel]);
if (staged.exitCode === 0) return { status: "unchanged_after_add", path: rel };
if (staged.exitCode !== 1) return { status: "failed", stage: "diff_cached", command: staged.command, exitCode: staged.exitCode, stderr: clipText(staged.stderr) };
const commitMessage = `data: archive caserun ${run.runId}`;
const commit = await gitProcess(context, run.caseRepo, ["commit", "-m", commitMessage, "--", rel]);
if (commit.exitCode !== 0) return { status: "failed", stage: "commit", command: commit.command, exitCode: commit.exitCode, stderr: clipText(commit.stderr), stdout: clipText(commit.stdout) };
const push = await gitProcess(context, run.caseRepo, ["push", "origin", "HEAD"]);
if (push.exitCode !== 0) return { status: "failed", stage: "push", command: push.command, exitCode: push.exitCode, stderr: clipText(push.stderr), stdout: clipText(push.stdout) };
const commitSha = text(commit.stdout.match(/\[\S+\s+([0-9a-f]{7,40})\]/iu)?.[1]);
return clean({ status: "pushed", path: rel, commitMessage, commitSha, commitStdout: clipText(commit.stdout, 1000), pushStdout: clipText(push.stdout, 1000), pushStderr: clipText(push.stderr, 1000) });
}
async function gitProcess(context: CaseContext, cwd: string, args: string[]) {
return (context.runProcess ?? runProcess)(["git", ...args], cwd, { ...process.env, ...context.env });
}
function collectSummaryFromArchive(run: PreparedCaseRun, evidence: any, archive: CaseRegistryArchive) {
@@ -1601,6 +1640,7 @@ function collectSummaryFromArchive(run: PreparedCaseRun, evidence: any, archive:
artifactManifestPath: archive.artifactManifestPath,
artifactCount: archive.files.length + 1,
skippedFiles: archive.skippedFiles,
registrySync: archive.registrySync ?? null,
trace: archive.manifest.trace ?? null,
traceLookup: archive.manifest.traceLookup ?? agentTraceLookupForRun(run, evidence),
agentStage: archive.manifest.agentStage ?? agentStageSummary(evidence),
@@ -1771,12 +1811,12 @@ function renderFinalResponseArtifact(run: PreparedCaseRun, evidence: any, finalR
].join("\n")}\n`;
}
async function refreshCompletedRunArchive(context: CaseContext, run: PreparedCaseRun, evidence?: any) {
async function refreshCompletedRunArchive(context: CaseContext, run: PreparedCaseRun, evidence?: any, options: { sync?: boolean } = {}) {
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);
const archive = await archiveCaseRunArtifacts(context, run, loadedEvidence, options);
return { evidence: loadedEvidence, archive, summary: collectSummaryFromArchive(run, loadedEvidence, archive) };
}
@@ -1816,12 +1856,12 @@ async function registryArtifactFileRecord(run: PreparedCaseRun, caseRepoRunDir:
});
}
function caseRunArtifactManifest(context: CaseContext, run: PreparedCaseRun, evidence: any, files: CaseRegistryArchiveFile[], skippedFiles: string[], readableAgent: CaseReadableAgentArchive) {
function caseRunArtifactManifest(context: CaseContext, run: PreparedCaseRun, evidence: any, files: CaseRegistryArchiveFile[], skippedFiles: string[], readableAgent: CaseReadableAgentArchive, existingManifest?: any) {
return clean({
contractVersion: "hwpod-case-run-artifact-manifest-v1",
caseId: run.caseId,
runId: run.runId,
archivedAt: context.now(),
archivedAt: text(existingManifest?.archivedAt) || context.now(),
sourceRunDir: run.runDir,
caseRepoRunDir: path.join(run.caseRepo, caseRegistryRunRel(run)),
trace: caseRunArtifactTrace(run, evidence),