import { beforeEach, describe, expect, it, vi } from 'vitest' function deferred() { let resolve!: (value: T) => void const promise = new Promise((done) => { resolve = done }) return { promise, resolve } } let store: typeof import('../../src/stores/settingsStore').useSettingsStore beforeEach(async () => { vi.resetModules() vi.mocked(window.opsxApi.settingsGet).mockReset().mockResolvedValue({ success: true }) vi.mocked(window.opsxApi.settingsSet).mockReset().mockResolvedValue({ success: true }) store = (await import('../../src/stores/settingsStore')).useSettingsStore }) describe('settings persistence', () => { it('serializes writes while keeping later edits visible', async () => { const first = deferred<{ success: boolean }>() vi.mocked(window.opsxApi.settingsSet).mockImplementationOnce(() => first.promise) const saveTheme = store.getState().setSetting('theme', 'light') const saveFont = store.getState().setSetting('fontSize', 20) expect(store.getState().settings).toMatchObject({ theme: 'light', fontSize: 20 }) await Promise.resolve() expect(window.opsxApi.settingsSet).toHaveBeenCalledTimes(1) first.resolve({ success: true }) await Promise.all([saveTheme, saveFont]) expect(window.opsxApi.settingsSet).toHaveBeenLastCalledWith( 'app-settings', expect.objectContaining({ theme: 'light', fontSize: 20 }) ) }) it('rolls back a rejected envelope without persisting the failed patch in the next write', async () => { vi.mocked(window.opsxApi.settingsSet).mockResolvedValueOnce({ success: false, error: 'disk full' }) await Promise.all([store.getState().setSetting('theme', 'light'), store.getState().setSetting('fontSize', 20)]) expect(store.getState().settings).toMatchObject({ theme: 'dark', fontSize: 20 }) expect(window.opsxApi.settingsSet).toHaveBeenLastCalledWith( 'app-settings', expect.objectContaining({ theme: 'dark', fontSize: 20 }) ) const { useToastStore } = await import('../../src/stores/toastStore') expect(useToastStore.getState().toasts.at(-1)).toMatchObject({ type: 'error', message: expect.stringContaining('disk full'), }) }) it('continues saving after a thrown IPC failure, including edits to the same field', async () => { vi.mocked(window.opsxApi.settingsSet).mockRejectedValueOnce(new Error('IPC unavailable')) await Promise.all([store.getState().setSetting('fontSize', 18), store.getState().setSetting('fontSize', 22)]) expect(store.getState().settings.fontSize).toBe(22) expect(window.opsxApi.settingsSet).toHaveBeenCalledTimes(2) }) it('merges edits made while initial settings are loading into the persisted snapshot', async () => { const read = deferred<{ success: boolean; value: unknown }>() vi.mocked(window.opsxApi.settingsGet).mockReturnValueOnce(read.promise) const load = store.getState().loadSettings() const save = store.getState().setSetting('fontSize', 22) read.resolve({ success: true, value: { theme: 'light', agentSafetyMode: 'relaxed' } }) await Promise.all([load, save]) expect(store.getState().settings).toMatchObject({ theme: 'light', fontSize: 22 }) expect(window.opsxApi.settingsSet).toHaveBeenLastCalledWith( 'app-settings', expect.objectContaining({ theme: 'light', fontSize: 22 }) ) }) })