// @ts-nocheck /** * tests/unit/core/StateStore.test.js * * Story 1.3 — Full unit coverage for StateStore. */ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { StateStore, VISIBILITY_MATRIX_KEY } from '../../../src/core/StateStore.js'; import { VISIBILITY_STATES } from '../../../src/contracts/visibility-matrix.js'; import { STATE_STORE_SNAPSHOTS } from '../../fixtures/state-store-snapshots.js'; // ─── Settings stub ─────────────────────────────────────────────────────────── function makeSettings(initial = null) { let stored = initial; return { get: vi.fn(() => stored), set: vi.fn((key, value) => { stored = value; return Promise.resolve(value); }), _getStored: () => stored, }; } // ─── Hooks stub ────────────────────────────────────────────────────────────── function makeHooksStub() { return { callAll: vi.fn(), on: vi.fn(), once: vi.fn(), off: vi.fn() }; } // ─── Setup ────────────────────────────────────────────────────────────────── function setupHooksGlobal() { globalThis.Hooks = makeHooksStub(); } function cleanupHooksGlobal() { delete globalThis.Hooks; } // ─── constructor ───────────────────────────────────────────────────────────── describe('StateStore constructor', () => { beforeEach(() => { setupHooksGlobal(); }); afterEach(() => { cleanupHooksGlobal(); }); it('creates instance with empty matrix and revision 0', () => { const settings = makeSettings(); const store = new StateStore(settings); expect(store.getMatrix()).toEqual({ _version: 1, matrix: {} }); }); it('throws TypeError if settings is null', () => { expect(() => new StateStore(null)).toThrow(TypeError); }); it('throws TypeError if settings is not an object', () => { expect(() => new StateStore('not-an-object')).toThrow(TypeError); }); }); // ─── init() ────────────────────────────────────────────────────────────────── describe('StateStore.init()', () => { beforeEach(() => { setupHooksGlobal(); }); afterEach(() => { cleanupHooksGlobal(); }); it('hydrates from a valid persisted matrix', () => { const settings = makeSettings(STATE_STORE_SNAPSHOTS.threeParticipants); const store = new StateStore(settings); store.init(); expect(store.getMatrix().matrix).toEqual(STATE_STORE_SNAPSHOTS.threeParticipants.matrix); }); it('starts fresh when settings.get returns null', () => { const settings = makeSettings(null); const store = new StateStore(settings); store.init(); expect(store.getMatrix()).toEqual({ _version: 1, matrix: {} }); }); it('starts fresh and logs warn on corrupt data', () => { const settings = makeSettings({ _version: 1, matrix: { 'user-1': 'INVALID_STATE' } }); const store = new StateStore(settings); const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); store.init(); expect(store.getMatrix()).toEqual({ _version: 1, matrix: {} }); // Warn is called with message and error details expect(warnSpy).toHaveBeenCalled(); expect(warnSpy.mock.calls[0][0]).toContain('[ScryingPool]'); warnSpy.mockRestore(); }); it('is idempotent — second call wins', () => { const settings = makeSettings(STATE_STORE_SNAPSHOTS.threeParticipants); const store = new StateStore(settings); store.init(); // Override stored value settings.get.mockReturnValue(STATE_STORE_SNAPSHOTS.empty); store.init(); expect(store.getMatrix()).toEqual(STATE_STORE_SNAPSHOTS.empty); }); }); // ─── getState() ────────────────────────────────────────────────────────────── describe('StateStore.getState()', () => { beforeEach(() => { setupHooksGlobal(); }); afterEach(() => { cleanupHooksGlobal(); }); it('returns null for unknown userId', () => { const store = new StateStore(makeSettings()); expect(store.getState('no-such-user')).toBeNull(); }); it('returns the current state after setVisibility', () => { const store = new StateStore(makeSettings()); store.setVisibility('user-1', 'active'); expect(store.getState('user-1')).toBe('active'); }); }); // ─── getMatrix() ───────────────────────────────────────────────────────────── describe('StateStore.getMatrix()', () => { beforeEach(() => { setupHooksGlobal(); }); afterEach(() => { cleanupHooksGlobal(); }); it('returns a deep copy — mutations do not affect internal state', () => { const store = new StateStore(makeSettings()); store.setVisibility('user-1', 'active'); const snapshot = store.getMatrix(); snapshot.matrix['user-1'] = 'hidden'; expect(store.getState('user-1')).toBe('active'); // internal state unchanged }); it('includes _version field', () => { const store = new StateStore(makeSettings()); expect(store.getMatrix()._version).toBe(1); }); }); // ─── setVisibility() ───────────────────────────────────────────────────────── describe('StateStore.setVisibility()', () => { beforeEach(() => { setupHooksGlobal(); }); afterEach(() => { cleanupHooksGlobal(); }); it('updates in-memory matrix immediately', () => { const store = new StateStore(makeSettings()); store.setVisibility('user-1', 'hidden'); expect(store.getState('user-1')).toBe('hidden'); }); it('calls settings.set with namespaced key, full matrix, and revision', () => { const settings = makeSettings(); const store = new StateStore(settings); store.setVisibility('user-1', 'active'); expect(settings.set).toHaveBeenCalledWith( VISIBILITY_MATRIX_KEY, { _version: 1, _revision: 1, matrix: { 'user-1': 'active' } }, ); }); it('emits scrying-pool:stateChanged via Hooks.callAll', () => { const store = new StateStore(makeSettings()); store.setVisibility('user-1', 'active'); expect(globalThis.Hooks.callAll).toHaveBeenCalledWith( 'scrying-pool:stateChanged', expect.objectContaining({ userId: 'user-1', state: 'active', revision: 1 }), ); }); it('increments revision on each call', () => { const store = new StateStore(makeSettings()); store.setVisibility('user-1', 'active'); store.setVisibility('user-2', 'hidden'); const event = globalThis.Hooks.callAll.mock.calls[1][1]; expect(event.revision).toBe(2); }); it('provides previousState = null for first-time set', () => { const store = new StateStore(makeSettings()); store.setVisibility('user-1', 'active'); const event = globalThis.Hooks.callAll.mock.calls[0][1]; expect(event.previousState).toBeNull(); }); it('provides previousState = old value on update', () => { const store = new StateStore(makeSettings()); store.setVisibility('user-1', 'active'); store.setVisibility('user-1', 'hidden'); const event = globalThis.Hooks.callAll.mock.calls[1][1]; expect(event.previousState).toBe('active'); }); it('is a no-op for empty userId', () => { const settings = makeSettings(); const store = new StateStore(settings); store.setVisibility('', 'active'); expect(settings.set).not.toHaveBeenCalled(); }); it('is a no-op for non-string userId', () => { const settings = makeSettings(); const store = new StateStore(settings); store.setVisibility(null, 'active'); expect(settings.set).not.toHaveBeenCalled(); }); it('is a no-op for invalid targetState', () => { const settings = makeSettings(); const store = new StateStore(settings); store.setVisibility('user-1', 'NONSENSE_STATE'); expect(settings.set).not.toHaveBeenCalled(); }); it.each(VISIBILITY_STATES)('accepts valid state "%s"', (state) => { const store = new StateStore(makeSettings()); expect(() => store.setVisibility('user-test', state)).not.toThrow(); expect(store.getState('user-test')).toBe(state); }); }); // ─── setMatrix() ───────────────────────────────────────────────────────────── describe('StateStore.setMatrix()', () => { beforeEach(() => { setupHooksGlobal(); }); afterEach(() => { cleanupHooksGlobal(); }); it('replaces the entire matrix from a valid snapshot', () => { const store = new StateStore(makeSettings()); store.setMatrix(STATE_STORE_SNAPSHOTS.threeParticipants); expect(store.getMatrix().matrix).toEqual(STATE_STORE_SNAPSHOTS.threeParticipants.matrix); }); it('persists via settings.set', () => { const settings = makeSettings(); const store = new StateStore(settings); store.setMatrix(STATE_STORE_SNAPSHOTS.threeParticipants); expect(settings.set).toHaveBeenCalledWith( VISIBILITY_MATRIX_KEY, expect.objectContaining({ matrix: STATE_STORE_SNAPSHOTS.threeParticipants.matrix }), ); }); it('emits scrying-pool:stateChanged via Hooks.callAll with full matrix', () => { const store = new StateStore(makeSettings()); store.setMatrix(STATE_STORE_SNAPSHOTS.threeParticipants); expect(globalThis.Hooks.callAll).toHaveBeenCalledWith( 'scrying-pool:stateChanged', expect.objectContaining({ matrix: expect.any(Object), revision: 1 }), ); }); it('throws TypeError on invalid matrix', () => { const store = new StateStore(makeSettings()); expect(() => store.setMatrix({ _version: 1, matrix: { 'user-1': 'INVALID' } })).toThrow(TypeError); }); it('deep-copies input — external mutations do not affect internal state', () => { const store = new StateStore(makeSettings()); const input = { _version: 1, matrix: { 'user-1': 'active' } }; store.setMatrix(input); input.matrix['user-1'] = 'hidden'; expect(store.getState('user-1')).toBe('active'); }); });