case-run: refresh registry artifacts from result
This commit is contained in:
@@ -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";
|
||||
}
|
||||
|
||||
@@ -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") },
|
||||
|
||||
Reference in New Issue
Block a user