import { beforeEach, describe, expect, it, vi } from "vitest"; import { waitFor } from "@testing-library/react"; import { ApiError } from "@/lib/api/client"; import { updateUserSettings } from "@/lib/api/domains/settings-api"; import { createQueuedUserSettingsSync, createQueuedUserSettingsSyncWithResponse, } from "./user-settings-sync"; vi.mock("@/lib/api/domains/settings-api", () => ({ updateUserSettings: vi.fn(), })); function deferred() { let resolve!: (value: T) => void; const promise = new Promise((res) => { resolve = res; }); return { promise, resolve }; } describe("createQueuedUserSettingsSync", () => { beforeEach(() => { window.localStorage.clear(); vi.mocked(updateUserSettings).mockReset(); }); it("serializes backend writes so later payloads are sent after earlier ones finish", async () => { const first = deferred>>(); vi.mocked(updateUserSettings) .mockReturnValueOnce(first.promise) .mockResolvedValueOnce({ settings: {} } as Awaited>); const sync = createQueuedUserSettingsSync((value) => ({ preferred_shell: value, })); void sync("bash"); void sync("zsh"); await waitFor(() => { expect(updateUserSettings).toHaveBeenCalledTimes(1); }); expect(updateUserSettings).toHaveBeenCalledWith({ preferred_shell: "bash" }); first.resolve({ settings: {} } as Awaited>); await waitFor(() => { expect(updateUserSettings).toHaveBeenCalledTimes(2); expect(updateUserSettings).toHaveBeenLastCalledWith({ preferred_shell: "zsh" }); }); }); it("returns the authoritative response when the caller needs its revision", async () => { const response = { settings: { revision: 7 } } as Awaited< ReturnType >; vi.mocked(updateUserSettings).mockResolvedValueOnce(response); const sync = createQueuedUserSettingsSyncWithResponse((value) => ({ preferred_shell: value, })); await expect(sync("zsh")).resolves.toBe(response); }); it("retries a transient settings write before processing the next payload", async () => { vi.mocked(updateUserSettings) .mockRejectedValueOnce(new Error("temporary failure")) .mockResolvedValueOnce({ settings: {} } as Awaited>) .mockResolvedValueOnce({ settings: {} } as Awaited>); const sync = createQueuedUserSettingsSync((value) => ({ preferred_shell: value, })); void sync("bash"); void sync("zsh"); await waitFor(() => { expect(updateUserSettings).toHaveBeenNthCalledWith(1, { preferred_shell: "bash" }); expect(updateUserSettings).toHaveBeenNthCalledWith(2, { preferred_shell: "bash" }); expect(updateUserSettings).toHaveBeenNthCalledWith(3, { preferred_shell: "zsh" }); }); }); it("does not retry an API error", async () => { vi.mocked(updateUserSettings).mockRejectedValueOnce(new ApiError("invalid settings", 400, {})); const sync = createQueuedUserSettingsSync((value) => ({ preferred_shell: value, })); await expect(sync("bash")).rejects.toBeInstanceOf(ApiError); expect(updateUserSettings).toHaveBeenCalledTimes(1); }); it("rejects failed writes so explicit-save callers remain dirty and retryable", async () => { const failure = new Error("network unavailable"); vi.mocked(updateUserSettings).mockRejectedValue(failure); const sync = createQueuedUserSettingsSync((value) => ({ preferred_shell: value, })); await expect(sync("zsh")).rejects.toBe(failure); }); });