// Checks on the transcript store in store.ts — the part that has to not lose // anything, since a transcript is the one thing here meant to survive the // call. It touches only localStorage and timers, both shimmed below, so it // runs under node once store.ts is bundled: // // npx esbuild src/transcribe/store.ts --format=esm --outfile=/tmp/store.mjs // node src/transcribe/store.test.mjs /tmp/store.mjs const backing = new Map() globalThis.localStorage = { getItem: k => (backing.has(k) ? backing.get(k) : null), setItem: (k, v) => backing.set(k, String(v)), removeItem: k => backing.delete(k) } // Saves are debounced through window.setTimeout; the tests drive flush() // directly rather than waiting on wall-clock time. globalThis.window = {setTimeout: setTimeout, clearTimeout: clearTimeout} const {TranscriptStore, formatTranscript} = await import( process.argv[2] ?? '/tmp/store.mjs' ) let failed = 0 const check = (label, ok, detail = '') => { if (!ok) failed++ console.log(`${ok ? 'ok ' : 'FAIL'} ${label}${detail ? ` — ${detail}` : ''}`) } const open = room => new TranscriptStore(room, () => {}) // 1. A transcript comes back in the room it was made in, and only there. { const a = open('standup') a.append('peer1', 'Ada', 'the build is green') a.append('peer2', 'Grace', 'ship it') a.flush() const reopened = open('standup') check('a transcript survives a rejoin', reopened.items.length === 2) check( 'with speakers and text intact', reopened.items[0].name === 'Ada' && reopened.items[0].text === 'the build is green' && reopened.items[1].name === 'Grace', reopened.items.map(i => `${i.name}: ${i.text}`).join(' | ') ) check('a different room is a different transcript', open('retro').items.length === 0) } // 2. Consecutive results from one speaker join into a paragraph; a change of // speaker starts a new one. { const s = open('merge') s.append('peer1', 'Ada', 'one') s.append('peer1', 'Ada', 'two') s.append('peer2', 'Grace', 'three') check( 'a speaker’s results merge into a paragraph', s.items.length === 2 && s.items[0].text === 'one two', s.items.map(i => i.text).join(' | ') ) } // 3. A restored transcript is never merged into: a new sitting starts its own // paragraph even if the last line was from the same person. { const s = open('sitting') s.append('peer1', 'Ada', 'yesterday') s.flush() const next = open('sitting') next.append('peer1', 'Ada', 'today') check( 'a new sitting starts a new paragraph', next.items.length === 2, next.items.map(i => i.text).join(' | ') ) } // 4. Sequence numbers are unique after a reload — they are React keys. { const s = open('keys') for (let i = 0; i < 5; i++) s.append(`p${i}`, `N${i}`, `line ${i}`) s.flush() const next = open('keys') next.append('px', 'Nx', 'fresh') const seqs = next.items.map(i => i.seq) check('sequence numbers stay unique', new Set(seqs).size === seqs.length) } // 5. Audio seconds accumulate across sittings, since the bill does. { const s = open('cost') s.addAudioSeconds(30) s.append('p', 'N', 'x') s.flush() const next = open('cost') check('audio seconds carry over', next.audioSeconds === 30, `${next.audioSeconds}`) next.addAudioSeconds(15) check('and keep accumulating', next.audioSeconds === 45) } // 6. A long-running room must not grow without bound on disk. Trimming keeps // the recent end, which is the part worth having. { const s = open('marathon') const line = 'x'.repeat(1000) for (let i = 0; i < 400; i++) s.append('p', 'N', `${i} ${line}`) s.flush() const stored = backing.get('commonroom:transcript:marathon') const next = open('marathon') check( 'storage stays bounded', stored.length < 300_000, `${(stored.length / 1000).toFixed(0)} kB for ${(400 * 1000) / 1000} kB of text` ) check('and keeps the most recent lines', next.items.at(-1).text.startsWith('399 ')) check('dropping the oldest', !next.items[0].text.startsWith('0 ')) } // 7. Clearing removes it from disk too, not just from the screen. { const s = open('gone') s.append('p', 'N', 'secret') s.flush() check('written before clearing', backing.has('commonroom:transcript:gone')) s.clear() check('clear empties the panel', s.items.length === 0) check('and the stored copy', !backing.has('commonroom:transcript:gone')) check('and does not come back on rejoin', open('gone').items.length === 0) check('and resets the cost readout', open('gone').audioSeconds === 0) } // 8. Corrupt or foreign storage must not break entering a room. { backing.set('commonroom:transcript:bad', 'not json at all') check('unparseable storage is ignored', open('bad').items.length === 0) backing.set('commonroom:transcript:bad2', JSON.stringify({v: 99, items: [1, 2]})) check('a future version is ignored', open('bad2').items.length === 0) backing.set( 'commonroom:transcript:bad3', JSON.stringify({v: 1, items: [{t: 'kept', ts: 1}, {nope: true}, null]}) ) check('malformed entries are skipped, good ones kept', open('bad3').items.length === 1) } // 9. Storage that throws (private mode, quota) must not take the call down. { const good = globalThis.localStorage.setItem globalThis.localStorage.setItem = () => { throw new Error('QuotaExceededError') } let threw = false try { const s = open('quota') s.append('p', 'N', 'still on screen') s.flush() check('a failing write leaves the transcript on screen', s.items.length === 1) } catch { threw = true } globalThis.localStorage.setItem = good check('a failing write does not throw', !threw) } // 10. The exported text is what lands in the clipboard and the .txt file. { const s = open('format') s.append('p1', 'Ada', 'hello') const out = formatTranscript(s.items) check('formatted output names the speaker', /\] Ada: hello$/.test(out), out) } console.log(failed === 0 ? '\nall checks passed' : `\n${failed} FAILED`) process.exit(failed === 0 ? 0 : 1)