feat(engine): post-session branch attribution audit
Contamination on fusion/<id> branches (FN-5233 was the recent example: two untrailered feat(FN-5353): commits sitting on fusion/fn-5233) used to be invisible until merge time, days after it happened. The executor already runs assertCleanBranchAtBase at worktree acquisition and reclaim — the gap was the active session window itself. Add reportBranchAttribution(repoDir, branch, baseSha, taskId) which walks base..branch and bins every commit into ownTrailed (healthy), ownUntrailed (subject tag but commit-msg hook didn't fire), foreign (different FN-id), or unattributed (no subject pattern, no trailer — typically a hand-merge or plumbing commit). Wire it into the executor right after captureModifiedFiles in the post-session path: when any anomaly bucket is non-empty, emit a structured branch:attribution- anomaly audit event and a task log entry. The audit itself is wrapped in a try/catch so a probe failure never destabilizes a completing session. New branch:attribution-anomaly and branch:auto-reattach- authoritative GitMutationType variants accept the structured metadata (the latter for the handoff re-attach added earlier this session). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -48,6 +48,7 @@ import {
|
||||
assertCleanBranchAtBase,
|
||||
inspectBranchConflict,
|
||||
listUniqueBranchCommits,
|
||||
reportBranchAttribution,
|
||||
} from "../branch-conflicts.js";
|
||||
|
||||
const mockedExecSync = vi.mocked(execSync);
|
||||
@@ -465,5 +466,65 @@ describe("branch-conflicts", () => {
|
||||
await expect(assertion).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
describe("reportBranchAttribution", () => {
|
||||
const RS = "\x1e";
|
||||
const FS = "\x1f";
|
||||
|
||||
function setupLog(records: { sha: string; subject: string; body: string }[]) {
|
||||
const log = records.map((r) => `${r.sha}${FS}${r.subject}${FS}${r.body}${RS}`).join("");
|
||||
mockedExecSync.mockImplementation((cmd: string | string[]) => {
|
||||
const command = typeof cmd === "string" ? cmd : cmd[0];
|
||||
if (command.includes("git log --format=%H%x1f%s%x1f%b%x1e")) {
|
||||
return Buffer.from(log);
|
||||
}
|
||||
throw new Error(`Unexpected command: ${command}`);
|
||||
});
|
||||
}
|
||||
|
||||
it("counts own-trailed commits as healthy", async () => {
|
||||
setupLog([
|
||||
{ sha: "aaa", subject: "feat(FN-1): add x", body: "Fusion-Task-Id: FN-1\n" },
|
||||
{ sha: "bbb", subject: "fix(FN-1): tweak", body: "Fusion-Task-Id: FN-1\n" },
|
||||
]);
|
||||
const r = await reportBranchAttribution("/tmp/repo", "fusion/fn-1", "main", "FN-1");
|
||||
expect(r.ownTrailed).toBe(2);
|
||||
expect(r.ownUntrailed).toEqual([]);
|
||||
expect(r.foreign).toEqual([]);
|
||||
expect(r.unattributed).toEqual([]);
|
||||
});
|
||||
|
||||
it("flags FN-5233-class foreign commits", async () => {
|
||||
setupLog([
|
||||
{ sha: "fff", subject: "feat(FN-5353): wire something", body: "" },
|
||||
{ sha: "ggg", subject: "feat(FN-1): legit", body: "Fusion-Task-Id: FN-1\n" },
|
||||
]);
|
||||
const r = await reportBranchAttribution("/tmp/repo", "fusion/fn-1", "main", "FN-1");
|
||||
expect(r.foreign).toEqual([{ sha: "fff", subject: "feat(FN-5353): wire something", foreignTaskId: "FN-5353" }]);
|
||||
expect(r.ownTrailed).toBe(1);
|
||||
});
|
||||
|
||||
it("flags own-but-untrailed commits (hook didn't fire)", async () => {
|
||||
setupLog([
|
||||
{ sha: "ccc", subject: "feat(FN-1): no trailer", body: "" },
|
||||
]);
|
||||
const r = await reportBranchAttribution("/tmp/repo", "fusion/fn-1", "main", "FN-1");
|
||||
expect(r.ownUntrailed).toEqual([{ sha: "ccc", subject: "feat(FN-1): no trailer" }]);
|
||||
expect(r.ownTrailed).toBe(0);
|
||||
});
|
||||
|
||||
it("flags unattributed commits (no subject pattern, no trailer)", async () => {
|
||||
setupLog([
|
||||
{ sha: "ddd", subject: "hand-merge", body: "" },
|
||||
]);
|
||||
const r = await reportBranchAttribution("/tmp/repo", "fusion/fn-1", "main", "FN-1");
|
||||
expect(r.unattributed).toEqual([{ sha: "ddd", subject: "hand-merge" }]);
|
||||
});
|
||||
|
||||
it("returns empty report when range is empty", async () => {
|
||||
setupLog([]);
|
||||
const r = await reportBranchAttribution("/tmp/repo", "fusion/fn-1", "main", "FN-1");
|
||||
expect(r).toEqual({ ownTrailed: 0, ownUntrailed: [], foreign: [], unattributed: [] });
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user