Files
kandev/apps/web/components/github/my-github/use-saved-presets-concurrency.test.ts
2026-08-11 11:52:34 +01:00

591 lines
20 KiB
TypeScript

import { act, renderHook, waitFor } from "@testing-library/react";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { fetchUserSettings, updateUserSettings } from "@/lib/api/domains/settings-api";
import {
fetchGitHubWorkspaceSettings,
updateGitHubWorkspaceSettings,
} from "@/lib/api/domains/github-api";
import { __resetSnapshotForTests, useSavedPresets, type SavedPreset } from "./use-saved-presets";
const WORKSPACE_ID = "ws-1";
const SETTINGS_TIMESTAMP = "2026-01-01T00:00:00Z";
const WORKSPACE_MODE = "workspace";
const PORTABLE_USER_MODE = "portable user";
const MODES = [WORKSPACE_MODE, PORTABLE_USER_MODE] as const;
type PersistenceMode = (typeof MODES)[number];
const valid: SavedPreset = {
id: "pr-a",
kind: "pr",
label: "PR A",
customQuery: "author:@me",
repoFilter: "",
createdAt: SETTINGS_TIMESTAMP,
isDefault: false,
};
vi.mock("@/lib/api/domains/settings-api", () => ({
fetchUserSettings: vi.fn(),
updateUserSettings: vi.fn(),
}));
vi.mock("@/lib/api/domains/github-api", () => ({
fetchGitHubWorkspaceSettings: vi.fn(),
updateGitHubWorkspaceSettings: vi.fn(),
}));
function deferred<T>() {
let resolve!: (value: T) => void;
const promise = new Promise<T>((res) => {
resolve = res;
});
return { promise, resolve };
}
function workspaceSettings(
savedPresets: unknown = [],
): Awaited<ReturnType<typeof fetchGitHubWorkspaceSettings>> {
return {
workspace_id: WORKSPACE_ID,
repo_scope_mode: "all",
repo_scope_orgs: [],
repo_scope_repos: [],
saved_presets: savedPresets,
default_query_presets: null,
created_at: SETTINGS_TIMESTAMP,
updated_at: SETTINGS_TIMESTAMP,
} as Awaited<ReturnType<typeof fetchGitHubWorkspaceSettings>>;
}
function requirePreset(value: SavedPreset | null): SavedPreset {
expect(value).not.toBeNull();
if (value === null) throw new Error("Expected a saved preset");
return value;
}
function mockHydration(mode: PersistenceMode, presets: SavedPreset[]) {
if (mode === WORKSPACE_MODE) {
vi.mocked(fetchGitHubWorkspaceSettings).mockResolvedValue(workspaceSettings(presets));
return;
}
vi.mocked(fetchUserSettings).mockResolvedValue({
settings: { github_saved_presets: presets },
} as Awaited<ReturnType<typeof fetchUserSettings>>);
}
function deferFirstPersistence(mode: PersistenceMode): () => void {
if (mode === WORKSPACE_MODE) {
const first = deferred<Awaited<ReturnType<typeof updateGitHubWorkspaceSettings>>>();
vi.mocked(updateGitHubWorkspaceSettings)
.mockReturnValueOnce(first.promise)
.mockResolvedValueOnce(workspaceSettings());
return () => first.resolve(workspaceSettings());
}
const first = deferred<Awaited<ReturnType<typeof updateUserSettings>>>();
const success = undefined as unknown as Awaited<ReturnType<typeof updateUserSettings>>;
vi.mocked(updateUserSettings).mockReturnValueOnce(first.promise).mockResolvedValueOnce(success);
return () => first.resolve(success);
}
function expectPersistenceCalls(mode: PersistenceMode, count: number) {
if (mode === WORKSPACE_MODE) {
expect(updateGitHubWorkspaceSettings).toHaveBeenCalledTimes(count);
} else {
expect(updateUserSettings).toHaveBeenCalledTimes(count);
}
}
function expectLastPersisted(mode: PersistenceMode, presets: SavedPreset[]) {
if (mode === WORKSPACE_MODE) {
expect(updateGitHubWorkspaceSettings).toHaveBeenLastCalledWith({
workspace_id: WORKSPACE_ID,
saved_presets: presets,
});
} else {
expect(updateUserSettings).toHaveBeenLastCalledWith({ github_saved_presets: presets });
}
}
async function renderLoaded(mode: PersistenceMode, presets: SavedPreset[]) {
mockHydration(mode, presets);
const hook = renderHook(() => useSavedPresets(mode === WORKSPACE_MODE ? WORKSPACE_ID : null));
await waitFor(() => expect(hook.result.current.presets).toEqual(presets));
return hook;
}
async function expectPendingDefaultTargetRemoval(mode: PersistenceMode) {
const prA = { ...valid, isDefault: true };
const prB = { ...valid, id: "pr-b", label: "PR B" };
const resolveDefault = deferFirstPersistence(mode);
const { result } = await renderLoaded(mode, [prA, prB]);
let defaultMutation!: Promise<boolean>;
act(() => {
defaultMutation = result.current.setDefault("pr", "pr-b");
});
await waitFor(() => expectPersistenceCalls(mode, 1));
let removeMutation!: Promise<boolean>;
act(() => {
removeMutation = result.current.remove("pr-b");
});
await act(async () => {
resolveDefault();
await Promise.all([defaultMutation, removeMutation]);
});
await waitFor(() => expectPersistenceCalls(mode, 2));
const expected = [prA];
expect(result.current.presets).toEqual(expected);
expectLastPersisted(mode, expected);
}
async function expectIndependentWorkspacePersistence() {
const firstWorkspaceId = "ws-first";
const secondWorkspaceId = "ws-second";
const firstWrite = deferred<Awaited<ReturnType<typeof updateGitHubWorkspaceSettings>>>();
vi.mocked(fetchGitHubWorkspaceSettings).mockImplementation(async (workspaceId) => ({
...workspaceSettings([valid]),
workspace_id: workspaceId,
}));
vi.mocked(updateGitHubWorkspaceSettings).mockImplementation((settings) => {
const response = { ...workspaceSettings(), workspace_id: settings.workspace_id };
return settings.workspace_id === firstWorkspaceId
? firstWrite.promise
: Promise.resolve(response);
});
const firstHook = renderHook(() => useSavedPresets(firstWorkspaceId));
const secondHook = renderHook(() => useSavedPresets(secondWorkspaceId));
await waitFor(() => expect(firstHook.result.current.presets).toEqual([valid]));
await waitFor(() => expect(secondHook.result.current.presets).toEqual([valid]));
let firstSave!: Promise<SavedPreset | null>;
act(() => {
firstSave = firstHook.result.current.save({
kind: "pr",
label: "First workspace",
customQuery: "is:open",
repoFilter: "",
});
});
await waitFor(() =>
expect(updateGitHubWorkspaceSettings).toHaveBeenCalledWith(
expect.objectContaining({ workspace_id: firstWorkspaceId }),
),
);
let secondSave!: Promise<SavedPreset | null>;
act(() => {
secondSave = secondHook.result.current.save({
kind: "pr",
label: "Second workspace",
customQuery: "is:closed",
repoFilter: "",
});
});
try {
await waitFor(
() =>
expect(updateGitHubWorkspaceSettings).toHaveBeenCalledWith(
expect.objectContaining({ workspace_id: secondWorkspaceId }),
),
{ timeout: 250 },
);
} finally {
await act(async () => {
firstWrite.resolve({ ...workspaceSettings(), workspace_id: firstWorkspaceId });
await Promise.all([firstSave, secondSave]);
});
}
}
async function expectQueuedWorkspaceSaveIsolation() {
const firstWorkspaceId = "ws-first";
const secondWorkspaceId = "ws-second";
const firstWorkspacePreset = { ...valid, label: "Workspace A" };
const secondWorkspacePreset = { ...valid, id: "pr-b", label: "Workspace B" };
const firstWrite = deferred<Awaited<ReturnType<typeof updateGitHubWorkspaceSettings>>>();
vi.mocked(fetchGitHubWorkspaceSettings).mockImplementation(async (workspaceId) => ({
...workspaceSettings(
workspaceId === firstWorkspaceId ? [firstWorkspacePreset] : [secondWorkspacePreset],
),
workspace_id: workspaceId,
}));
vi.mocked(updateGitHubWorkspaceSettings)
.mockReturnValueOnce(firstWrite.promise)
.mockResolvedValueOnce({ ...workspaceSettings(), workspace_id: firstWorkspaceId });
const { result, rerender } = renderHook(({ workspaceId }) => useSavedPresets(workspaceId), {
initialProps: { workspaceId: firstWorkspaceId },
});
await waitFor(() => expect(result.current.presets).toEqual([firstWorkspacePreset]));
let firstSave!: Promise<SavedPreset | null>;
act(() => {
firstSave = result.current.save({
kind: "pr",
label: "First queued",
customQuery: "is:open",
repoFilter: "",
});
});
await waitFor(() => expect(updateGitHubWorkspaceSettings).toHaveBeenCalledTimes(1));
let secondSave!: Promise<SavedPreset | null>;
act(() => {
secondSave = result.current.save({
kind: "pr",
label: "Second queued",
customQuery: "is:closed",
repoFilter: "",
});
});
rerender({ workspaceId: secondWorkspaceId });
await waitFor(() => expect(result.current.presets).toEqual([secondWorkspacePreset]));
await act(async () => {
firstWrite.resolve({ ...workspaceSettings(), workspace_id: firstWorkspaceId });
await Promise.all([firstSave, secondSave]);
});
await waitFor(() => expect(updateGitHubWorkspaceSettings).toHaveBeenCalledTimes(2));
const secondPayload = vi.mocked(updateGitHubWorkspaceSettings).mock.calls[1]?.[0];
expect(secondPayload?.workspace_id).toBe(firstWorkspaceId);
expect((secondPayload?.saved_presets as SavedPreset[]).map((preset) => preset.label)).toEqual([
"Workspace A",
"First queued",
"Second queued",
]);
expect(result.current.presets).toEqual([secondWorkspacePreset]);
}
async function expectWorkspaceNavigationUnblocksNewMutations() {
const firstWorkspaceId = "ws-first";
const secondWorkspaceId = "ws-second";
const firstWorkspacePreset = { ...valid, label: "Workspace A" };
const secondWorkspacePreset = { ...valid, id: "pr-b", label: "Workspace B" };
const firstWrite = deferred<Awaited<ReturnType<typeof updateGitHubWorkspaceSettings>>>();
vi.mocked(fetchGitHubWorkspaceSettings).mockImplementation(async (workspaceId) => ({
...workspaceSettings(
workspaceId === firstWorkspaceId ? [firstWorkspacePreset] : [secondWorkspacePreset],
),
workspace_id: workspaceId,
}));
vi.mocked(updateGitHubWorkspaceSettings)
.mockReturnValueOnce(firstWrite.promise)
.mockResolvedValue(workspaceSettings());
const { result, rerender } = renderHook(({ workspaceId }) => useSavedPresets(workspaceId), {
initialProps: { workspaceId: firstWorkspaceId },
});
await waitFor(() => expect(result.current.presets).toEqual([firstWorkspacePreset]));
let firstSave!: Promise<SavedPreset | null>;
act(() => {
firstSave = result.current.save({
kind: "pr",
label: "First pending",
customQuery: "is:open",
repoFilter: "",
});
});
await waitFor(() => expect(updateGitHubWorkspaceSettings).toHaveBeenCalledTimes(1));
rerender({ workspaceId: secondWorkspaceId });
await waitFor(() => expect(result.current.presets).toEqual([secondWorkspacePreset]));
let secondSave!: Promise<SavedPreset | null>;
act(() => {
secondSave = result.current.save({
kind: "pr",
label: "Second immediate",
customQuery: "is:closed",
repoFilter: "",
});
});
try {
await waitFor(
() =>
expect(updateGitHubWorkspaceSettings).toHaveBeenCalledWith(
expect.objectContaining({ workspace_id: secondWorkspaceId }),
),
{ timeout: 250 },
);
} finally {
await act(async () => {
firstWrite.resolve(workspaceSettings());
await Promise.all([firstSave, secondSave]);
});
}
}
async function expectSaveThenRemove(mode: PersistenceMode) {
const resolveSave = deferFirstPersistence(mode);
const { result } = await renderLoaded(mode, [valid]);
let saveMutation!: Promise<SavedPreset | null>;
act(() => {
saveMutation = result.current.save({
kind: "pr",
label: "Saved while deleting",
customQuery: "is:open",
repoFilter: "",
});
});
await waitFor(() => expectPersistenceCalls(mode, 1));
let removeMutation!: Promise<boolean>;
act(() => {
removeMutation = result.current.remove(valid.id);
});
let created: SavedPreset | null = null;
let removed = false;
await act(async () => {
resolveSave();
[created, removed] = await Promise.all([saveMutation, removeMutation]);
});
await waitFor(() => expectPersistenceCalls(mode, 2));
const expected = [requirePreset(created)];
expect(removed).toBe(true);
expect(result.current.presets).toEqual(expected);
expectLastPersisted(mode, expected);
}
async function expectPortableDefaultsAcrossHookInstances() {
const pr = { ...valid, isDefault: false };
const issue = { ...valid, id: "issue-a", kind: "issue" as const, isDefault: false };
const resolveFirst = deferFirstPersistence(PORTABLE_USER_MODE);
mockHydration(PORTABLE_USER_MODE, [pr, issue]);
const first = renderHook(() => useSavedPresets());
const second = renderHook(() => useSavedPresets());
await waitFor(() => expect(first.result.current.presets).toEqual([pr, issue]));
await waitFor(() => expect(second.result.current.presets).toEqual([pr, issue]));
let firstMutation!: Promise<boolean>;
act(() => {
firstMutation = first.result.current.setDefault("pr", pr.id);
});
await waitFor(() => expectPersistenceCalls(PORTABLE_USER_MODE, 1));
let secondMutation!: Promise<boolean>;
act(() => {
secondMutation = second.result.current.setDefault("issue", issue.id);
});
await act(async () => {
resolveFirst();
await Promise.all([firstMutation, secondMutation]);
});
await waitFor(() => expectPersistenceCalls(PORTABLE_USER_MODE, 2));
const expected = [
{ ...pr, isDefault: true },
{ ...issue, isDefault: true },
];
expectLastPersisted(PORTABLE_USER_MODE, expected);
expect(first.result.current.presets).toEqual(expected);
expect(second.result.current.presets).toEqual(expected);
}
async function expectPortableDefaultLastWriteWins() {
const prA = { ...valid, isDefault: false };
const prB = { ...valid, id: "pr-b", label: "PR B", isDefault: false };
const resolveFirst = deferFirstPersistence(PORTABLE_USER_MODE);
mockHydration(PORTABLE_USER_MODE, [prA, prB]);
const first = renderHook(() => useSavedPresets());
const second = renderHook(() => useSavedPresets());
await waitFor(() => expect(first.result.current.presets).toEqual([prA, prB]));
await waitFor(() => expect(second.result.current.presets).toEqual([prA, prB]));
let firstMutation!: Promise<boolean>;
act(() => {
firstMutation = first.result.current.setDefault("pr", prA.id);
});
await waitFor(() => expectPersistenceCalls(PORTABLE_USER_MODE, 1));
let secondMutation!: Promise<boolean>;
act(() => {
secondMutation = second.result.current.setDefault("pr", prB.id);
});
await act(async () => {
resolveFirst();
await Promise.all([firstMutation, secondMutation]);
});
await waitFor(() => expectPersistenceCalls(PORTABLE_USER_MODE, 2));
const expected = [
{ ...prA, isDefault: false },
{ ...prB, isDefault: true },
];
expectLastPersisted(PORTABLE_USER_MODE, expected);
expect(first.result.current.presets).toEqual(expected);
expect(second.result.current.presets).toEqual(expected);
}
async function expectWorkspaceResetDetachesStaleMutation() {
const firstWrite = deferred<Awaited<ReturnType<typeof updateGitHubWorkspaceSettings>>>();
vi.mocked(updateGitHubWorkspaceSettings)
.mockReturnValueOnce(firstWrite.promise)
.mockResolvedValue(workspaceSettings());
const firstHook = await renderLoaded(WORKSPACE_MODE, [valid]);
let firstSave!: Promise<SavedPreset | null>;
act(() => {
firstSave = firstHook.result.current.save({
kind: "pr",
label: "First write",
customQuery: "is:open",
repoFilter: "",
});
});
await waitFor(() => expect(updateGitHubWorkspaceSettings).toHaveBeenCalledTimes(1));
firstHook.unmount();
__resetSnapshotForTests();
const secondHook = await renderLoaded(WORKSPACE_MODE, [valid]);
let secondSave!: Promise<SavedPreset | null>;
act(() => {
secondSave = secondHook.result.current.save({
kind: "pr",
label: "Second write",
customQuery: "is:closed",
repoFilter: "",
});
});
await waitFor(() => expect(updateGitHubWorkspaceSettings).toHaveBeenCalledTimes(2));
await act(async () => {
firstWrite.resolve(workspaceSettings());
await Promise.all([firstSave, secondSave]);
});
}
async function expectPortableResetDetachesStaleMutation() {
const resolveFirst = deferFirstPersistence(PORTABLE_USER_MODE);
const firstHook = await renderLoaded(PORTABLE_USER_MODE, [valid]);
let firstDefault!: Promise<boolean>;
act(() => {
firstDefault = firstHook.result.current.setDefault("pr", valid.id);
});
await waitFor(() => expectPersistenceCalls(PORTABLE_USER_MODE, 1));
firstHook.unmount();
__resetSnapshotForTests();
const freshPreset = { ...valid, label: "Fresh test state" };
const secondHook = await renderLoaded(PORTABLE_USER_MODE, [freshPreset]);
await act(async () => {
resolveFirst();
await firstDefault;
});
expect(secondHook.result.current.presets).toEqual([freshPreset]);
}
function resetMutationTestState() {
__resetSnapshotForTests();
vi.mocked(fetchUserSettings).mockReset();
vi.mocked(updateUserSettings).mockReset();
vi.mocked(fetchGitHubWorkspaceSettings).mockReset();
vi.mocked(updateGitHubWorkspaceSettings).mockReset();
}
describe("useSavedPresets mutation ordering", () => {
beforeEach(resetMutationTestState);
it.each(MODES)("preserves a %s save while a default update is pending", async (mode) => {
const prA = { ...valid, isDefault: true };
const prB = { ...valid, id: "pr-b", label: "PR B" };
const resolveDefault = deferFirstPersistence(mode);
const { result } = await renderLoaded(mode, [prA, prB]);
let defaultMutation!: Promise<boolean>;
act(() => {
defaultMutation = result.current.setDefault("pr", "pr-b");
});
await waitFor(() => expectPersistenceCalls(mode, 1));
let saveMutation!: Promise<SavedPreset | null>;
act(() => {
saveMutation = result.current.save({
kind: "pr",
label: "Saved while pending",
customQuery: "is:open",
repoFilter: "kdlbs/kandev",
});
});
let created: SavedPreset | null = null;
await act(async () => {
resolveDefault();
[, created] = await Promise.all([defaultMutation, saveMutation]);
});
await waitFor(() => expectPersistenceCalls(mode, 2));
const saved = requirePreset(created);
const expected = [{ ...prA, isDefault: false }, { ...prB, isDefault: true }, saved];
expect(result.current.presets).toEqual(expected);
expectLastPersisted(mode, expected);
});
it.each(MODES)("preserves a %s delete while a default update is pending", async (mode) => {
const prA = { ...valid, isDefault: true };
const prB = { ...valid, id: "pr-b", label: "PR B" };
const resolveDefault = deferFirstPersistence(mode);
const { result } = await renderLoaded(mode, [prA, prB]);
let defaultMutation!: Promise<boolean>;
act(() => {
defaultMutation = result.current.setDefault("pr", "pr-b");
});
await waitFor(() => expectPersistenceCalls(mode, 1));
let removeMutation!: Promise<boolean>;
act(() => {
removeMutation = result.current.remove("pr-a");
});
await act(async () => {
resolveDefault();
await Promise.all([defaultMutation, removeMutation]);
});
await waitFor(() => expectPersistenceCalls(mode, 2));
const expected = [{ ...prB, isDefault: true }];
expect(result.current.presets).toEqual(expected);
expectLastPersisted(mode, expected);
});
it.each(MODES)("applies a %s delete while a save is pending", expectSaveThenRemove);
it(
"serializes portable defaults across hook instances",
expectPortableDefaultsAcrossHookInstances,
);
it("last portable default wins across hook instances", expectPortableDefaultLastWriteWins);
it.each(MODES)("removes a pending %s default target", expectPendingDefaultTargetRemoval);
it("persists different workspaces independently", expectIndependentWorkspacePersistence);
it("keeps queued saves scoped across workspace navigation", expectQueuedWorkspaceSaveIsolation);
it("new workspace skips the old mutation queue", expectWorkspaceNavigationUnblocksNewMutations);
it(
"detaches future workspace writes from a stale test queue",
expectWorkspaceResetDetachesStaleMutation,
);
it(
"ignores stale portable outcomes after a test reset",
expectPortableResetDetachesStaleMutation,
);
});