453 lines · 16.2 KB · TypeScript
RawDownload
import {  DEFAULT_SERVER_SETTINGS,  ProviderDriverKind,  ProviderInstanceId,} from "@t3tools/contracts";import { DEFAULT_CLIENT_SETTINGS, type ClientSettings } from "@t3tools/contracts/settings";import { afterEach, beforeEach, describe, expect, it, vi } from "vite-plus/test"; const persistenceMocks = vi.hoisted(() => ({  getClientSettings: vi.fn<() => Promise<ClientSettings | null>>(),  setClientSettings: vi.fn<(settings: ClientSettings) => Promise<void>>(),})); vi.mock("~/localApi", () => ({  ensureLocalApi: () => ({ persistence: persistenceMocks }),})); import {  __resetClientSettingsPersistenceForTests,  __setClientSettingsForTests,  ensureClientSettingsHydrated,  getClientSettings,  mergeEnvironmentSettings,  persistClientSettingsPatch,  persistClientSettingsUpdate,  resolveEnvironmentIdentificationMode,} from "./useSettings"; beforeEach(() => {  persistenceMocks.getClientSettings.mockReset().mockResolvedValue(null);  persistenceMocks.setClientSettings.mockReset().mockResolvedValue(undefined);  __resetClientSettingsPersistenceForTests();}); afterEach(() => {  vi.restoreAllMocks();}); describe("client settings hydration", () => {  const savedSettings = {    ...DEFAULT_CLIENT_SETTINGS,    timestampFormat: "12-hour" as const,    favorites: [{ provider: ProviderInstanceId.make("codex_work"), model: "gpt-5.6" }],  };  const onboardingCompletedAt = "2026-09-05T12:00:00.000Z";  const complete = (current: ClientSettings) => ({ ...current, onboardingCompletedAt });   it("rejects completion after a failed read and preserves saved preferences on retry", async () => {    const failure = new Error("storage unavailable");    vi.spyOn(console, "error").mockImplementation(() => undefined);    persistenceMocks.getClientSettings      .mockRejectedValueOnce(failure)      .mockResolvedValue(savedSettings);     await expect(persistClientSettingsUpdate(complete)).rejects.toBe(failure);    expect(persistenceMocks.setClientSettings).not.toHaveBeenCalled();    expect(getClientSettings()).toBe(DEFAULT_CLIENT_SETTINGS);     const completedSettings = { ...savedSettings, onboardingCompletedAt };    await expect(persistClientSettingsUpdate(complete)).resolves.toEqual(completedSettings);    expect(persistenceMocks.setClientSettings).toHaveBeenCalledExactlyOnceWith(completedSettings);    expect(persistenceMocks.getClientSettings).toHaveBeenCalledTimes(2);  });   it("uses defaults only after storage confirms no saved settings exist", async () => {    const completedSettings = { ...DEFAULT_CLIENT_SETTINGS, onboardingCompletedAt };     await expect(persistClientSettingsUpdate(complete)).resolves.toEqual(completedSettings);    expect(persistenceMocks.getClientSettings).toHaveBeenCalledOnce();    expect(persistenceMocks.setClientSettings).toHaveBeenCalledExactlyOnceWith(completedSettings);  });   it("holds patches until a pending read supplies the saved preferences", async () => {    let finishRead!: (settings: ClientSettings) => void;    persistenceMocks.getClientSettings.mockImplementationOnce(      () =>        new Promise((resolve) => {          finishRead = resolve;        }),    );    const persisted = new Promise<ClientSettings>((resolve) => {      persistenceMocks.setClientSettings.mockImplementationOnce(async (settings) => {        resolve(settings);      });    });     const hydration = ensureClientSettingsHydrated();    persistClientSettingsPatch({ wordWrap: false });    expect(getClientSettings()).toBe(DEFAULT_CLIENT_SETTINGS);    expect(persistenceMocks.setClientSettings).not.toHaveBeenCalled();     finishRead(savedSettings);    await hydration;    await expect(persisted).resolves.toEqual({ ...savedSettings, wordWrap: false });    expect(persistenceMocks.getClientSettings).toHaveBeenCalledOnce();  });   it("handles failed patch reads without writing and retries with the saved preferences", async () => {    const failure = new Error("storage unavailable");    vi.spyOn(console, "error").mockImplementation(() => undefined);    persistenceMocks.getClientSettings.mockRejectedValue(failure);     const hydration = ensureClientSettingsHydrated();    persistClientSettingsPatch({ wordWrap: false });    await expect(hydration).rejects.toBe(failure);    expect(persistenceMocks.setClientSettings).not.toHaveBeenCalled();     persistenceMocks.getClientSettings.mockResolvedValue(savedSettings);    const persisted = new Promise<ClientSettings>((resolve) => {      persistenceMocks.setClientSettings.mockImplementationOnce(async (settings) => {        resolve(settings);      });    });    persistClientSettingsPatch({ wordWrap: false });     await expect(persisted).resolves.toEqual({ ...savedSettings, wordWrap: false });  });   it("preserves patch order across hydration and a blocked completion write", async () => {    let finishRead!: (settings: ClientSettings) => void;    const read = new Promise<ClientSettings>((resolve) => {      finishRead = resolve;    });    persistenceMocks.getClientSettings.mockReturnValue(read);    let finishCompletionWrite!: () => void;    const blockedWrite = new Promise<void>((resolve) => {      finishCompletionWrite = resolve;    });    let signalCompletionWrite!: () => void;    const completionWriteStarted = new Promise<void>((resolve) => {      signalCompletionWrite = resolve;    });    let durableSettings: ClientSettings = savedSettings;    const persist = vi      .fn<(settings: ClientSettings) => Promise<void>>()      .mockImplementationOnce(async (settings) => {        signalCompletionWrite();        await blockedWrite;        durableSettings = settings;      })      .mockImplementation(async (settings) => {        durableSettings = settings;      });     const completion = persistClientSettingsUpdate(complete, persist);    persistClientSettingsPatch({ wordWrap: false }, persist);    finishRead(savedSettings);    await completionWriteStarted;    persistClientSettingsPatch({ wordWrap: true }, persist);    const finalWrite = persistClientSettingsUpdate((current) => current, persist);     finishCompletionWrite();    await completion;    await finalWrite;     const expected = { ...savedSettings, onboardingCompletedAt, wordWrap: true };    expect(getClientSettings()).toEqual(expected);    expect(durableSettings).toEqual(expected);  });}); describe("persistClientSettingsPatch", () => {  it("waits for settings writes in request order", async () => {    __setClientSettingsForTests(DEFAULT_CLIENT_SETTINGS);    let finishFirst!: () => void;    let markFirstStarted!: () => void;    const firstStarted = new Promise<void>((resolve) => {      markFirstStarted = resolve;    });    persistenceMocks.setClientSettings.mockImplementationOnce(() => {      markFirstStarted();      return new Promise<void>((resolve) => {        finishFirst = resolve;      });    });     const firstSettings = { ...DEFAULT_CLIENT_SETTINGS, snapShotFlash: false };    const secondSettings = { ...firstSettings, snapShotPlaySound: false };    const first = persistClientSettingsPatch({ snapShotFlash: false });    await firstStarted;    const second = persistClientSettingsPatch({ snapShotPlaySound: false });     expect(persistenceMocks.setClientSettings).toHaveBeenCalledTimes(1);    expect(getClientSettings()).toEqual(secondSettings);    finishFirst();    await Promise.all([first, second]);     expect(persistenceMocks.setClientSettings).toHaveBeenNthCalledWith(1, firstSettings);    expect(persistenceMocks.setClientSettings).toHaveBeenNthCalledWith(2, secondSettings);  });}); describe("persistClientSettingsUpdate", () => {  it("publishes the update only after persistence succeeds", async () => {    let finishPersistence!: () => void;    const persistence = new Promise<void>((resolve) => {      finishPersistence = resolve;    });    const setClientSettings = vi.fn(() => persistence);    __setClientSettingsForTests(DEFAULT_CLIENT_SETTINGS);     const pending = persistClientSettingsUpdate(      (current) => ({        ...current,        timestampFormat: "12-hour",      }),      setClientSettings,    );     expect(getClientSettings().timestampFormat).toBe(DEFAULT_CLIENT_SETTINGS.timestampFormat);    finishPersistence();    await expect(pending).resolves.toMatchObject({ timestampFormat: "12-hour" });    expect(getClientSettings().timestampFormat).toBe("12-hour");  });   it("keeps the current snapshot and propagates persistence failure", async () => {    const failure = new Error("disk full");    const setClientSettings = vi.fn().mockRejectedValue(failure);    __setClientSettingsForTests(DEFAULT_CLIENT_SETTINGS);     await expect(      persistClientSettingsUpdate(        (current) => ({ ...current, timestampFormat: "12-hour" }),        setClientSettings,      ),    ).rejects.toBe(failure);    expect(getClientSettings()).toBe(DEFAULT_CLIENT_SETTINGS);  });   it("preserves an optimistic write made while an awaited update persists", async () => {    let finishFirstPersistence!: () => void;    let durableSettings = DEFAULT_CLIENT_SETTINGS;    const firstPersistence = new Promise<void>((resolve) => {      finishFirstPersistence = resolve;    });    const persist = vi      .fn<(settings: typeof DEFAULT_CLIENT_SETTINGS) => Promise<void>>()      .mockImplementationOnce((settings) =>        firstPersistence.then(() => {          durableSettings = settings;        }),      )      .mockImplementation(async (settings) => {        durableSettings = settings;      });    __setClientSettingsForTests(DEFAULT_CLIENT_SETTINGS);    const importedProfile = { id: "profile-import", name: "Imported", kind: "persistent" as const };     const pending = persistClientSettingsUpdate(      (current) => ({        ...current,        browserProfiles: [...current.browserProfiles, importedProfile],      }),      persist,    );    await Promise.resolve();    const patch = persistClientSettingsPatch({ wordWrap: false }, persist);    finishFirstPersistence();    await Promise.all([pending, patch]);     expect(persist).toHaveBeenCalledTimes(3);    expect(persist.mock.calls[1]?.[0]).toMatchObject({      wordWrap: false,    });    expect(persist.mock.calls[1]?.[0].browserProfiles).toContainEqual(importedProfile);    expect(durableSettings.wordWrap).toBe(false);    expect(durableSettings.browserProfiles).toContainEqual(importedProfile);    expect(getClientSettings().wordWrap).toBe(false);    expect(getClientSettings().browserProfiles).toContainEqual(importedProfile);  });   it("orders an awaited update after an older optimistic write", async () => {    let finishOldWrite!: () => void;    let durableSettings = DEFAULT_CLIENT_SETTINGS;    const oldWrite = new Promise<void>((resolve) => {      finishOldWrite = resolve;    });    const persist = vi      .fn<(settings: typeof DEFAULT_CLIENT_SETTINGS) => Promise<void>>()      .mockImplementationOnce((settings) =>        oldWrite.then(() => {          durableSettings = settings;        }),      )      .mockImplementation(async (settings) => {        durableSettings = settings;      });    __setClientSettingsForTests(DEFAULT_CLIENT_SETTINGS);     void persistClientSettingsPatch({ wordWrap: false }, persist);    const importedProfile = { id: "profile-import", name: "Imported", kind: "persistent" as const };    const registration = persistClientSettingsUpdate(      (current) => ({        ...current,        browserProfiles: [...current.browserProfiles, importedProfile],      }),      persist,    );    await Promise.resolve();    expect(persist).toHaveBeenCalledTimes(1);     finishOldWrite();    await registration;     expect(persist).toHaveBeenCalledTimes(2);    expect(durableSettings.wordWrap).toBe(false);    expect(durableSettings.browserProfiles).toContainEqual(importedProfile);  });   it("continues the queue after a rejected write", async () => {    const failure = new Error("disk full");    const persist = vi      .fn<(settings: typeof DEFAULT_CLIENT_SETTINGS) => Promise<void>>()      .mockRejectedValueOnce(failure)      .mockResolvedValue(undefined);    __setClientSettingsForTests(DEFAULT_CLIENT_SETTINGS);     await expect(      persistClientSettingsUpdate(        (current) => ({ ...current, timestampFormat: "12-hour" }),        persist,      ),    ).rejects.toBe(failure);    await expect(      persistClientSettingsUpdate((current) => ({ ...current, wordWrap: false }), persist),    ).resolves.toMatchObject({ wordWrap: false });  });}); describe("resolveEnvironmentIdentificationMode", () => {  it("keeps identification hidden until client settings hydrate", () => {    expect(resolveEnvironmentIdentificationMode({ mode: "artwork", settingsHydrated: false })).toBe(      "none",    );    expect(resolveEnvironmentIdentificationMode({ mode: "pill", settingsHydrated: true })).toBe(      "pill",    );  });   it("uses a pill instead of artwork with a palette theme", () => {    expect(      resolveEnvironmentIdentificationMode({        mode: "artwork",        settingsHydrated: true,        paletteThemeActive: true,      }),    ).toBe("pill");  });   it("respects none with a palette theme", () => {    expect(      resolveEnvironmentIdentificationMode({        mode: "none",        settingsHydrated: true,        paletteThemeActive: true,      }),    ).toBe("none");  });   it("keeps artwork when the palette theme opts into it", () => {    expect(      resolveEnvironmentIdentificationMode({        mode: "artwork",        settingsHydrated: true,        paletteThemeActive: true,        paletteThemeAllowsArtwork: true,      }),    ).toBe("artwork");  });}); describe("mergeEnvironmentSettings", () => {  it("combines the selected environment's server settings with client preferences", () => {    const serverSettings = {      ...DEFAULT_SERVER_SETTINGS,      providerInstances: {        [ProviderInstanceId.make("codex_remote")]: {          driver: ProviderDriverKind.make("codex"),          enabled: true,        },      },    };    const clientSettings = {      ...DEFAULT_CLIENT_SETTINGS,      favorites: [        {          provider: ProviderInstanceId.make("codex_remote"),          model: "gpt-5.4",        },      ],    };     const settings = mergeEnvironmentSettings(serverSettings, clientSettings);     expect(settings.providerInstances).toBe(serverSettings.providerInstances);    expect(settings.favorites).toBe(clientSettings.favorites);  });   it("keeps server settlement settings when legacy client data contains retired keys", () => {    const serverSettings = {      ...DEFAULT_SERVER_SETTINGS,      sidebarAutoSettleAfterDays: 14,      sidebarAutoSettleOnMerge: false,    };    const legacyClientSettings = {      ...DEFAULT_CLIENT_SETTINGS,      sidebarAutoSettleAfterDays: 1,      sidebarAutoSettleOnMerge: true,    };     const settings = mergeEnvironmentSettings(serverSettings, legacyClientSettings);     expect(settings.sidebarAutoSettleAfterDays).toBe(14);    expect(settings.sidebarAutoSettleOnMerge).toBe(false);  });}); describe("onboarding completion persistence", () => {  it("keeps onboarding incomplete after a failed save and preserves preferences on retry", async () => {    const failure = new Error("disk full");    const persist = vi      .fn<(settings: typeof DEFAULT_CLIENT_SETTINGS) => Promise<void>>()      .mockRejectedValueOnce(failure)      .mockResolvedValue(undefined);    const existingSettings = {      ...DEFAULT_CLIENT_SETTINGS,      timestampFormat: "12-hour" as const,      favorites: [        {          provider: ProviderInstanceId.make("codex_work"),          model: "gpt-5.6",        },      ],    };    __setClientSettingsForTests(existingSettings);    const onboardingCompletedAt = "2026-09-01T12:00:00.000Z";    const complete = (current: typeof DEFAULT_CLIENT_SETTINGS) => ({      ...current,      onboardingCompletedAt,    });     await expect(persistClientSettingsUpdate(complete, persist)).rejects.toBe(failure);    expect(getClientSettings()).toBe(existingSettings);    expect(getClientSettings().onboardingCompletedAt).toBeNull();     const completedSettings = { ...existingSettings, onboardingCompletedAt };    await expect(persistClientSettingsUpdate(complete, persist)).resolves.toEqual(      completedSettings,    );    expect(getClientSettings()).toEqual(completedSettings);    expect(persist).toHaveBeenLastCalledWith(completedSettings);  });});