/**
* @vitest-environment jsdom
*/
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { renderHook } from "@testing-library/react";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import type { ReactNode } from "react";
import { setApiInstance } from "../api";
import type { ApiClient } from "../api/client";
import { workspaceKeys } from "./queries";
import { useActorName } from "./hooks";
// useActorName reads the current workspace from the core WorkspaceId provider;
// the directory-name resolution under test does not depend on the real id.
vi.mock("../hooks", () => ({
useWorkspaceId: () => "ws-1",
}));
function createWrapper(qc: QueryClient) {
return function Wrapper({ children }: { children: ReactNode }) {
return {children};
};
}
describe("useActorName", () => {
let qc: QueryClient;
beforeEach(() => {
qc = new QueryClient({ defaultOptions: { queries: { retry: false } } });
});
afterEach(() => {
qc.clear();
vi.restoreAllMocks();
});
// MUL-4985 regression: while the member/agent/squad directory queries are
// still loading, `data` is undefined. A `= []` default allocated a fresh
// array every render, so `getActorName` (memoized on those arrays) changed
// identity on every render. Consumers that list `getActorName` in their own
// memo deps (BoardView's `groups`, SwimLaneView's `laneGroups`) then churned
// a new value each render and spun the column-resync effect without end —
// an infinite re-render that react-virtuoso escalated into "Maximum update
// depth exceeded" on the Issues route. The fix shares one stable empty
// reference for the loading snapshot, so `getActorName` must be stable
// across re-renders while the directories are unresolved.
it("returns a referentially stable getActorName across renders during cold load", () => {
// Directory endpoints never resolve → the queries stay pending, so the
// hook renders repeatedly with undefined directory data (the cold-load
// state that used to loop).
const pending = () => new Promise(() => {});
setApiInstance({
listMembers: pending,
listAgents: pending,
listSquads: pending,
} as unknown as ApiClient);
const { result, rerender } = renderHook(() => useActorName(), {
wrapper: createWrapper(qc),
});
const first = result.current.getActorName;
rerender();
const second = result.current.getActorName;
rerender();
const third = result.current.getActorName;
expect(second).toBe(first);
expect(third).toBe(first);
// A stable resolver over an empty directory still resolves gracefully.
expect(first("member", "user-1")).toBe("Unknown");
});
it("resolves names once the directories are loaded", () => {
// Seed the caches directly so the hook reads resolved directories on its
// first render — this guards that stabilizing the loading default did not
// break name resolution when data IS present.
const members = [{ user_id: "user-1", name: "Ada", avatar_url: null }];
const agents = [{ id: "agent-1", name: "Walt", avatar_url: null }];
const squads = [{ id: "squad-1", name: "Core", avatar_url: null }];
setApiInstance({
listMembers: () => Promise.resolve(members),
listAgents: () => Promise.resolve(agents),
listSquads: () => Promise.resolve(squads),
} as unknown as ApiClient);
qc.setQueryData(workspaceKeys.members("ws-1"), members);
qc.setQueryData(workspaceKeys.agents("ws-1"), agents);
qc.setQueryData(workspaceKeys.squads("ws-1"), squads);
const { result } = renderHook(() => useActorName(), {
wrapper: createWrapper(qc),
});
expect(result.current.getActorName("member", "user-1")).toBe("Ada");
expect(result.current.getActorName("agent", "agent-1")).toBe("Walt");
expect(result.current.getActorName("squad", "squad-1")).toBe("Core");
});
});