fix: sync caserun registry artifacts
This commit is contained in:
@@ -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");
|
||||
|
||||
@@ -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),
|
||||
|
||||
Reference in New Issue
Block a user