feat(FN-4527): complete Step 2 — enforce done-task diffStats precedence in TaskCard

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

View File

@@ -979,7 +979,7 @@ function TaskCardComponent({
);
// Viewport-gated diff stats fetching - only fetch when card is visible
const { stats: diffStats } = useTaskDiffStats(
const { stats: diffStats, loading: diffLoading } = useTaskDiffStats(
task.id,
task.column,
task.mergeDetails?.commitSha,
@@ -1357,11 +1357,17 @@ function TaskCardComponent({
}
if (task.column === "done") {
// Prefer diff stats from the same endpoint the modal uses so the
// count is always consistent with the Changes tab.
const diffCount = diffStats?.filesChanged;
const mergedCount = task.mergeDetails?.filesChanged;
const displayCount = diffCount ?? mergedCount;
// Per FN-4527: /api/tasks/:id/diff is authoritative for done-task file
// counts. mergeDetails.filesChanged can be stale after rebase-and-push
// (FN-4526), so only use it as a transient loading placeholder.
let displayCount: number | undefined;
if (diffStats) {
displayCount = diffStats.filesChanged;
} else if (diffLoading) {
displayCount = task.mergeDetails?.filesChanged ?? undefined;
} else {
displayCount = undefined;
}
if (displayCount != null && displayCount > 0) {
return (
<button

View File

@@ -1938,24 +1938,54 @@ describe("TaskCard", () => {
expect(css).toMatch(/\.card-github-badge\s*\{[^}]*border:\s*1px\s+solid\s+transparent;[^}]*\}/);
});
it("prefers done-task /diff filesChanged over mergeDetails.filesChanged", () => {
useTaskDiffStatsMock.mockReturnValue({
stats: { filesChanged: 4, additions: 10, deletions: 2 },
loading: false,
});
it.each([
{
name: "uses live diff stats over stale mergeDetails",
diff: { stats: { filesChanged: 2, additions: 4, deletions: 1 }, loading: false },
mergeDetails: { filesChanged: 108 },
expectedLabel: "2 files changed",
},
{
name: "uses mergeDetails as transient placeholder while loading",
diff: { stats: null, loading: true },
mergeDetails: { filesChanged: 108 },
expectedLabel: "108 files changed",
},
{
name: "hides badge when fetch resolved null",
diff: { stats: null, loading: false },
mergeDetails: { filesChanged: 108 },
expectedLabel: null,
},
{
name: "hides badge when live diff resolves zero",
diff: { stats: { filesChanged: 0, additions: 0, deletions: 0 }, loading: false },
mergeDetails: { filesChanged: 108 },
expectedLabel: null,
},
{
name: "uses singular grammar for one live file",
diff: { stats: { filesChanged: 1, additions: 1, deletions: 0 }, loading: false },
mergeDetails: undefined,
expectedLabel: "1 file changed",
},
])("FN-4527 done-task files changed contract: $name", ({ diff, mergeDetails, expectedLabel }) => {
useTaskDiffStatsMock.mockReturnValue(diff);
render(
<TaskCard
task={makeTask({
column: "done",
mergeDetails: {
commitSha: "abc123",
filesChanged: 1,
insertions: 10,
deletions: 2,
mergedAt: "2026-04-25T15:00:00.000Z",
mergeConfirmed: true,
},
mergeDetails: mergeDetails
? {
commitSha: "abc123",
insertions: 10,
deletions: 2,
mergedAt: "2026-04-25T15:00:00.000Z",
mergeConfirmed: true,
...mergeDetails,
}
: undefined,
})}
onOpenDetail={noop}
addToast={noop}
@@ -1963,11 +1993,21 @@ describe("TaskCard", () => {
/>,
);
expect(screen.getByRole("button", { name: "4 files changed" })).toBeDefined();
expect(screen.queryByRole("button", { name: "1 file changed" })).toBeNull();
if (expectedLabel) {
expect(screen.getByRole("button", { name: expectedLabel })).toBeDefined();
return;
}
const filesChangedButton = document.querySelector(".card-session-files");
expect(filesChangedButton).toBeNull();
});
it("renders files-changed metadata and timer chip in footer row", () => {
useTaskDiffStatsMock.mockReturnValue({
stats: { filesChanged: 4, additions: 10, deletions: 2 },
loading: false,
});
const { container } = render(
<TaskCard
task={makeTask({