mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-27 21:33:41 +02:00
Board and Swimlane crashed with "Maximum update depth exceeded" on first paint of the Issues route. During cold load the member/agent/squad directory queries are unresolved, so useActorName's `= []` defaults allocated a fresh array every render and its `getActorName` memo changed identity each render. BoardView's `groups` (and SwimLaneView's `laneGroups`) list `getActorName` as a dep, so they churned every render and re-fired the column-resync effect without end; react-virtuoso escalated the loop into the reported crash. This predates MUL-4797 — it was exposed when board/swimlane columns were virtualized with a real <Virtuoso>. - Share stable empty references for the loading directory snapshot so getActorName is referentially stable across cold-load renders (root cause). - Equality-guard the shared column setter in useDragSettle so a content-equal rebuild returns the previous reference and cannot spin the resync effect (defense-in-depth). Regression coverage: useActorName stability during cold load; the column-setter equality guard; and Board (40 cards) + Swimlane rendered with the REAL react-virtuoso under pending directories, which reproduce "Maximum update depth exceeded" without the fix and pass with it. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai>
54 lines
2.1 KiB
TypeScript
54 lines
2.1 KiB
TypeScript
/**
|
|
* @vitest-environment jsdom
|
|
*/
|
|
import { describe, it, expect } from "vitest";
|
|
import { useEffect } from "react";
|
|
import { render, act } from "@testing-library/react";
|
|
import { useDragSettle } from "./use-drag-settle";
|
|
|
|
describe("useDragSettle", () => {
|
|
// MUL-4985 defense-in-depth: the board/list/swimlane resync effect calls
|
|
// `setColumns(buildColumns(...))` whenever its `groups` input changes
|
|
// identity. When `groups` churns a fresh-but-content-equal value every render
|
|
// (the cold-load failure mode), an unguarded setter allocated a new column
|
|
// object each render and spun into "Maximum update depth exceeded". The
|
|
// equality guard returns the previous reference for a content-equal rebuild,
|
|
// so React bails and the loop cannot start.
|
|
it("does not loop when a resync effect rebuilds a content-equal column map every render", () => {
|
|
let renders = 0;
|
|
function Harness() {
|
|
renders++;
|
|
const { columns, setColumns } = useDragSettle(() => ({ todo: ["a", "b"] }));
|
|
// A fresh object of identical content on every render — what an unstable
|
|
// `groups` fed through buildColumns produces.
|
|
useEffect(() => {
|
|
setColumns({ todo: ["a", "b"] });
|
|
});
|
|
return <div>{Object.keys(columns).join(",")}</div>;
|
|
}
|
|
|
|
expect(() => render(<Harness />)).not.toThrow();
|
|
// A guarded setter settles in one or two renders; an unguarded loop is in
|
|
// the hundreds before React throws. Bound generously to stay robust.
|
|
expect(renders).toBeLessThan(10);
|
|
});
|
|
|
|
it("still applies a content-changed column update", () => {
|
|
function Harness({ next }: { next: Record<string, string[]> }) {
|
|
const { columns, setColumns } = useDragSettle(() => ({ todo: ["a"] }));
|
|
useEffect(() => {
|
|
setColumns(next);
|
|
}, [next, setColumns]);
|
|
return <div data-testid="cols">{(columns.todo ?? []).join(",")}</div>;
|
|
}
|
|
|
|
const { getByTestId, rerender } = render(<Harness next={{ todo: ["a"] }} />);
|
|
expect(getByTestId("cols").textContent).toBe("a");
|
|
|
|
act(() => {
|
|
rerender(<Harness next={{ todo: ["a", "b"] }} />);
|
|
});
|
|
expect(getByTestId("cols").textContent).toBe("a,b");
|
|
});
|
|
});
|