d3ro-voice/apps/mobile-rn/__tests__/knowledge-realtime-redteam-r3-20.test.ts
Yun Chan ba9ef9741e fix: red-team round 3 hardening across desktop, mobile, core and server
Batch of red-team r3 fixes that were in the working tree before the
2026-09-28 design overhaul, committed as one unit with their tests.

- desktop main: STT timeouts and sidecar, voice recording store, sync
  (credentials, audio, knowledge reindex, push gates), runtime
  provisioner, update policy, AltGr keybindings, voice-command policy,
  dictionary file codec/limits, meeting transcript condensing and a
  local recording ledger so interrupted-session recovery only closes
  meetings this device recorded (a phone's live meeting is left alone).
- mobile: login CSRF via implicit token callbacks rejected, account
  deletion/retention, durable queue retention, knowledge realtime
  without unfiltered DELETE, meeting re-record failure paths, cloud STT
  client, preferences store/resync.
- core: text chunking splits long unbroken transcripts to fit, template
  field policy, dictionary limits, meeting markdown inline handling.
- server: payple webhook policy and cancellation order scope, meeting
  document generation quota, team RPC null-role guard, unified LLM
  quota in-flight accounting, knowledge chunk vector index, meeting
  re-record failure paths (migrations 20260929*).
- ci: portable/runtime feed gates, update-policy schema, Forgejo file
  delete and alias planning.

Four older tests are updated to the new contracts rather than the old
behavior: token-pair auth callbacks are rejected, knowledge realtime no
longer subscribes to DELETE, long transcript lines are split, and
meeting recovery requires the local recording ledger for empty rows.
2026-09-28 20:45:52 +09:00

166 lines
5.4 KiB
TypeScript

const mockRealtimeOn = jest.fn()
const mockRealtimeSubscribe = jest.fn()
const mockRemoveChannel = jest.fn(async () => 'ok')
const mockRealtimeChannel: Record<string, unknown> = {
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<typeof setInterval>
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' },
])
})
})