/** * Tests for the child-side hook store view (hook-store-proxy). * * The load-bearing behavior is the transaction buffer: the hook's callback * runs against a buffering view, and NOTHING crosses the transport until the * callback resolves. A hook that writes then throws must send no frame at all * — that is what makes D4's discard real rather than a promise the broker * keeps. These tests watch the transport, which stands in for the socket. */ import { describe, expect, it } from 'bun:test'; import { type StoreTransport, buildStoreView } from './hook-store-proxy'; /** Record every transport call; answer `get` from a scripted map. */ function fakeTransport(scripted: Record = {}) { const calls: Array<{ store: string; method: string; args: unknown[] }> = []; const transport: StoreTransport = (store, method, args) => { calls.push({ store, method, args }); if (method === 'get') { return Promise.resolve(scripted[args[0] as string]); } return Promise.resolve(null); }; return { calls, transport }; } describe('hook store view (child side)', () => { it('keeps the map surface readable alongside the accessor methods', () => { const { transport } = fakeTransport({ bot_token: 'v' }); const view = buildStoreView('secrets', { bot_token: 'v' }, transport); expect(view.bot_token).toBe('v'); expect(view.get('bot_token')).resolves.toBe('v'); }); it('forwards set/get/delete to the transport with the store name', async () => { const { calls, transport } = fakeTransport(); const view = buildStoreView('config', {}, transport); await view.set('public_ip', '203.0.113.7'); await view.delete('public_ip'); expect(calls).toEqual([ { store: 'config', method: 'set', args: ['public_ip', '203.0.113.7'] }, { store: 'config', method: 'delete', args: ['public_ip'] }, ]); }); it('transaction sends one frame with the buffered ops after fn resolves', async () => { const { calls, transport } = fakeTransport(); const view = buildStoreView('secrets', {}, transport); await view.transaction((s) => { s.set('bot_token', 'a'); s.set('api_key', 'b'); s.delete('old_key'); }); expect(calls).toEqual([ { store: 'secrets', method: 'transaction', args: [ [ { op: 'set', name: 'bot_token', value: 'a' }, { op: 'set', name: 'api_key', value: 'b' }, { op: 'delete', name: 'old_key' }, ], ], }, ]); }); it('a throwing fn sends NOTHING — the discard is on the wire', async () => { const { calls, transport } = fakeTransport(); const view = buildStoreView('secrets', {}, transport); await expect( view.transaction(async (s) => { s.set('bot_token', 'doomed'); throw new Error('hook failed midway'); }), ).rejects.toThrow('hook failed midway'); expect(calls).toEqual([]); }); it('get inside a transaction reads committed state, not the buffer', async () => { const { transport } = fakeTransport({ bot_token: 'committed' }); const view = buildStoreView('secrets', {}, transport); let seenInside: string | undefined; await view.transaction(async (s) => { s.set('bot_token', 'buffered'); seenInside = await s.get('bot_token'); }); expect(seenInside).toBe('committed'); }); it('a nested transaction rejects instead of reordering writes', async () => { const { calls, transport } = fakeTransport(); const view = buildStoreView('secrets', {}, transport); await expect(view.transaction((s) => s.transaction(() => Promise.resolve()))).rejects.toThrow( /nested|transaction/i, ); expect(calls).toEqual([]); }); });