feat(FN-4527): complete Step 1 — harden done-task diff stats fallback

Fusion-Task-Id: FN-4527
Fusion-Task-Lineage: de75c453-dc86-43d2-9cc5-50b20ee7014d
This commit is contained in:
Fusion
2026-05-14 17:43:52 -07:00
committed by gsxdsm
parent d2df78dfb1
commit 6df38aca8b
2 changed files with 110 additions and 12 deletions

View File

@@ -353,6 +353,96 @@ describe("FN-4524 done-task diff stats", () => {
}
});
it("uses live shortstat for done tasks even when mergeDetails stats are stale", async () => {
const rootDir = mkdtempSync(join(tmpdir(), "fn-4527-done-stale-merge-details-"));
try {
git(rootDir, "init", "-b", "main");
git(rootDir, "config", "user.email", "fusion@example.com");
git(rootDir, "config", "user.name", "Fusion");
commitFile(rootDir, "base.ts", "export const base = 1;\n", "base");
git(rootDir, "checkout", "-b", "task-branch");
commitFile(rootDir, "one.ts", "export const one = 1;\n", "task one");
commitFile(rootDir, "two.ts", "export const two = 2;\n", "task two");
git(rootDir, "checkout", "main");
git(rootDir, "merge", "--squash", "task-branch");
git(rootDir, "commit", "-m", "squash merge task");
const squashCommit = git(rootDir, "rev-parse", "HEAD");
const expected = shortstatForShow(rootDir, squashCommit);
expect(expected).toEqual({ filesChanged: 2, additions: 2, deletions: 0 });
const store = new RealGitStore(rootDir);
store.addTask({
id: "FN-4524",
title: "stale mergeDetails test",
description: "stale mergeDetails test",
column: "done",
dependencies: [],
steps: [],
currentStep: 0,
log: [],
createdAt: "2026-05-14T00:00:00.000Z",
updatedAt: "2026-05-14T00:00:00.000Z",
columnMovedAt: "2026-05-14T00:00:00.000Z",
baseBranch: "main",
mergeDetails: {
commitSha: squashCommit,
filesChanged: 108,
insertions: 999,
deletions: 999,
},
} as Task);
const response = await getDoneDiff(store);
expect(response.status).toBe(200);
expect(response.body.stats).toEqual(expected);
expect(response.body.stats).not.toEqual({ filesChanged: 108, additions: 999, deletions: 999 });
} finally {
rmSync(rootDir, { recursive: true, force: true });
}
});
it("returns zero stats when merge SHA is unresolvable even if mergeDetails has stored counts", async () => {
const rootDir = mkdtempSync(join(tmpdir(), "fn-4527-done-no-merge-sha-"));
try {
git(rootDir, "init", "-b", "main");
git(rootDir, "config", "user.email", "fusion@example.com");
git(rootDir, "config", "user.name", "Fusion");
commitFile(rootDir, "base.ts", "export const base = 1;\n", "base");
const store = new RealGitStore(rootDir);
store.addTask({
id: "FN-4524",
title: "missing sha test",
description: "missing sha test",
column: "done",
dependencies: [],
steps: [],
currentStep: 0,
log: [],
createdAt: "2026-05-14T00:00:00.000Z",
updatedAt: "2026-05-14T00:00:00.000Z",
columnMovedAt: "2026-05-14T00:00:00.000Z",
baseBranch: "main",
mergeDetails: {
filesChanged: 108,
insertions: 999,
deletions: 999,
},
} as Task);
const response = await getDoneDiff(store);
expect(response.status).toBe(200);
expect(response.body.stats).toEqual({ filesChanged: 0, additions: 0, deletions: 0 });
} finally {
rmSync(rootDir, { recursive: true, force: true });
}
});
it("matches shortstat when hunks include ++/-- content lines", async () => {
const rootDir = mkdtempSync(join(tmpdir(), "fn-4524-done-plusminus-"));

View File

@@ -197,6 +197,17 @@ type AggregatedDoneTaskFile = {
patch: string;
};
function parseGitShortstat(output: string): { filesChanged: number; additions: number; deletions: number } {
const filesMatch = output.match(/(\d+) files? changed/);
const additionsMatch = output.match(/(\d+) insertions?\(\+\)/);
const deletionsMatch = output.match(/(\d+) deletions?\(-\)/);
return {
filesChanged: filesMatch ? Number(filesMatch[1]) : 0,
additions: additionsMatch ? Number(additionsMatch[1]) : 0,
deletions: deletionsMatch ? Number(deletionsMatch[1]) : 0,
};
}
function statusPriority(status: DoneTaskFileStatus): number {
switch (status) {
case "added":
@@ -641,13 +652,14 @@ export function registerSessionDiffRoutes(router: Router, deps: SessionDiffRoute
}
if (!resolvedMergeSha) {
const md = task.mergeDetails;
// FN-4527: mergeDetails summary stats can be stale after post-merge
// rebase-and-push (FN-4526). Never echo stored values from /diff.
res.json({
files: [],
stats: {
filesChanged: md?.filesChanged ?? 0,
additions: md?.insertions ?? 0,
deletions: md?.deletions ?? 0,
filesChanged: 0,
additions: 0,
deletions: 0,
},
});
return;
@@ -697,17 +709,13 @@ export function registerSessionDiffRoutes(router: Router, deps: SessionDiffRoute
return;
}
const patch = await runGitCommand(["diff", diffSpec.range], rootDir, 10000).catch(() => "");
const filesChanged = (await runGitCommand(["diff", "--name-only", diffSpec.range], rootDir, 10000)
.then((output) => output.split("\n").filter(Boolean).length)
.catch(() => 0));
const shortstat = await runGitCommand(["show", "--shortstat", "--format=", resolvedMergeSha], rootDir, 10000)
.then((output) => parseGitShortstat(output))
.catch(() => ({ filesChanged: 0, additions: 0, deletions: 0 }));
res.json({
files: [],
stats: {
filesChanged,
...countPatchLines(patch),
},
stats: shortstat,
});
return;
}