fix: record caserun source-root baseline
This commit is contained in:
@@ -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+<PackID>Keil.STM32F1xx_DFP.2.4.1</PackID>\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: "" };
|
||||
}
|
||||
@@ -85,6 +85,17 @@ type PreparedSubjectRun = {
|
||||
worktreePath: string;
|
||||
workspacePath: string;
|
||||
setup: Record<string, unknown>;
|
||||
sourceRootBaseline?: SourceRootSnapshot;
|
||||
sourceRootAfterPrepare?: SourceRootSnapshot;
|
||||
};
|
||||
|
||||
type SourceRootSnapshot = {
|
||||
label: string;
|
||||
statusShort: string;
|
||||
diffStat: string;
|
||||
diffSha256: string;
|
||||
diffBytes: number;
|
||||
commands?: Record<string, unknown>;
|
||||
};
|
||||
|
||||
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<string, unknown>[];
|
||||
};
|
||||
|
||||
@@ -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<SourceRootSnapshot> {
|
||||
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<string, unknown>; 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<string, unknown>) {
|
||||
@@ -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 ?? [],
|
||||
|
||||
Reference in New Issue
Block a user