Files
opsxshell/test/stores/settingsPersistence.test.ts

72 lines
3.3 KiB
TypeScript

import { beforeEach, describe, expect, it, vi } from 'vitest'
function deferred<T>() {
let resolve!: (value: T) => void
const promise = new Promise<T>((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 })
)
})
})