From cf6062c524a5b594ca3c374f3a1d20fd76be2fe4 Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Fri, 31 Jul 2026 00:02:50 -0700 Subject: [PATCH] fix(dashboard): finished cards on a renamed board never refreshed their diff stats (#3001) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Last live site from the stale-lane-dependency sweep recorded in #2998. **Nine persistent candidates; this and #2996 were real.** The other seven are covered transitively or by a dependency that already carries the flags — each checked by hand rather than filed, which is the whole point of that doc. ## The defect `mergeSignature` is the key `useTaskDiffStats` uses to notice that a merge changed what a finished card should display. It early-returns `undefined` unless `isCompleteColumn`, which derives from the `taskColumnFlags` **prop** — and its dependency list was three `task.*` fields, none of which is that prop or carries it. The flags arrive after first paint, so: 1. first computation runs with flags `undefined`; 2. the role helper falls back to the legacy id — `isCompleteColumnRole(undefined, "shipped")` is **false**; 3. the key is `undefined`; 4. for a card **already merged when the board loaded** — the common case for anything sitting in a completion lane — neither `mergeDetails` field changes afterwards either; 5. nothing recomputes, and the hook never learns a merge landed. A legacy board hides it: `column === "done"` answers true on the very first paint. ## Measured | check | result | |---|---| | test written first | red for the right reason — control and negative passed, only the arrival case failed (`expected undefined to be defined`) | | after the fix | 3 passed | | dropping the dependency again | that same case fails | | `TaskCard.test` + new suite | **391 tests green** | | gates | census + FNXC green; lint and `tsc` clean | The observable is the options object handed to `useTaskDiffStats`, so the assertion is on the value this component is responsible for producing rather than on what the hook does with it. ## The negative case Recomputing must not hand a signature to cards that aren't finished. An in-flight card has no merge to key on, and inventing one would have the diff-stats hook treat unfinished work as landed. ## The sweep is now closed For anyone picking this up later: the four "bounded" sites from #2998's triage remain unexamined **by design** — their dependency lists all contain a fast-refreshing value (`allTasks`, a live clock), so any wrong answer there survives only until the next update. That's a judgement about priority, not a claim that they're correct. Co-authored-by: Claude Opus 5 (1M context) --- .../taskcard-merge-signature-renamed-lanes.md | 7 ++ .../dashboard/app/components/TaskCard.tsx | 14 ++- ...ard.merge-signature-renamed-lanes.test.tsx | 111 ++++++++++++++++++ 3 files changed, 131 insertions(+), 1 deletion(-) create mode 100644 .changeset/taskcard-merge-signature-renamed-lanes.md create mode 100644 packages/dashboard/app/components/__tests__/TaskCard.merge-signature-renamed-lanes.test.tsx diff --git a/.changeset/taskcard-merge-signature-renamed-lanes.md b/.changeset/taskcard-merge-signature-renamed-lanes.md new file mode 100644 index 0000000000..02efd51b25 --- /dev/null +++ b/.changeset/taskcard-merge-signature-renamed-lanes.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": patch +--- + +summary: Finished cards on renamed boards now refresh their diff stats after a merge. +category: fix +dev: `mergeSignature`'s dependency list omitted `isCompleteColumn`, so the key stayed undefined when column traits arrived after first paint. diff --git a/packages/dashboard/app/components/TaskCard.tsx b/packages/dashboard/app/components/TaskCard.tsx index 141731ae6e..e96da7faf8 100644 --- a/packages/dashboard/app/components/TaskCard.tsx +++ b/packages/dashboard/app/components/TaskCard.tsx @@ -1890,7 +1890,19 @@ function TaskCardComponent({ const landedFilesCount = task.mergeDetails?.landedFiles?.length ?? ""; const filesChanged = task.mergeDetails?.filesChanged ?? ""; return `${landedFilesCount}:${filesChanged}`; - }, [task.column, task.mergeDetails?.landedFiles?.length, task.mergeDetails?.filesChanged]); + /* + FNXC:WorkflowResolvedColumns 2026-07-30-23:25: + `isCompleteColumn` BELONGS IN THIS LIST — it derives from the async `taskColumnFlags` prop. + + Without it the signature is computed once, during the pre-load render where the flags are undefined + and the role helper falls back to the legacy id. On a renamed board that answers false, so the key + is `undefined`; and for a card already merged when the board loaded, neither `mergeDetails` field + changes afterwards either, so nothing ever recomputes and `useTaskDiffStats` loses the signal that + a merge changed what the card should show. + + A legacy board hid it: `column === "done"` is already true on the first paint. + */ + }, [task.column, task.mergeDetails?.landedFiles?.length, task.mergeDetails?.filesChanged, isCompleteColumn]); // Viewport-gated diff stats fetching - only fetch when card is visible const { stats: diffStats, loading: diffLoading } = useTaskDiffStats( diff --git a/packages/dashboard/app/components/__tests__/TaskCard.merge-signature-renamed-lanes.test.tsx b/packages/dashboard/app/components/__tests__/TaskCard.merge-signature-renamed-lanes.test.tsx new file mode 100644 index 0000000000..a90337d556 --- /dev/null +++ b/packages/dashboard/app/components/__tests__/TaskCard.merge-signature-renamed-lanes.test.tsx @@ -0,0 +1,111 @@ +/* +FNXC:WorkflowResolvedColumns 2026-07-30-23:20: +THE CARD'S DIFF-STATS REFRESH KEY STAYED UNDEFINED ON A RENAMED BOARD. + +`mergeSignature` is the value `useTaskDiffStats` uses to notice that a merge changed what a finished +card should display. It early-returns `undefined` unless `isCompleteColumn`, which derives from the +`taskColumnFlags` PROP — and its dependency list was three `task.*` fields, none of which is that +prop or anything carrying it. + +The flags arrive after first paint (the board resolves workflow traits asynchronously). The first +computation therefore runs with them undefined, the role helper falls back to the legacy id, and on a +renamed board `isCompleteColumnRole(undefined, "shipped")` is false — so the signature is `undefined`. +When the flags arrive, `task.column` has not changed and, for a card already merged when the board +loaded, neither have the two `mergeDetails` fields. Nothing recomputes and the key stays absent. + +WHY THE DEFAULT BOARD HID IT: `column === "done"` answers true on the first paint, so the memo's +initial value is already right. + +Last live site from the sweep recorded in the learnings doc (nine persistent candidates, seven +covered transitively or by a dependency that already carries the flags). +*/ + +import { describe, it, expect, vi } from "vitest"; +import { render } from "@testing-library/react"; +import type { Task } from "@fusion/core"; +import { TaskCard } from "../TaskCard"; + +/* Capture the options `useTaskDiffStats` is called with — `mergeSignature` is the observable. */ +const seen: (string | undefined)[] = []; +vi.mock("../../hooks/useTaskDiffStats", () => ({ + useTaskDiffStats: ( + _id: string, + _column: string, + _sha: string | undefined, + _projectId: string | undefined, + options: { mergeSignature?: string }, + ) => { + seen.push(options?.mergeSignature); + return { stats: undefined, loading: false }; + }, +})); + +const noop = () => {}; + +/** A card that merged BEFORE the board loaded: its mergeDetails never change afterwards. */ +function mergedTaskIn(column: string): Task { + return { + id: "KB-1", + title: "a merged card", + description: "t", + column, + createdAt: "2026-06-01T00:00:00.000Z", + updatedAt: "2026-06-01T00:00:00.000Z", + steps: [], + mergeDetails: { commitSha: "abc1234", filesChanged: 3, landedFiles: ["a.ts", "b.ts", "c.ts"] }, + } as unknown as Task; +} + +const latest = () => seen[seen.length - 1]; + +describe("the diff-stats refresh key when column traits arrive after first paint", () => { + /* Control: on a legacy board the key is present from the first paint, with no flags at all. */ + it("default vocabulary: a card in `done` gets a merge signature", () => { + seen.length = 0; + render(); + + expect(latest()).toBeDefined(); + }); + + /* The defect: the renamed complete lane never produced a key, so a merge could not refresh stats. */ + it("renamed vocabulary: the key appears once the complete trait arrives", () => { + seen.length = 0; + const { rerender } = render( + , + ); + expect(latest()).toBeUndefined(); + + rerender( + , + ); + + expect(latest()).toBeDefined(); + }); + + /* + The paired negative: recomputing must not hand a signature to cards that are not finished. An + in-flight card has no merge to key on, and inventing one would make the diff-stats hook treat + unfinished work as landed. + */ + it("renamed vocabulary: a card in the WIP lane still has no signature", () => { + seen.length = 0; + const { rerender } = render( + , + ); + rerender( + , + ); + + expect(latest()).toBeUndefined(); + }); +});