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:
@@ -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
|
||||
|
||||
@@ -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({
|
||||
|
||||
Reference in New Issue
Block a user