import { describe, it, expect, beforeEach, afterEach, vi } from "vitest"; import { act, renderHook, waitFor } from "@testing-library/react"; 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 SETTINGS_DOWN = "settings down"; 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(), })); const legacyValid = { id: "p_1", kind: "pr" as const, label: "My PRs", customQuery: "author:@me", repoFilter: "", createdAt: SETTINGS_TIMESTAMP, }; const valid: SavedPreset = { ...legacyValid, isDefault: false }; function resetTestState() { __resetSnapshotForTests(); vi.mocked(fetchUserSettings).mockReset(); vi.mocked(updateUserSettings).mockReset(); vi.mocked(fetchGitHubWorkspaceSettings).mockReset(); vi.mocked(updateGitHubWorkspaceSettings).mockReset(); } function workspaceSettings( savedPresets: unknown = [], ): Awaited> { 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>; } async function expectRandomUuidPresetId() { const uuid = "123e4567-e89b-42d3-a456-426614174000"; vi.spyOn(globalThis.crypto, "randomUUID").mockReturnValue(uuid); const existing = { ...valid, id: "p_existing" }; vi.mocked(fetchGitHubWorkspaceSettings).mockResolvedValue(workspaceSettings([existing])); vi.mocked(updateGitHubWorkspaceSettings).mockResolvedValue(workspaceSettings()); const { result } = renderHook(() => useSavedPresets(WORKSPACE_ID)); await waitFor(() => expect(result.current.presets).toEqual([existing])); let created: SavedPreset | null = null; await act(async () => { created = await result.current.save({ kind: "pr", label: "UUID preset", customQuery: "is:open", repoFilter: "", }); }); expect((created as SavedPreset | null)?.id).toBe(`p_${uuid}`); } afterEach(() => { vi.unstubAllEnvs(); vi.restoreAllMocks(); }); describe("useSavedPresets", () => { beforeEach(() => { resetTestState(); }); it("uses empty backend settings without writing", async () => { vi.mocked(fetchUserSettings).mockResolvedValue({ settings: { github_saved_presets: [] }, } as Awaited>); const { result } = renderHook(() => useSavedPresets()); await waitFor(() => expect(result.current.presets).toEqual([])); expect(updateUserSettings).not.toHaveBeenCalled(); }); it("rejects the test reset in production", () => { vi.stubEnv("NODE_ENV", "production"); expect(() => __resetSnapshotForTests()).toThrow( "__resetSnapshotForTests must not be called in production", ); }); }); describe("useSavedPresets workspace sync", () => { beforeEach(() => { resetTestState(); }); it("uses empty workspace settings without writing", async () => { vi.mocked(fetchGitHubWorkspaceSettings).mockResolvedValue(workspaceSettings()); const { result } = renderHook(() => useSavedPresets(WORKSPACE_ID)); await waitFor(() => expect(result.current.presets).toEqual([])); expect(updateGitHubWorkspaceSettings).not.toHaveBeenCalled(); }); it("uses existing workspace presets without writing", async () => { const server = { ...valid, id: "p_server", label: "Server" }; vi.mocked(fetchGitHubWorkspaceSettings).mockResolvedValue(workspaceSettings([server])); const { result } = renderHook(() => useSavedPresets(WORKSPACE_ID)); await waitFor(() => expect(result.current.presets).toEqual([server])); expect(updateGitHubWorkspaceSettings).not.toHaveBeenCalled(); }); it("uses a random UUID for new preset ids", expectRandomUuidPresetId); it("does not save while workspace presets are still loading", async () => { vi.mocked(fetchGitHubWorkspaceSettings).mockReturnValue(new Promise(() => {})); const { result } = renderHook(() => useSavedPresets(WORKSPACE_ID)); let created: SavedPreset | null = valid; await act(async () => { created = await result.current.save({ kind: "pr", label: "Loading", customQuery: "is:open", repoFilter: "", }); }); expect(created).toBeNull(); expect(updateGitHubWorkspaceSettings).not.toHaveBeenCalled(); }); it("does not save after workspace presets fail to load", async () => { vi.mocked(fetchGitHubWorkspaceSettings).mockRejectedValue(new Error(SETTINGS_DOWN)); const { result } = renderHook(() => useSavedPresets(WORKSPACE_ID)); await waitFor(() => expect(fetchGitHubWorkspaceSettings).toHaveBeenCalled()); let created: SavedPreset | null = valid; await act(async () => { created = await result.current.save({ kind: "pr", label: "Failed load", customQuery: "is:open", repoFilter: "", }); }); expect(created).toBeNull(); expect(updateGitHubWorkspaceSettings).not.toHaveBeenCalled(); }); it("does not remove while workspace presets are still loading", async () => { vi.mocked(fetchGitHubWorkspaceSettings).mockReturnValue(new Promise(() => {})); const { result } = renderHook(() => useSavedPresets(WORKSPACE_ID)); await act(async () => { await result.current.remove("p_1"); }); expect(updateGitHubWorkspaceSettings).not.toHaveBeenCalled(); }); it("does not set a default while workspace presets are still loading", async () => { vi.mocked(fetchGitHubWorkspaceSettings).mockReturnValue(new Promise(() => {})); const { result } = renderHook(() => useSavedPresets(WORKSPACE_ID)); await expect(result.current.setDefault("pr", "p_1")).resolves.toBe(false); expect(updateGitHubWorkspaceSettings).not.toHaveBeenCalled(); }); it("keeps mutation callbacks stable across optimistic workspace updates", async () => { vi.mocked(fetchGitHubWorkspaceSettings).mockResolvedValue(workspaceSettings([valid])); vi.mocked(updateGitHubWorkspaceSettings).mockResolvedValue(workspaceSettings()); const { result } = renderHook(() => useSavedPresets(WORKSPACE_ID)); await waitFor(() => expect(result.current.presets).toEqual([valid])); const callbacks = { save: result.current.save, remove: result.current.remove, setDefault: result.current.setDefault, }; await act(async () => { await result.current.save({ kind: "pr", label: "Stable callbacks", customQuery: "is:open", repoFilter: "", }); }); expect(result.current.save).toBe(callbacks.save); expect(result.current.remove).toBe(callbacks.remove); expect(result.current.setDefault).toBe(callbacks.setDefault); }); it("does not remove after workspace presets fail to load", async () => { vi.mocked(fetchGitHubWorkspaceSettings).mockRejectedValue(new Error(SETTINGS_DOWN)); const { result } = renderHook(() => useSavedPresets(WORKSPACE_ID)); await waitFor(() => expect(fetchGitHubWorkspaceSettings).toHaveBeenCalled()); await act(async () => { await result.current.remove("p_1"); }); expect(updateGitHubWorkspaceSettings).not.toHaveBeenCalled(); }); }); describe("useSavedPresets workspace mutation failures", () => { beforeEach(() => { resetTestState(); }); it("rolls back an optimistic save when persistence fails", async () => { vi.mocked(fetchGitHubWorkspaceSettings).mockResolvedValue(workspaceSettings([valid])); vi.mocked(updateGitHubWorkspaceSettings).mockRejectedValue(new Error(SETTINGS_DOWN)); const { result } = renderHook(() => useSavedPresets(WORKSPACE_ID)); await waitFor(() => expect(result.current.presets).toEqual([valid])); await act(async () => { await expect( result.current.save({ kind: "pr", label: "Unsaved", customQuery: "is:open", repoFilter: "", }), ).rejects.toThrow(SETTINGS_DOWN); }); expect(result.current.presets).toEqual([valid]); }); it("rolls back an optimistic removal when persistence fails", async () => { vi.mocked(fetchGitHubWorkspaceSettings).mockResolvedValue(workspaceSettings([valid])); vi.mocked(updateGitHubWorkspaceSettings).mockRejectedValue(new Error(SETTINGS_DOWN)); const { result } = renderHook(() => useSavedPresets(WORKSPACE_ID)); await waitFor(() => expect(result.current.presets).toEqual([valid])); await act(async () => { await expect(result.current.remove(valid.id)).rejects.toThrow(SETTINGS_DOWN); }); expect(result.current.presets).toEqual([valid]); }); }); describe("useSavedPresets repository defaults", () => { beforeEach(() => { resetTestState(); }); it("preserves the chosen repository in the workspace settings update", async () => { const server = { ...valid, id: "p_server", label: "Server" }; vi.mocked(fetchGitHubWorkspaceSettings).mockResolvedValue(workspaceSettings([server])); vi.mocked(updateGitHubWorkspaceSettings).mockResolvedValue(workspaceSettings()); const { result } = renderHook(() => useSavedPresets(WORKSPACE_ID)); await waitFor(() => expect(result.current.presets).toEqual([server])); let created: SavedPreset | null = null; await act(async () => { created = await result.current.save({ kind: "pr", label: "Kandev PRs", customQuery: "is:open", repoFilter: "kdlbs/kandev", }); }); expect((created as SavedPreset | null)?.isDefault).toBe(false); await waitFor(() => expect(updateGitHubWorkspaceSettings).toHaveBeenCalledWith({ workspace_id: WORKSPACE_ID, saved_presets: [ server, expect.objectContaining({ kind: "pr", label: "Kandev PRs", customQuery: "is:open", repoFilter: "kdlbs/kandev", }), ], }), ); }); it.each([ { caseName: "missing", serverPreset: { id: "p_legacy_missing", kind: "pr", label: "Legacy missing repo", customQuery: "author:@me", createdAt: SETTINGS_TIMESTAMP, }, }, { caseName: "non-string", serverPreset: { id: "p_legacy_invalid", kind: "pr", label: "Legacy invalid repo", customQuery: "author:@me", repoFilter: 42, createdAt: SETTINGS_TIMESTAMP, }, }, ])("normalizes a $caseName server repoFilter to All repos", async ({ serverPreset }) => { vi.mocked(fetchGitHubWorkspaceSettings).mockResolvedValue(workspaceSettings([serverPreset])); const { result } = renderHook(() => useSavedPresets(WORKSPACE_ID)); await waitFor(() => expect(result.current.presets).toEqual([ { ...serverPreset, repoFilter: "", isDefault: false }, ]), ); }); it("normalizes a legacy saved query to a non-default view", async () => { vi.mocked(fetchGitHubWorkspaceSettings).mockResolvedValue(workspaceSettings([legacyValid])); const { result } = renderHook(() => useSavedPresets(WORKSPACE_ID)); await waitFor(() => expect(result.current.presets).toEqual([{ ...legacyValid, isDefault: false }]), ); }); it("keeps only the first persisted default for each result kind", async () => { const issue = { ...valid, id: "issue-1", kind: "issue", isDefault: true }; const duplicates = [ { ...valid, id: "pr-1", isDefault: true }, { ...valid, id: "pr-2", isDefault: true }, issue, ]; vi.mocked(fetchGitHubWorkspaceSettings).mockResolvedValue(workspaceSettings(duplicates)); const { result } = renderHook(() => useSavedPresets(WORKSPACE_ID)); await waitFor(() => expect(result.current.presets.map(({ id, isDefault }) => ({ id, isDefault }))).toEqual([ { id: "pr-1", isDefault: true }, { id: "pr-2", isDefault: false }, { id: "issue-1", isDefault: true }, ]), ); }); }); describe("useSavedPresets default persistence", () => { beforeEach(() => { resetTestState(); }); it("publishes a workspace default only after persistence succeeds", async () => { const prA = { ...valid, id: "pr-a", isDefault: true }; const prB = { ...valid, id: "pr-b" }; const issue = { ...valid, id: "issue-a", kind: "issue" as const, isDefault: true }; vi.mocked(fetchGitHubWorkspaceSettings).mockResolvedValue(workspaceSettings([prA, prB, issue])); let resolveUpdate!: (value: Awaited>) => void; vi.mocked(updateGitHubWorkspaceSettings).mockReturnValue( new Promise((resolve) => { resolveUpdate = resolve; }), ); const { result } = renderHook(() => useSavedPresets(WORKSPACE_ID)); await waitFor(() => expect(result.current.presets).toHaveLength(3)); let mutation!: Promise; act(() => { mutation = result.current.setDefault("pr", "pr-b"); }); expect(result.current.presets.map((preset) => preset.isDefault)).toEqual([true, false, true]); let persisted!: boolean; await act(async () => { resolveUpdate(workspaceSettings()); persisted = await mutation; }); expect(persisted).toBe(true); expect(updateGitHubWorkspaceSettings).toHaveBeenCalledWith({ workspace_id: WORKSPACE_ID, saved_presets: [{ ...prA, isDefault: false }, { ...prB, isDefault: true }, issue], }); expect(result.current.presets.map((preset) => preset.isDefault)).toEqual([false, true, true]); }); it("retains the prior workspace default when persistence fails", async () => { const prA = { ...valid, id: "pr-a", isDefault: true }; const prB = { ...valid, id: "pr-b" }; vi.mocked(fetchGitHubWorkspaceSettings).mockResolvedValue(workspaceSettings([prA, prB])); vi.mocked(updateGitHubWorkspaceSettings).mockRejectedValue(new Error(SETTINGS_DOWN)); const { result } = renderHook(() => useSavedPresets(WORKSPACE_ID)); await waitFor(() => expect(result.current.presets).toHaveLength(2)); await expect(result.current.setDefault("pr", "pr-b")).rejects.toThrow(SETTINGS_DOWN); expect(result.current.presets.map((preset) => preset.isDefault)).toEqual([true, false]); }); it("persists and publishes a portable user default", async () => { const prA = { ...valid, id: "pr-a" }; vi.mocked(fetchUserSettings).mockResolvedValue({ settings: { github_saved_presets: [prA] }, } as Awaited>); vi.mocked(updateUserSettings).mockResolvedValue( undefined as unknown as Awaited>, ); const { result } = renderHook(() => useSavedPresets()); await waitFor(() => expect(result.current.presets).toEqual([prA])); await act(async () => { await result.current.setDefault("pr", "pr-a"); }); expect(updateUserSettings).toHaveBeenCalledWith({ github_saved_presets: [{ ...prA, isDefault: true }], }); expect(result.current.presets).toEqual([{ ...prA, isDefault: true }]); }); it("retains the prior portable user default when persistence fails", async () => { const prA = { ...valid, id: "pr-a", isDefault: true }; const prB = { ...valid, id: "pr-b" }; vi.mocked(fetchUserSettings).mockResolvedValue({ settings: { github_saved_presets: [prA, prB] }, } as Awaited>); vi.mocked(updateUserSettings).mockRejectedValue(new Error(SETTINGS_DOWN)); const { result } = renderHook(() => useSavedPresets()); await waitFor(() => expect(result.current.presets).toHaveLength(2)); await expect(result.current.setDefault("pr", "pr-b")).rejects.toThrow(SETTINGS_DOWN); expect(result.current.presets.map((preset) => preset.isDefault)).toEqual([true, false]); }); });