import { describe, it, expect, vi } from 'vitest'; import { RecoveryStateStore, extractErrorCode, type RecoveryState } from './recovery'; describe('RecoveryStateStore', () => { it('returns the initial state', () => { const store = new RecoveryStateStore(); const s = store.get(); expect(s.mode).toBe('normal'); expect(s.reason).toBeNull(); expect(s.updateState).toBe('idle'); expect(s.updateVersion).toBeNull(); expect(s.lastUpdateError).toBeNull(); expect(s.lastCheckResult).toBeNull(); }); it('shallow-merges partial updates', () => { const store = new RecoveryStateStore(); store.update({ updateState: 'checking' }); expect(store.get().updateState).toBe('checking'); expect(store.get().mode).toBe('normal'); store.update({ updateVersion: '1.2.3' }); expect(store.get().updateState).toBe('checking'); expect(store.get().updateVersion).toBe('1.2.3'); }); it('fires listeners on every update', () => { const store = new RecoveryStateStore(); const cb = vi.fn(); store.subscribe(cb); store.update({ updateState: 'checking' }); store.update({ updateState: 'idle' }); expect(cb).toHaveBeenCalledTimes(2); }); it('subscribers receive the latest snapshot', () => { const store = new RecoveryStateStore(); let received: RecoveryState | null = null; store.subscribe((s) => { received = s; }); store.update({ updateState: 'downloaded', updateVersion: '2.0.0' }); expect(received).not.toBeNull(); expect(received!.updateState).toBe('downloaded'); expect(received!.updateVersion).toBe('2.0.0'); }); it('unsubscribe stops callbacks', () => { const store = new RecoveryStateStore(); const cb = vi.fn(); const unsub = store.subscribe(cb); store.update({ updateState: 'checking' }); unsub(); store.update({ updateState: 'idle' }); expect(cb).toHaveBeenCalledTimes(1); }); it('multiple subscribers all receive updates', () => { const store = new RecoveryStateStore(); const a = vi.fn(); const b = vi.fn(); store.subscribe(a); store.subscribe(b); store.update({ updateState: 'checking' }); expect(a).toHaveBeenCalledTimes(1); expect(b).toHaveBeenCalledTimes(1); }); it('isInRecoveryMode is false initially, true after markRecoveryEntered, false after markRecoveryExited', () => { const store = new RecoveryStateStore(); expect(store.isInRecoveryMode()).toBe(false); store.markRecoveryEntered(); expect(store.isInRecoveryMode()).toBe(true); store.markRecoveryExited(); expect(store.isInRecoveryMode()).toBe(false); }); it('a throwing listener does not stop other listeners', () => { const store = new RecoveryStateStore(); const errSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); const a = vi.fn(() => { throw new Error('boom'); }); const b = vi.fn(); store.subscribe(a); store.subscribe(b); expect(() => store.update({ updateState: 'checking' })).not.toThrow(); expect(a).toHaveBeenCalledTimes(1); expect(b).toHaveBeenCalledTimes(1); errSpy.mockRestore(); }); it('a listener can unsubscribe itself during notification without breaking the pass', () => { const store = new RecoveryStateStore(); const b = vi.fn(); let unsubA: (() => void) | null = null; unsubA = store.subscribe(() => { unsubA?.(); }); store.subscribe(b); expect(() => store.update({ updateState: 'checking' })).not.toThrow(); // Second update: the self-unsubscribed listener should be gone, b still fires store.update({ updateState: 'idle' }); expect(b).toHaveBeenCalledTimes(2); }); it('returned state is frozen — accidental external mutation throws in strict mode', () => { const store = new RecoveryStateStore(); store.update({ updateState: 'checking' }); const s = store.get(); expect(Object.isFrozen(s)).toBe(true); }); }); describe('extractErrorCode', () => { it('extracts string code from Error-like object', () => { const err = new Error('boom') as Error & { code: string }; err.code = 'ENOSPC'; expect(extractErrorCode(err)).toBe('ENOSPC'); }); it('returns null for Error without code', () => { expect(extractErrorCode(new Error('boom'))).toBeNull(); }); it('returns null when code is non-string', () => { const err = { code: 42 }; expect(extractErrorCode(err)).toBeNull(); }); it('returns null for null/undefined/primitives', () => { expect(extractErrorCode(null)).toBeNull(); expect(extractErrorCode(undefined)).toBeNull(); expect(extractErrorCode('string')).toBeNull(); expect(extractErrorCode(42)).toBeNull(); }); it('handles plain objects with code', () => { expect(extractErrorCode({ code: 'NET_FAIL' })).toBe('NET_FAIL'); }); });