| 1 | import { |
| 2 | DEFAULT_SERVER_SETTINGS, |
| 3 | ProviderDriverKind, |
| 4 | ProviderInstanceId, |
| 5 | } from "@t3tools/contracts"; |
| 6 | import { DEFAULT_CLIENT_SETTINGS, type ClientSettings } from "@t3tools/contracts/settings"; |
| 7 | import { afterEach, beforeEach, describe, expect, it, vi } from "vite-plus/test"; |
| 8 | |
| 9 | const persistenceMocks = vi.hoisted(() => ({ |
| 10 | getClientSettings: vi.fn<() => Promise<ClientSettings | null>>(), |
| 11 | setClientSettings: vi.fn<(settings: ClientSettings) => Promise<void>>(), |
| 12 | })); |
| 13 | |
| 14 | vi.mock("~/localApi", () => ({ |
| 15 | ensureLocalApi: () => ({ persistence: persistenceMocks }), |
| 16 | })); |
| 17 | |
| 18 | import { |
| 19 | __resetClientSettingsPersistenceForTests, |
| 20 | __setClientSettingsForTests, |
| 21 | ensureClientSettingsHydrated, |
| 22 | getClientSettings, |
| 23 | mergeEnvironmentSettings, |
| 24 | persistClientSettingsPatch, |
| 25 | persistClientSettingsUpdate, |
| 26 | prepareModelPreferencesPatch, |
| 27 | resolveEnvironmentIdentificationMode, |
| 28 | } from "./useSettings"; |
| 29 | import { applyServerSettingsPatch } from "@t3tools/shared/serverSettings"; |
| 30 | |
| 31 | beforeEach(() => { |
| 32 | persistenceMocks.getClientSettings.mockReset().mockResolvedValue(null); |
| 33 | persistenceMocks.setClientSettings.mockReset().mockResolvedValue(undefined); |
| 34 | __resetClientSettingsPersistenceForTests(); |
| 35 | }); |
| 36 | |
| 37 | afterEach(() => { |
| 38 | vi.restoreAllMocks(); |
| 39 | }); |
| 40 | |
| 41 | describe("client settings hydration", () => { |
| 42 | const savedSettings = { |
| 43 | ...DEFAULT_CLIENT_SETTINGS, |
| 44 | timestampFormat: "12-hour" as const, |
| 45 | favorites: [{ provider: ProviderInstanceId.make("codex_work"), model: "gpt-5.6" }], |
| 46 | }; |
| 47 | const onboardingCompletedAt = "2026-09-05T12:00:00.000Z"; |
| 48 | const complete = (current: ClientSettings) => ({ ...current, onboardingCompletedAt }); |
| 49 | |
| 50 | it("waits for saved model preferences before applying the first server edit", async () => { |
| 51 | const provider = ProviderInstanceId.make("codex_work"); |
| 52 | let finishRead!: (settings: ClientSettings) => void; |
| 53 | persistenceMocks.getClientSettings.mockReturnValueOnce( |
| 54 | new Promise((resolve) => { |
| 55 | finishRead = resolve; |
| 56 | }), |
| 57 | ); |
| 58 | const patch = prepareModelPreferencesPatch(DEFAULT_SERVER_SETTINGS, { |
| 59 | setModelFavorites: [{ provider, model: "new", favorite: true }], |
| 60 | setModelsHidden: [{ provider, model: "new", hidden: true }], |
| 61 | }); |
| 62 | expect(getClientSettings()).toBe(DEFAULT_CLIENT_SETTINGS); |
| 63 | finishRead({ |
| 64 | ...savedSettings, |
| 65 | providerModelPreferences: { |
| 66 | [provider]: { hiddenModels: ["saved-hidden"], modelOrder: ["saved-order"] }, |
| 67 | }, |
| 68 | }); |
| 69 | const edited = applyServerSettingsPatch(DEFAULT_SERVER_SETTINGS, await patch); |
| 70 | expect(edited.favorites).toEqual([...savedSettings.favorites, { provider, model: "new" }]); |
| 71 | expect(edited.providerModelPreferences?.[provider]).toEqual({ |
| 72 | hiddenModels: ["saved-hidden", "new"], |
| 73 | modelOrder: ["saved-order"], |
| 74 | }); |
| 75 | }); |
| 76 | |
| 77 | it("does not prepare a model preference write after a failed legacy read", async () => { |
| 78 | const failure = new Error("storage unavailable"); |
| 79 | vi.spyOn(console, "error").mockImplementation(() => undefined); |
| 80 | persistenceMocks.getClientSettings.mockRejectedValueOnce(failure); |
| 81 | await expect( |
| 82 | prepareModelPreferencesPatch(DEFAULT_SERVER_SETTINGS, { |
| 83 | setProviderModelOrder: { provider: ProviderInstanceId.make("codex_work"), modelOrder: [] }, |
| 84 | }), |
| 85 | ).rejects.toBe(failure); |
| 86 | }); |
| 87 | |
| 88 | it("rejects completion after a failed read and preserves saved preferences on retry", async () => { |
| 89 | const failure = new Error("storage unavailable"); |
| 90 | vi.spyOn(console, "error").mockImplementation(() => undefined); |
| 91 | persistenceMocks.getClientSettings |
| 92 | .mockRejectedValueOnce(failure) |
| 93 | .mockResolvedValue(savedSettings); |
| 94 | |
| 95 | await expect(persistClientSettingsUpdate(complete)).rejects.toBe(failure); |
| 96 | expect(persistenceMocks.setClientSettings).not.toHaveBeenCalled(); |
| 97 | expect(getClientSettings()).toBe(DEFAULT_CLIENT_SETTINGS); |
| 98 | |
| 99 | const completedSettings = { ...savedSettings, onboardingCompletedAt }; |
| 100 | await expect(persistClientSettingsUpdate(complete)).resolves.toEqual(completedSettings); |
| 101 | expect(persistenceMocks.setClientSettings).toHaveBeenCalledExactlyOnceWith(completedSettings); |
| 102 | expect(persistenceMocks.getClientSettings).toHaveBeenCalledTimes(2); |
| 103 | }); |
| 104 | |
| 105 | it("uses defaults only after storage confirms no saved settings exist", async () => { |
| 106 | const completedSettings = { ...DEFAULT_CLIENT_SETTINGS, onboardingCompletedAt }; |
| 107 | |
| 108 | await expect(persistClientSettingsUpdate(complete)).resolves.toEqual(completedSettings); |
| 109 | expect(persistenceMocks.getClientSettings).toHaveBeenCalledOnce(); |
| 110 | expect(persistenceMocks.setClientSettings).toHaveBeenCalledExactlyOnceWith(completedSettings); |
| 111 | }); |
| 112 | |
| 113 | it("holds patches until a pending read supplies the saved preferences", async () => { |
| 114 | let finishRead!: (settings: ClientSettings) => void; |
| 115 | persistenceMocks.getClientSettings.mockImplementationOnce( |
| 116 | () => |
| 117 | new Promise((resolve) => { |
| 118 | finishRead = resolve; |
| 119 | }), |
| 120 | ); |
| 121 | const persisted = new Promise<ClientSettings>((resolve) => { |
| 122 | persistenceMocks.setClientSettings.mockImplementationOnce(async (settings) => { |
| 123 | resolve(settings); |
| 124 | }); |
| 125 | }); |
| 126 | |
| 127 | const hydration = ensureClientSettingsHydrated(); |
| 128 | persistClientSettingsPatch({ wordWrap: false }); |
| 129 | expect(getClientSettings()).toBe(DEFAULT_CLIENT_SETTINGS); |
| 130 | expect(persistenceMocks.setClientSettings).not.toHaveBeenCalled(); |
| 131 | |
| 132 | finishRead(savedSettings); |
| 133 | await hydration; |
| 134 | await expect(persisted).resolves.toEqual({ ...savedSettings, wordWrap: false }); |
| 135 | expect(persistenceMocks.getClientSettings).toHaveBeenCalledOnce(); |
| 136 | }); |
| 137 | |
| 138 | it("handles failed patch reads without writing and retries with the saved preferences", async () => { |
| 139 | const failure = new Error("storage unavailable"); |
| 140 | vi.spyOn(console, "error").mockImplementation(() => undefined); |
| 141 | persistenceMocks.getClientSettings.mockRejectedValue(failure); |
| 142 | |
| 143 | const hydration = ensureClientSettingsHydrated(); |
| 144 | persistClientSettingsPatch({ wordWrap: false }); |
| 145 | await expect(hydration).rejects.toBe(failure); |
| 146 | expect(persistenceMocks.setClientSettings).not.toHaveBeenCalled(); |
| 147 | |
| 148 | persistenceMocks.getClientSettings.mockResolvedValue(savedSettings); |
| 149 | const persisted = new Promise<ClientSettings>((resolve) => { |
| 150 | persistenceMocks.setClientSettings.mockImplementationOnce(async (settings) => { |
| 151 | resolve(settings); |
| 152 | }); |
| 153 | }); |
| 154 | persistClientSettingsPatch({ wordWrap: false }); |
| 155 | |
| 156 | await expect(persisted).resolves.toEqual({ ...savedSettings, wordWrap: false }); |
| 157 | }); |
| 158 | |
| 159 | it("preserves patch order across hydration and a blocked completion write", async () => { |
| 160 | let finishRead!: (settings: ClientSettings) => void; |
| 161 | const read = new Promise<ClientSettings>((resolve) => { |
| 162 | finishRead = resolve; |
| 163 | }); |
| 164 | persistenceMocks.getClientSettings.mockReturnValue(read); |
| 165 | let finishCompletionWrite!: () => void; |
| 166 | const blockedWrite = new Promise<void>((resolve) => { |
| 167 | finishCompletionWrite = resolve; |
| 168 | }); |
| 169 | let signalCompletionWrite!: () => void; |
| 170 | const completionWriteStarted = new Promise<void>((resolve) => { |
| 171 | signalCompletionWrite = resolve; |
| 172 | }); |
| 173 | let durableSettings: ClientSettings = savedSettings; |
| 174 | const persist = vi |
| 175 | .fn<(settings: ClientSettings) => Promise<void>>() |
| 176 | .mockImplementationOnce(async (settings) => { |
| 177 | signalCompletionWrite(); |
| 178 | await blockedWrite; |
| 179 | durableSettings = settings; |
| 180 | }) |
| 181 | .mockImplementation(async (settings) => { |
| 182 | durableSettings = settings; |
| 183 | }); |
| 184 | |
| 185 | const completion = persistClientSettingsUpdate(complete, persist); |
| 186 | persistClientSettingsPatch({ wordWrap: false }, persist); |
| 187 | finishRead(savedSettings); |
| 188 | await completionWriteStarted; |
| 189 | persistClientSettingsPatch({ wordWrap: true }, persist); |
| 190 | const finalWrite = persistClientSettingsUpdate((current) => current, persist); |
| 191 | |
| 192 | finishCompletionWrite(); |
| 193 | await completion; |
| 194 | await finalWrite; |
| 195 | |
| 196 | const expected = { ...savedSettings, onboardingCompletedAt, wordWrap: true }; |
| 197 | expect(getClientSettings()).toEqual(expected); |
| 198 | expect(durableSettings).toEqual(expected); |
| 199 | }); |
| 200 | }); |
| 201 | |
| 202 | describe("persistClientSettingsPatch", () => { |
| 203 | it("waits for settings writes in request order", async () => { |
| 204 | __setClientSettingsForTests(DEFAULT_CLIENT_SETTINGS); |
| 205 | let finishFirst!: () => void; |
| 206 | let markFirstStarted!: () => void; |
| 207 | const firstStarted = new Promise<void>((resolve) => { |
| 208 | markFirstStarted = resolve; |
| 209 | }); |
| 210 | persistenceMocks.setClientSettings.mockImplementationOnce(() => { |
| 211 | markFirstStarted(); |
| 212 | return new Promise<void>((resolve) => { |
| 213 | finishFirst = resolve; |
| 214 | }); |
| 215 | }); |
| 216 | |
| 217 | const firstSettings = { ...DEFAULT_CLIENT_SETTINGS, snapShotFlash: false }; |
| 218 | const secondSettings = { ...firstSettings, snapShotPlaySound: false }; |
| 219 | const first = persistClientSettingsPatch({ snapShotFlash: false }); |
| 220 | await firstStarted; |
| 221 | const second = persistClientSettingsPatch({ snapShotPlaySound: false }); |
| 222 | |
| 223 | expect(persistenceMocks.setClientSettings).toHaveBeenCalledTimes(1); |
| 224 | expect(getClientSettings()).toEqual(secondSettings); |
| 225 | finishFirst(); |
| 226 | await Promise.all([first, second]); |
| 227 | |
| 228 | expect(persistenceMocks.setClientSettings).toHaveBeenNthCalledWith(1, firstSettings); |
| 229 | expect(persistenceMocks.setClientSettings).toHaveBeenNthCalledWith(2, secondSettings); |
| 230 | }); |
| 231 | }); |
| 232 | |
| 233 | describe("persistClientSettingsUpdate", () => { |
| 234 | it("publishes the update only after persistence succeeds", async () => { |
| 235 | let finishPersistence!: () => void; |
| 236 | const persistence = new Promise<void>((resolve) => { |
| 237 | finishPersistence = resolve; |
| 238 | }); |
| 239 | const setClientSettings = vi.fn(() => persistence); |
| 240 | __setClientSettingsForTests(DEFAULT_CLIENT_SETTINGS); |
| 241 | |
| 242 | const pending = persistClientSettingsUpdate( |
| 243 | (current) => ({ |
| 244 | ...current, |
| 245 | timestampFormat: "12-hour", |
| 246 | }), |
| 247 | setClientSettings, |
| 248 | ); |
| 249 | |
| 250 | expect(getClientSettings().timestampFormat).toBe(DEFAULT_CLIENT_SETTINGS.timestampFormat); |
| 251 | finishPersistence(); |
| 252 | await expect(pending).resolves.toMatchObject({ timestampFormat: "12-hour" }); |
| 253 | expect(getClientSettings().timestampFormat).toBe("12-hour"); |
| 254 | }); |
| 255 | |
| 256 | it("keeps the current snapshot and propagates persistence failure", async () => { |
| 257 | const failure = new Error("disk full"); |
| 258 | const setClientSettings = vi.fn().mockRejectedValue(failure); |
| 259 | __setClientSettingsForTests(DEFAULT_CLIENT_SETTINGS); |
| 260 | |
| 261 | await expect( |
| 262 | persistClientSettingsUpdate( |
| 263 | (current) => ({ ...current, timestampFormat: "12-hour" }), |
| 264 | setClientSettings, |
| 265 | ), |
| 266 | ).rejects.toBe(failure); |
| 267 | expect(getClientSettings()).toBe(DEFAULT_CLIENT_SETTINGS); |
| 268 | }); |
| 269 | |
| 270 | it("preserves an optimistic write made while an awaited update persists", async () => { |
| 271 | let finishFirstPersistence!: () => void; |
| 272 | let durableSettings = DEFAULT_CLIENT_SETTINGS; |
| 273 | const firstPersistence = new Promise<void>((resolve) => { |
| 274 | finishFirstPersistence = resolve; |
| 275 | }); |
| 276 | const persist = vi |
| 277 | .fn<(settings: typeof DEFAULT_CLIENT_SETTINGS) => Promise<void>>() |
| 278 | .mockImplementationOnce((settings) => |
| 279 | firstPersistence.then(() => { |
| 280 | durableSettings = settings; |
| 281 | }), |
| 282 | ) |
| 283 | .mockImplementation(async (settings) => { |
| 284 | durableSettings = settings; |
| 285 | }); |
| 286 | __setClientSettingsForTests(DEFAULT_CLIENT_SETTINGS); |
| 287 | const importedProfile = { id: "profile-import", name: "Imported", kind: "persistent" as const }; |
| 288 | |
| 289 | const pending = persistClientSettingsUpdate( |
| 290 | (current) => ({ |
| 291 | ...current, |
| 292 | browserProfiles: [...current.browserProfiles, importedProfile], |
| 293 | }), |
| 294 | persist, |
| 295 | ); |
| 296 | await Promise.resolve(); |
| 297 | const patch = persistClientSettingsPatch({ wordWrap: false }, persist); |
| 298 | finishFirstPersistence(); |
| 299 | await Promise.all([pending, patch]); |
| 300 | |
| 301 | expect(persist).toHaveBeenCalledTimes(3); |
| 302 | expect(persist.mock.calls[1]?.[0]).toMatchObject({ |
| 303 | wordWrap: false, |
| 304 | }); |
| 305 | expect(persist.mock.calls[1]?.[0].browserProfiles).toContainEqual(importedProfile); |
| 306 | expect(durableSettings.wordWrap).toBe(false); |
| 307 | expect(durableSettings.browserProfiles).toContainEqual(importedProfile); |
| 308 | expect(getClientSettings().wordWrap).toBe(false); |
| 309 | expect(getClientSettings().browserProfiles).toContainEqual(importedProfile); |
| 310 | }); |
| 311 | |
| 312 | it("orders an awaited update after an older optimistic write", async () => { |
| 313 | let finishOldWrite!: () => void; |
| 314 | let durableSettings = DEFAULT_CLIENT_SETTINGS; |
| 315 | const oldWrite = new Promise<void>((resolve) => { |
| 316 | finishOldWrite = resolve; |
| 317 | }); |
| 318 | const persist = vi |
| 319 | .fn<(settings: typeof DEFAULT_CLIENT_SETTINGS) => Promise<void>>() |
| 320 | .mockImplementationOnce((settings) => |
| 321 | oldWrite.then(() => { |
| 322 | durableSettings = settings; |
| 323 | }), |
| 324 | ) |
| 325 | .mockImplementation(async (settings) => { |
| 326 | durableSettings = settings; |
| 327 | }); |
| 328 | __setClientSettingsForTests(DEFAULT_CLIENT_SETTINGS); |
| 329 | |
| 330 | void persistClientSettingsPatch({ wordWrap: false }, persist); |
| 331 | const importedProfile = { id: "profile-import", name: "Imported", kind: "persistent" as const }; |
| 332 | const registration = persistClientSettingsUpdate( |
| 333 | (current) => ({ |
| 334 | ...current, |
| 335 | browserProfiles: [...current.browserProfiles, importedProfile], |
| 336 | }), |
| 337 | persist, |
| 338 | ); |
| 339 | await Promise.resolve(); |
| 340 | expect(persist).toHaveBeenCalledTimes(1); |
| 341 | |
| 342 | finishOldWrite(); |
| 343 | await registration; |
| 344 | |
| 345 | expect(persist).toHaveBeenCalledTimes(2); |
| 346 | expect(durableSettings.wordWrap).toBe(false); |
| 347 | expect(durableSettings.browserProfiles).toContainEqual(importedProfile); |
| 348 | }); |
| 349 | |
| 350 | it("continues the queue after a rejected write", async () => { |
| 351 | const failure = new Error("disk full"); |
| 352 | const persist = vi |
| 353 | .fn<(settings: typeof DEFAULT_CLIENT_SETTINGS) => Promise<void>>() |
| 354 | .mockRejectedValueOnce(failure) |
| 355 | .mockResolvedValue(undefined); |
| 356 | __setClientSettingsForTests(DEFAULT_CLIENT_SETTINGS); |
| 357 | |
| 358 | await expect( |
| 359 | persistClientSettingsUpdate( |
| 360 | (current) => ({ ...current, timestampFormat: "12-hour" }), |
| 361 | persist, |
| 362 | ), |
| 363 | ).rejects.toBe(failure); |
| 364 | await expect( |
| 365 | persistClientSettingsUpdate((current) => ({ ...current, wordWrap: false }), persist), |
| 366 | ).resolves.toMatchObject({ wordWrap: false }); |
| 367 | }); |
| 368 | }); |
| 369 | |
| 370 | describe("resolveEnvironmentIdentificationMode", () => { |
| 371 | it("keeps identification hidden until client settings hydrate", () => { |
| 372 | expect(resolveEnvironmentIdentificationMode({ mode: "artwork", settingsHydrated: false })).toBe( |
| 373 | "none", |
| 374 | ); |
| 375 | expect(resolveEnvironmentIdentificationMode({ mode: "pill", settingsHydrated: true })).toBe( |
| 376 | "pill", |
| 377 | ); |
| 378 | }); |
| 379 | |
| 380 | it("uses a pill instead of artwork with a palette theme", () => { |
| 381 | expect( |
| 382 | resolveEnvironmentIdentificationMode({ |
| 383 | mode: "artwork", |
| 384 | settingsHydrated: true, |
| 385 | paletteThemeActive: true, |
| 386 | }), |
| 387 | ).toBe("pill"); |
| 388 | }); |
| 389 | |
| 390 | it("respects none with a palette theme", () => { |
| 391 | expect( |
| 392 | resolveEnvironmentIdentificationMode({ |
| 393 | mode: "none", |
| 394 | settingsHydrated: true, |
| 395 | paletteThemeActive: true, |
| 396 | }), |
| 397 | ).toBe("none"); |
| 398 | }); |
| 399 | |
| 400 | it("keeps artwork when the palette theme opts into it", () => { |
| 401 | expect( |
| 402 | resolveEnvironmentIdentificationMode({ |
| 403 | mode: "artwork", |
| 404 | settingsHydrated: true, |
| 405 | paletteThemeActive: true, |
| 406 | paletteThemeAllowsArtwork: true, |
| 407 | }), |
| 408 | ).toBe("artwork"); |
| 409 | }); |
| 410 | }); |
| 411 | |
| 412 | describe("mergeEnvironmentSettings", () => { |
| 413 | it("combines the selected environment's server settings with client preferences", () => { |
| 414 | const serverSettings = { |
| 415 | ...DEFAULT_SERVER_SETTINGS, |
| 416 | providerInstances: { |
| 417 | [ProviderInstanceId.make("codex_remote")]: { |
| 418 | driver: ProviderDriverKind.make("codex"), |
| 419 | enabled: true, |
| 420 | }, |
| 421 | }, |
| 422 | }; |
| 423 | const clientSettings = { |
| 424 | ...DEFAULT_CLIENT_SETTINGS, |
| 425 | favorites: [ |
| 426 | { |
| 427 | provider: ProviderInstanceId.make("codex_remote"), |
| 428 | model: "gpt-5.4", |
| 429 | }, |
| 430 | ], |
| 431 | }; |
| 432 | |
| 433 | const settings = mergeEnvironmentSettings(serverSettings, clientSettings); |
| 434 | |
| 435 | expect(settings.providerInstances).toBe(serverSettings.providerInstances); |
| 436 | expect(settings.favorites).toBe(clientSettings.favorites); |
| 437 | }); |
| 438 | |
| 439 | it("keeps server settlement settings when legacy client data contains retired keys", () => { |
| 440 | const serverSettings = { |
| 441 | ...DEFAULT_SERVER_SETTINGS, |
| 442 | sidebarAutoSettleAfterDays: 14, |
| 443 | sidebarAutoSettleOnMerge: false, |
| 444 | }; |
| 445 | const legacyClientSettings = { |
| 446 | ...DEFAULT_CLIENT_SETTINGS, |
| 447 | sidebarAutoSettleAfterDays: 1, |
| 448 | sidebarAutoSettleOnMerge: true, |
| 449 | }; |
| 450 | |
| 451 | const settings = mergeEnvironmentSettings(serverSettings, legacyClientSettings); |
| 452 | |
| 453 | expect(settings.sidebarAutoSettleAfterDays).toBe(14); |
| 454 | expect(settings.sidebarAutoSettleOnMerge).toBe(false); |
| 455 | }); |
| 456 | }); |
| 457 | |
| 458 | describe("onboarding completion persistence", () => { |
| 459 | it("keeps onboarding incomplete after a failed save and preserves preferences on retry", async () => { |
| 460 | const failure = new Error("disk full"); |
| 461 | const persist = vi |
| 462 | .fn<(settings: typeof DEFAULT_CLIENT_SETTINGS) => Promise<void>>() |
| 463 | .mockRejectedValueOnce(failure) |
| 464 | .mockResolvedValue(undefined); |
| 465 | const existingSettings = { |
| 466 | ...DEFAULT_CLIENT_SETTINGS, |
| 467 | timestampFormat: "12-hour" as const, |
| 468 | favorites: [ |
| 469 | { |
| 470 | provider: ProviderInstanceId.make("codex_work"), |
| 471 | model: "gpt-5.6", |
| 472 | }, |
| 473 | ], |
| 474 | }; |
| 475 | __setClientSettingsForTests(existingSettings); |
| 476 | const onboardingCompletedAt = "2026-09-01T12:00:00.000Z"; |
| 477 | const complete = (current: typeof DEFAULT_CLIENT_SETTINGS) => ({ |
| 478 | ...current, |
| 479 | onboardingCompletedAt, |
| 480 | }); |
| 481 | |
| 482 | await expect(persistClientSettingsUpdate(complete, persist)).rejects.toBe(failure); |
| 483 | expect(getClientSettings()).toBe(existingSettings); |
| 484 | expect(getClientSettings().onboardingCompletedAt).toBeNull(); |
| 485 | |
| 486 | const completedSettings = { ...existingSettings, onboardingCompletedAt }; |
| 487 | await expect(persistClientSettingsUpdate(complete, persist)).resolves.toEqual( |
| 488 | completedSettings, |
| 489 | ); |
| 490 | expect(getClientSettings()).toEqual(completedSettings); |
| 491 | expect(persist).toHaveBeenLastCalledWith(completedSettings); |
| 492 | }); |
| 493 | }); |
| 494 | |
| 495 | it("uses the environment's model preferences after migration, including empty lists", () => { |
| 496 | const client = { |
| 497 | ...DEFAULT_CLIENT_SETTINGS, |
| 498 | favorites: [{ provider: ProviderInstanceId.make("codex_work"), model: "old" }], |
| 499 | providerModelPreferences: { codex_work: { hiddenModels: ["old"], modelOrder: [] } }, |
| 500 | }; |
| 501 | expect(mergeEnvironmentSettings(DEFAULT_SERVER_SETTINGS, client).favorites).toEqual( |
| 502 | client.favorites, |
| 503 | ); |
| 504 | const merged = mergeEnvironmentSettings( |
| 505 | { ...DEFAULT_SERVER_SETTINGS, favorites: [], providerModelPreferences: {} }, |
| 506 | client, |
| 507 | ); |
| 508 | expect(merged.favorites).toEqual([]); |
| 509 | expect(merged.providerModelPreferences).toEqual({}); |
| 510 | }); |