Files
kandev/apps/web/hooks/domains/session/use-cumulative-diff.test.ts
T
José Almeida 2781556ead perf: tame browser-grinding cumulative diffs during large rebases (#1430)
* perf(backend): cap cumulative diff bytes and file count during large rebases

* perf(web): coalesce cumulative-diff refetches and skip redundant status compares

* feat(web): warn when the changes panel hides files from a capped diff

* fix(backend): enforce cumulative diff total byte budget strictly

* fix(web): keep coalesced refetch timer shared across envKey subscribers

* fix(web): drop unsound git-status SHA fast-path that staled live diffs

* fix(web): cancel coalesced timer on mid-flight drain to avoid duplicate refetch

* refactor(web): move changes-panel banner and helper to a sibling file

* docs(backend): clarify cumulative diff truncated-files-count semantics

* fix(web): tighten useSelectedFileKey mode param to the all|file union
2026-06-18 17:28:21 +01:00

149 lines
4.6 KiB
TypeScript

import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { cleanup, renderHook, act } from "@testing-library/react";
const mockRequest = vi.fn();
vi.mock("@/lib/ws/connection", () => ({
getWebSocketClient: () => ({ request: mockRequest }),
}));
let storeState: Record<string, unknown> = {};
vi.mock("@/components/state-provider", () => ({
useAppStore: (selector: (state: Record<string, unknown>) => unknown) => selector(storeState),
}));
import { useCumulativeDiff, invalidateCumulativeDiffCache } from "./use-cumulative-diff";
// Mirrors COALESCE_WINDOW_MS in the hook; advance past it to flush the timer.
const WINDOW = 250;
function setStore() {
// No environment mapping → envKey resolves to the sessionId itself.
storeState = {
environmentIdBySessionId: {} as Record<string, string>,
};
}
describe("useCumulativeDiff invalidation coalescing", () => {
beforeEach(() => {
vi.clearAllMocks();
vi.useFakeTimers();
setStore();
mockRequest.mockResolvedValue({ cumulative_diff: { session_id: "x", files: {} } });
});
afterEach(() => {
cleanup();
vi.useRealTimers();
});
it("collapses a burst of invalidations into a single trailing refetch", async () => {
const sid = "sess-burst";
renderHook(() => useCumulativeDiff(sid));
// Drain the mount fetch.
await act(async () => {
await vi.advanceTimersByTimeAsync(0);
});
expect(mockRequest).toHaveBeenCalledTimes(1);
mockRequest.mockClear();
// Fire 5 invalidations within the coalesce window.
act(() => {
for (let i = 0; i < 5; i += 1) invalidateCumulativeDiffCache(sid);
});
// Nothing should have fired yet (trailing edge).
expect(mockRequest).toHaveBeenCalledTimes(0);
await act(async () => {
await vi.advanceTimersByTimeAsync(WINDOW);
});
expect(mockRequest).toHaveBeenCalledTimes(1);
});
it("does not refetch after an unmounted subscriber's coalesced timer fires", async () => {
const sid = "sess-unmount";
const { unmount } = renderHook(() => useCumulativeDiff(sid));
await act(async () => {
await vi.advanceTimersByTimeAsync(0);
});
mockRequest.mockClear();
act(() => {
invalidateCumulativeDiffCache(sid);
});
// Unmount before the window elapses. The timer is NOT cleared (it is shared
// per envKey), but the listener-subscription cleanup removed this hook's
// handler, so the timer firing is a no-op for the unmounted subscriber.
act(() => {
unmount();
});
await act(async () => {
await vi.advanceTimersByTimeAsync(WINDOW);
});
expect(mockRequest).toHaveBeenCalledTimes(0);
});
it("does not double-fetch when a mid-flight invalidation drains alongside a queued timer", async () => {
const sid = "sess-drain";
let resolveFirst: (v: unknown) => void = () => {};
mockRequest.mockReset();
mockRequest
.mockImplementationOnce(
() =>
new Promise((r) => {
resolveFirst = r;
}),
)
.mockResolvedValue({ cumulative_diff: { session_id: "x", files: {} } });
renderHook(() => useCumulativeDiff(sid));
// Mount fetch is in-flight (request #1 pending).
expect(mockRequest).toHaveBeenCalledTimes(1);
// Invalidate while in-flight → sets the pending flag AND queues a timer.
act(() => {
invalidateCumulativeDiffCache(sid);
});
// Resolve the in-flight fetch → finally drains the pending flag (immediate
// follow-up fetch #2) and must cancel the queued timer.
await act(async () => {
resolveFirst({ cumulative_diff: { session_id: "x", files: {} } });
await vi.advanceTimersByTimeAsync(0);
});
expect(mockRequest).toHaveBeenCalledTimes(2);
// The cancelled timer must NOT fire a third fetch.
await act(async () => {
await vi.advanceTimersByTimeAsync(WINDOW);
});
expect(mockRequest).toHaveBeenCalledTimes(2);
});
it("uses independent timers per envKey", async () => {
const sidA = "sess-A";
const sidB = "sess-B";
renderHook(() => useCumulativeDiff(sidA));
renderHook(() => useCumulativeDiff(sidB));
await act(async () => {
await vi.advanceTimersByTimeAsync(0);
});
expect(mockRequest).toHaveBeenCalledTimes(2); // one mount fetch each
mockRequest.mockClear();
act(() => {
invalidateCumulativeDiffCache(sidA);
invalidateCumulativeDiffCache(sidB);
});
await act(async () => {
await vi.advanceTimersByTimeAsync(WINDOW);
});
// One refetch per envKey — they don't collapse into each other.
expect(mockRequest).toHaveBeenCalledTimes(2);
});
});