import { test } from 'node:test'; import assert from 'node:assert/strict'; import { computeCoverage, mergeSearchResults, putRows, queryRoom, getCoverage, saveRoomIndex, clearRoom, clearAll, deleteSearchCacheDatabase, SearchCacheRow, } from './searchCache'; // --- Pure helpers: mergeSearchResults --------------------------------------- type Item = { event: { event_id: string; origin_server_ts?: number } }; const item = (eventId: string, ts?: number): Item => ({ event: { event_id: eventId, origin_server_ts: ts }, }); test('mergeSearchResults: sorts by origin_server_ts descending', () => { const out = mergeSearchResults([item('$a', 10), item('$b', 30), item('$c', 20)], []); assert.deepEqual( out.map((i) => i.event.event_id), ['$b', '$c', '$a'], ); }); test('mergeSearchResults: dedupes by event_id with in-memory winning', () => { const memory = [{ event: { event_id: '$dup', origin_server_ts: 5 }, tag: 'memory' }]; const cached = [ { event: { event_id: '$dup', origin_server_ts: 5 }, tag: 'cached' }, { event: { event_id: '$only', origin_server_ts: 9 }, tag: 'cached' }, ]; const out = mergeSearchResults(memory, cached); assert.equal(out.length, 2); const dup = out.find((i) => i.event.event_id === '$dup'); assert.equal(dup?.tag, 'memory'); }); test('mergeSearchResults: cached-only hits are included', () => { const out = mergeSearchResults([], [item('$c1', 1), item('$c2', 2)]); assert.equal(out.length, 2); }); test('mergeSearchResults: missing ts sorts as 0 (last)', () => { const out = mergeSearchResults([item('$noTs'), item('$withTs', 100)], []); assert.deepEqual( out.map((i) => i.event.event_id), ['$withTs', '$noTs'], ); }); // --- Pure helpers: computeCoverage ------------------------------------------ const row = (ts: number): Pick => ({ ts }); test('computeCoverage: derives oldest/newest from rows', () => { const cov = computeCoverage('!r', [row(30), row(10), row(20)], 3); assert.deepEqual(cov, { roomId: '!r', oldestTs: 10, newestTs: 30, count: 3 }); }); test('computeCoverage: widens the window against previous coverage', () => { const prev = { roomId: '!r', oldestTs: 5, newestTs: 25, count: 2 }; const cov = computeCoverage('!r', [row(15), row(40)], 4, prev); assert.equal(cov.oldestTs, 5); // previous oldest kept assert.equal(cov.newestTs, 40); // batch newest wins assert.equal(cov.count, 4); // authoritative count from caller }); test('computeCoverage: empty rows with no previous yields zeroed window', () => { const cov = computeCoverage('!r', [], 0); assert.deepEqual(cov, { roomId: '!r', oldestTs: 0, newestTs: 0, count: 0 }); }); // --- IDB round-trip: skip when IndexedDB is unavailable (e.g. node --test) --- const hasIdb = typeof indexedDB !== 'undefined'; test('searchCache IDB round-trip', { skip: !hasIdb }, async () => { await clearAll(); const rows: SearchCacheRow[] = [ { roomId: '!r1', eventId: '$1', ts: 100, sender: '@a', body: 'hello world' }, { roomId: '!r1', eventId: '$2', ts: 200, sender: '@b', body: 'goodbye', formattedBody: 'x', }, { roomId: '!r2', eventId: '$3', ts: 300, sender: '@a', body: 'other room' }, ]; await putRows(rows); const r1 = await queryRoom('!r1'); assert.equal(r1.length, 2); assert.deepEqual(r1.map((x) => x.eventId).sort(), ['$1', '$2']); await saveRoomIndex( '!r1', rows.filter((x) => x.roomId === '!r1'), ); const cov = await getCoverage('!r1'); assert.equal(cov?.count, 2); assert.equal(cov?.oldestTs, 100); assert.equal(cov?.newestTs, 200); await clearRoom('!r1'); assert.equal((await queryRoom('!r1')).length, 0); assert.equal((await queryRoom('!r2')).length, 1); await deleteSearchCacheDatabase(); }); test('resilient helpers never throw when IDB is unavailable', { skip: hasIdb }, async () => { // In this environment IndexedDB is absent; every call must degrade to a // cache-miss rather than throwing. await assert.doesNotReject( putRows([{ roomId: '!r', eventId: '$1', ts: 1, sender: '@a', body: 'x' }]), ); assert.deepEqual(await queryRoom('!r'), []); assert.equal(await getCoverage('!r'), null); await assert.doesNotReject(saveRoomIndex('!r', [])); await assert.doesNotReject(clearRoom('!r')); await assert.doesNotReject(clearAll()); await assert.doesNotReject(deleteSearchCacheDatabase()); });