From 760df7938cad21c61793109b77eef29d0f13d6b7 Mon Sep 17 00:00:00 2001 From: Codex Agent Date: Sun, 7 Jun 2026 01:52:51 +0800 Subject: [PATCH] fix: record caserun source-root baseline --- tools/hwlab-caserun.test.ts | 95 ++++++++++++++++++++++++++++++ tools/src/hwlab-caserun-lib.ts | 104 +++++++++++++++++++++++++++++++-- 2 files changed, 193 insertions(+), 6 deletions(-) create mode 100644 tools/hwlab-caserun.test.ts diff --git a/tools/hwlab-caserun.test.ts b/tools/hwlab-caserun.test.ts new file mode 100644 index 00000000..db1e09af --- /dev/null +++ b/tools/hwlab-caserun.test.ts @@ -0,0 +1,95 @@ +import assert from "node:assert/strict"; +import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import { test } from "bun:test"; + +import { prepareCaseRun } from "./src/hwlab-caserun-lib.ts"; + +const NOW = "2026-06-06T00:00:00.000Z"; +const SUBJECT_COMMIT = "df7a4e6e551fa90d64bde5537cc000f89d63dd20"; + +test("CaseRun records source root baseline so pre-existing dirty files are not attributed to the run", async () => { + const root = await mkdtemp(path.join(os.tmpdir(), "hwlab-caserun-source-root-")); + try { + const caseRepo = path.join(root, "case-registry"); + const caseDir = path.join(caseRepo, "cases", "dirty-source-root"); + await mkdir(path.join(caseRepo, ".git"), { recursive: true }); + await mkdir(caseDir, { recursive: true }); + await writeFile(path.join(caseRepo, ".git", "HEAD"), "ref: refs/heads/main\n", "utf8"); + await writeFile(path.join(caseDir, "case.json"), JSON.stringify({ + contractVersion: "hwpod-case-v1", + caseId: "dirty-source-root", + hwpodSpec: "hwpod-spec.yaml", + subject: { + repoLocalPath: "F:\\Work\\HWLAB-CASE-F103", + commitId: SUBJECT_COMMIT, + subdir: "projects/01_baseline" + }, + runtime: { apiUrl: "http://hwlab.test" }, + agentTask: { workspace: "subjectWorktree", prompt: "record source root baseline", constraints: [] } + }, null, 2), "utf8"); + await writeFile(path.join(caseDir, "hwpod-spec.yaml"), [ + "apiVersion: hwlab.dev/v0alpha1", + "kind: Hwpod", + "metadata:", + " name: d601-f103-v2", + "spec:", + " targetDevice:", + " board: D601-F103-V2", + " mcu: STM32F103", + " workspace:", + " path: \"F:\\\\Work\\\\HWLAB-CASE-F103\"", + " toolchain: keil-mdk", + " keilProject: projects/01_baseline/Projects/MDK-ARM/atk_f103.uvprojx", + " debugProbe:", + " type: daplink", + " adapter: keil", + " ioProbe:", + " uart:", + " id: uart/1", + " port: COM9", + " nodeBinding:", + " nodeId: node-d601-f103-v2", + "" + ].join("\n"), "utf8"); + + const dirtyStatus = " M projects/01_baseline/Projects/MDK-ARM/atk_f103.uvprojx\n"; + const dirtyDiff = "diff --git a/projects/01_baseline/Projects/MDK-ARM/atk_f103.uvprojx b/projects/01_baseline/Projects/MDK-ARM/atk_f103.uvprojx\n+Keil.STM32F1xx_DFP.2.4.1\n"; + const fetchImpl = async (_url: string, init: RequestInit) => { + const plan = JSON.parse(String(init.body)); + const results = plan.ops.map((op: any) => ({ opId: op.opId, op: op.op, ok: true, status: "completed", output: outputForGit(op.args.argv, dirtyStatus, dirtyDiff) })); + return new Response(JSON.stringify({ ok: true, status: "completed", body: { results } }), { status: 200, headers: { "content-type": "application/json" } }); + }; + + const result = await prepareCaseRun({ + parsed: { _: [], caseRepo, runId: "source-root-baseline" }, + env: {}, + fetchImpl: fetchImpl as typeof fetch, + cwd: root, + now: () => NOW, + sleep: async () => {}, + rest: ["prepare", "dirty-source-root"] + }); + + assert.equal(result.status, "succeeded"); + assert.equal(result.summary.status, "prepared"); + assert.equal(result.run.subject.sourceRootBaseline.statusShort, dirtyStatus.trim()); + assert.equal(result.run.subject.sourceRootAfterPrepare.statusShort, dirtyStatus.trim()); + assert.equal(result.run.subject.sourceRootBaseline.diffSha256, result.run.subject.sourceRootAfterPrepare.diffSha256); + assert.match(await readFile(result.specPath, "utf8"), /caserun-source-root-baseline/u); + } finally { + await rm(root, { recursive: true, force: true }); + } +}); + +function outputForGit(argv: string[], dirtyStatus: string, dirtyDiff: string) { + const command = argv.join(" "); + if (command.includes("cat-file -e")) return { exitCode: 0, stdout: "", stderr: "" }; + if (command.includes("worktree add")) return { exitCode: 0, stdout: "Preparing worktree\n", stderr: "" }; + if (command.includes("rev-parse HEAD")) return { exitCode: 0, stdout: `${SUBJECT_COMMIT}\n`, stderr: "" }; + if (command.endsWith("status --short")) return { exitCode: 0, stdout: dirtyStatus, stderr: "" }; + if (command.endsWith("diff --stat")) return { exitCode: 0, stdout: " projects/01_baseline/Projects/MDK-ARM/atk_f103.uvprojx | 8 ++++----\n", stderr: "" }; + if (command.endsWith("diff --binary")) return { exitCode: 0, stdout: dirtyDiff, stderr: "" }; + return { exitCode: 0, stdout: "", stderr: "" }; +} diff --git a/tools/src/hwlab-caserun-lib.ts b/tools/src/hwlab-caserun-lib.ts index b5deacd0..60a2fe13 100644 --- a/tools/src/hwlab-caserun-lib.ts +++ b/tools/src/hwlab-caserun-lib.ts @@ -85,6 +85,17 @@ type PreparedSubjectRun = { worktreePath: string; workspacePath: string; setup: Record; + sourceRootBaseline?: SourceRootSnapshot; + sourceRootAfterPrepare?: SourceRootSnapshot; +}; + +type SourceRootSnapshot = { + label: string; + statusShort: string; + diffStat: string; + diffSha256: string; + diffBytes: number; + commands?: Record; }; type PreparedAgentTask = { @@ -132,6 +143,11 @@ type AgentDiffStage = { diffPatchPath: string; diffPatchSha256: string; sourceRootStatusShort: string; + sourceRootBaseline?: SourceRootSnapshot; + sourceRootAfterPrepare?: SourceRootSnapshot; + sourceRootAfterAgent?: SourceRootSnapshot; + sourceRootChangedAfterPrepare?: boolean; + sourceRootChangedAfterAgent?: boolean; requests: Record[]; }; @@ -977,7 +993,7 @@ async function collectAgentDiff(context: CaseContext, run: PreparedCaseRun) { const status = await requestSubjectCommand(context, run, ["-C", run.subject.worktreePath, "status", "--short"]); const diffStat = await requestSubjectCommand(context, run, ["-C", run.subject.worktreePath, "diff", "--stat"]); const diffPatch = await requestSubjectCommand(context, run, ["-C", run.subject.worktreePath, "diff", "--binary"]); - const sourceStatus = await requestSubjectCommand(context, run, ["status", "--short"]); + const sourceAfterAgent = await collectSourceRootSnapshot(context, run, "after-agent"); const statusShort = text(status.stdout); const patchText = text(diffPatch.stdout); const diffPatchPath = path.join(run.runDir, "agent-diff.patch"); @@ -987,18 +1003,49 @@ async function collectAgentDiff(context: CaseContext, run: PreparedCaseRun) { diffStat: text(diffStat.stdout), diffPatchPath, diffPatchSha256: sha256(patchText), - sourceRootStatusShort: text(sourceStatus.stdout), + sourceRootStatusShort: sourceAfterAgent.statusShort, + sourceRootBaseline: run.subject.sourceRootBaseline, + sourceRootAfterPrepare: run.subject.sourceRootAfterPrepare, + sourceRootAfterAgent: sourceAfterAgent, + sourceRootChangedAfterPrepare: sourceRootChanged(run.subject.sourceRootBaseline, run.subject.sourceRootAfterPrepare), + sourceRootChangedAfterAgent: sourceRootChanged(run.subject.sourceRootAfterPrepare ?? run.subject.sourceRootBaseline, sourceAfterAgent), requests: [ requestSummary("status", status), requestSummary("diff-stat", diffStat), requestSummary("diff-patch", diffPatch), - requestSummary("source-root-status", sourceStatus) + { label: "source-root-baseline", statusShort: run.subject.sourceRootBaseline?.statusShort ?? "", diffSha256: run.subject.sourceRootBaseline?.diffSha256 ?? "" }, + { label: "source-root-after-prepare", statusShort: run.subject.sourceRootAfterPrepare?.statusShort ?? "", diffSha256: run.subject.sourceRootAfterPrepare?.diffSha256 ?? "" }, + { label: "source-root-after-agent", statusShort: sourceAfterAgent.statusShort, diffSha256: sourceAfterAgent.diffSha256 } ] }; const nextRun = await updateRun(context, run, { agentDiff: diff }); return { run: nextRun, diff }; } +async function collectSourceRootSnapshot(context: CaseContext, run: PreparedCaseRun, label: string): Promise { + const status = await requestSubjectCommand(context, run, ["status", "--short"]); + const diffStat = await requestSubjectCommand(context, run, ["diff", "--stat"]); + const diffPatch = await requestSubjectCommand(context, run, ["diff", "--binary"]); + const diffText = text(diffPatch.stdout); + return { + label, + statusShort: text(status.stdout), + diffStat: text(diffStat.stdout), + diffSha256: sha256(diffText), + diffBytes: Buffer.byteLength(diffText), + commands: { + status: requestSummary("source-root-status", status), + diffStat: requestSummary("source-root-diff-stat", diffStat), + diffPatch: requestSummary("source-root-diff-patch", diffPatch) + } + }; +} + +function sourceRootChanged(before?: SourceRootSnapshot, after?: SourceRootSnapshot) { + if (!before || !after) return undefined; + return before.statusShort !== after.statusShort || before.diffSha256 !== after.diffSha256; +} + async function requestSubjectCommand(context: CaseContext, run: PreparedCaseRun, argv: string[]) { const document = await readHwpodSpec(run.specPath); const hwpodId = text(document.metadata.name) || text(document.metadata.uid) || run.caseId; @@ -1304,7 +1351,19 @@ function traceSummary(trace: AgentTraceStage) { } function diffSummary(diff: AgentDiffStage) { - return { statusShort: diff.statusShort, diffStat: diff.diffStat, diffPatchPath: diff.diffPatchPath, diffPatchSha256: diff.diffPatchSha256, sourceRootStatusShort: diff.sourceRootStatusShort, requests: diff.requests }; + return clean({ + statusShort: diff.statusShort, + diffStat: diff.diffStat, + diffPatchPath: diff.diffPatchPath, + diffPatchSha256: diff.diffPatchSha256, + sourceRootStatusShort: diff.sourceRootStatusShort, + sourceRootBaseline: diff.sourceRootBaseline, + sourceRootAfterPrepare: diff.sourceRootAfterPrepare, + sourceRootAfterAgent: diff.sourceRootAfterAgent, + sourceRootChangedAfterPrepare: diff.sourceRootChangedAfterPrepare, + sourceRootChangedAfterAgent: diff.sourceRootChangedAfterAgent, + requests: diff.requests + }); } function stringArray(value: unknown) { @@ -1323,8 +1382,30 @@ async function prepareSubjectWorktree(context: CaseContext, input: { caseId: str const subject = subjectFromDefinition(input.definition); const worktreePath = subjectWorktreePath(subject.repoLocalPath, input.runId); const relativeWorktreePath = subjectRelativeWorktreePath(subject.repoLocalPath, worktreePath); + const snapshotRun = caseRunForSubjectSnapshot(input, subject, worktreePath); + const sourceRootBaseline = await collectSourceRootSnapshot(context, snapshotRun, "before-prepare"); const setup = await requestSubjectWorktree(context, input, subject, worktreePath, relativeWorktreePath); - return { ...subject, worktreePath, workspacePath: worktreePath, setup }; + const sourceRootAfterPrepare = await collectSourceRootSnapshot(context, snapshotRun, "after-prepare"); + return { ...subject, worktreePath, workspacePath: worktreePath, setup, sourceRootBaseline, sourceRootAfterPrepare }; +} + +function caseRunForSubjectSnapshot(input: { caseId: string; runId: string; runDir: string; apiUrl: string; definition: Record; sourceSpecPath: string }, subject: { repoLocalPath: string; commitId: string; subdir: string }, worktreePath: string): PreparedCaseRun { + return { + caseId: input.caseId, + runId: input.runId, + runDir: input.runDir, + caseRepo: "", + caseDir: "", + caseFile: "", + sourceSpecPath: input.sourceSpecPath, + specPath: input.sourceSpecPath, + apiUrl: input.apiUrl, + compileOnly: true, + createdAt: "", + updatedAt: "", + definition: input.definition, + subject: { ...subject, worktreePath, workspacePath: worktreePath, setup: {} } + }; } function subjectFromDefinition(definition: Record) { @@ -2086,7 +2167,18 @@ function caseRunArtifactManifest(context: CaseContext, run: PreparedCaseRun, evi finalResponse: readableAgent.finalResponse }), prompt: clean({ path: "agent-prompt.md", sha256: evidence.agent?.promptSha256 ?? run.agent?.promptSha256 }), - diff: clean({ path: "agent-diff.patch", sha256: evidence.agentDiff?.diffPatchSha256 ?? run.agentDiff?.diffPatchSha256, statusShort: evidence.agentDiff?.statusShort ?? run.agentDiff?.statusShort, diffStat: evidence.agentDiff?.diffStat ?? run.agentDiff?.diffStat }), + diff: clean({ + path: "agent-diff.patch", + sha256: evidence.agentDiff?.diffPatchSha256 ?? run.agentDiff?.diffPatchSha256, + statusShort: evidence.agentDiff?.statusShort ?? run.agentDiff?.statusShort, + diffStat: evidence.agentDiff?.diffStat ?? run.agentDiff?.diffStat, + sourceRootStatusShort: evidence.agentDiff?.sourceRootStatusShort ?? run.agentDiff?.sourceRootStatusShort, + sourceRootChangedAfterPrepare: evidence.agentDiff?.sourceRootChangedAfterPrepare ?? run.agentDiff?.sourceRootChangedAfterPrepare, + sourceRootChangedAfterAgent: evidence.agentDiff?.sourceRootChangedAfterAgent ?? run.agentDiff?.sourceRootChangedAfterAgent, + sourceRootBaseline: evidence.agentDiff?.sourceRootBaseline ?? run.agentDiff?.sourceRootBaseline, + sourceRootAfterPrepare: evidence.agentDiff?.sourceRootAfterPrepare ?? run.agentDiff?.sourceRootAfterPrepare, + sourceRootAfterAgent: evidence.agentDiff?.sourceRootAfterAgent ?? run.agentDiff?.sourceRootAfterAgent + }), agentStage: agentStageSummary(evidence), keilJob: evidence.keilJob ?? null, artifacts: evidence.artifacts ?? [],