const mockRealtimeOn = jest.fn() const mockRealtimeSubscribe = jest.fn() const mockRemoveChannel = jest.fn(async () => 'ok') const mockRealtimeChannel: Record = { on: mockRealtimeOn, subscribe: mockRealtimeSubscribe, } mockRealtimeOn.mockReturnValue(mockRealtimeChannel) mockRealtimeSubscribe.mockReturnValue(mockRealtimeChannel) jest.mock('../src/lib/supabase', () => ({ supabase: { from: jest.fn(), channel: jest.fn(() => mockRealtimeChannel), removeChannel: (...args: unknown[]) => mockRemoveChannel(...(args as [])), }, })) import { type ForegroundStatePort, type IntervalPort, KNOWLEDGE_DELETION_RECONCILE_INTERVAL_MS, knowledgeRealtimeBindings, startForegroundReconciliation, } from '../src/features/knowledge/knowledge-realtime' import { subscribeToKnowledgeDocuments } from '../src/features/knowledge/knowledge-service' const USER_A = '11111111-1111-4111-8111-111111111111' interface FakeForeground extends ForegroundStatePort { set: (active: boolean) => void listenerCount: () => number } function fakeForeground(initial: boolean): FakeForeground { let active = initial const listeners = new Set<(active: boolean) => void>() return { isActive: () => active, onChange: (listener) => { listeners.add(listener) return { remove: () => { listeners.delete(listener) } } }, set: (next) => { active = next for (const listener of listeners) listener(next) }, listenerCount: () => listeners.size, } } interface FakeInterval extends IntervalPort { tick: () => void cleared: () => boolean lastMs: () => number | null } function fakeInterval(): FakeInterval { let callback: (() => void) | null = null let cleared = false let lastMs: number | null = null const handle = 1 as unknown as ReturnType return { set: (cb, ms) => { callback = cb; lastMs = ms; return handle }, clear: () => { cleared = true; callback = null }, tick: () => { callback?.() }, cleared: () => cleared, lastMs: () => lastMs, } } function subscribedBindings(): Array<{ event: string, filter?: string, table: string }> { return mockRealtimeOn.mock.calls.map((call) => call[1] as { event: string filter?: string table: string }) } describe('knowledge realtime policy (redteam r3-20)', () => { beforeEach(() => { mockRealtimeOn.mockClear() mockRealtimeSubscribe.mockClear() mockRemoveChannel.mockClear() }) it('never subscribes to unfiltered DELETE events on knowledge_documents', () => { const subscription = subscribeToKnowledgeDocuments(jest.fn(), jest.fn(), { reconciliation: { foreground: fakeForeground(true), interval: fakeInterval() }, }) const events = subscribedBindings().map((binding) => binding.event) expect(events).not.toContain('DELETE') expect(events).not.toContain('*') expect(events).toEqual(['INSERT', 'UPDATE']) void subscription.unsubscribe() }) it('narrows INSERT/UPDATE to the owner when a user id is supplied', () => { const subscription = subscribeToKnowledgeDocuments(jest.fn(), jest.fn(), { userId: USER_A, reconciliation: { foreground: fakeForeground(true), interval: fakeInterval() }, }) expect(subscribedBindings()).toEqual([ expect.objectContaining({ event: 'INSERT', filter: `user_id=eq.${USER_A}` }), expect.objectContaining({ event: 'UPDATE', filter: `user_id=eq.${USER_A}` }), ]) void subscription.unsubscribe() }) it('rejects a malformed owner id instead of interpolating it into the filter', () => { expect(() => subscribeToKnowledgeDocuments(jest.fn(), jest.fn(), { userId: 'x,user_id=neq.0', })).toThrow() expect(mockRealtimeOn).not.toHaveBeenCalled() }) it('reconciles deletions by foreground polling and stops on unsubscribe', async () => { const onChanged = jest.fn() const foreground = fakeForeground(true) const interval = fakeInterval() const subscription = subscribeToKnowledgeDocuments(onChanged, jest.fn(), { reconciliation: { foreground, interval }, }) expect(interval.lastMs()).toBe(KNOWLEDGE_DELETION_RECONCILE_INTERVAL_MS) interval.tick() expect(onChanged).toHaveBeenCalledTimes(1) await subscription.unsubscribe() expect(interval.cleared()).toBe(true) expect(foreground.listenerCount()).toBe(0) expect(mockRemoveChannel).toHaveBeenCalledWith(mockRealtimeChannel) interval.tick() foreground.set(false) foreground.set(true) expect(onChanged).toHaveBeenCalledTimes(1) }) it('pauses polling in the background and reconciles once on resume', () => { const onReconcile = jest.fn() const foreground = fakeForeground(true) const interval = fakeInterval() const reconciliation = startForegroundReconciliation(onReconcile, { foreground, interval }) foreground.set(false) interval.tick() expect(onReconcile).not.toHaveBeenCalled() foreground.set(true) expect(onReconcile).toHaveBeenCalledTimes(1) foreground.set(true) expect(onReconcile).toHaveBeenCalledTimes(1) interval.tick() expect(onReconcile).toHaveBeenCalledTimes(2) reconciliation.stop() reconciliation.stop() }) it('builds bindings without DELETE and without a filter when no owner is given', () => { expect(knowledgeRealtimeBindings()).toEqual([ { event: 'INSERT', schema: 'public', table: 'knowledge_documents' }, { event: 'UPDATE', schema: 'public', table: 'knowledge_documents' }, ]) }) })