import assert from 'node:assert/strict'; import { readFileSync } from 'node:fs'; import { createRequire } from 'node:module'; import { setImmediate } from 'node:timers/promises'; import test from 'node:test'; const requireWeb = createRequire(new URL('../../apps/web/package.json', import.meta.url)); const ts = requireWeb('typescript'); const source = readFileSync(new URL('../../apps/web/src/projects/store.ts', import.meta.url), 'utf8'); const compiled = ts.transpileModule(source, { compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2022, } }).outputText; function deferred() { let resolve, reject; const promise = new Promise((ok, fail) => { resolve = ok; reject = fail; }); return { promise, resolve, reject }; } function loadStore(putSessionMessages, fetchSessionMessages = async () => ({ messages: [] })) { const exported = {}; new Function('require', 'exports', compiled)((id) => { if (id === '../api/client') return { putSessionMessages, fetchSessionMessages }; if (id === './sessionTitle') return {}; throw new Error(`Unexpected dependency: ${id}`); }, exported); return exported; } test('delayed old PUT completes before the confirmed snapshot can persist', async () => { const oldResponse = deferred(); const requests = []; let persisted; const store = loadStore(async (_id, messages) => { requests.push(messages); if (requests.length === 1) await oldResponse.promise; persisted = messages; }); store.saveMessages('session', [{ confirmHandled: false }]); await setImmediate(); const confirmed = store.persistMessages('session', [{ confirmHandled: true }]); await setImmediate(); assert.equal(requests.length, 1); oldResponse.resolve(); await confirmed; assert.equal(requests.length, 2); assert.deepEqual(persisted, [{ confirmHandled: true }]); }); test('queued payload is a call-time snapshot including nested fields', async () => { const gate = deferred(); const requests = []; const store = loadStore(async (_id, messages) => { requests.push(messages); if (requests.length === 1) await gate.promise; }); const first = store.persistMessages('session', [{ text: 'old' }]); const messages = [{ block: { handled: true } }]; const second = store.persistMessages('session', messages); messages[0].block.handled = false; messages.push({ text: 'changed after enqueue' }); gate.resolve(); await Promise.all([first, second]); assert.deepEqual(requests[1], [{ block: { handled: true } }]); }); test('a failed PUT rejects its caller but does not block the next write', async () => { const gate = deferred(); const requests = []; const store = loadStore(async (_id, messages) => { requests.push(messages); if (requests.length === 1) await gate.promise; }); const first = store.persistMessages('session', [{ text: 'old' }]); const failed = assert.rejects(first, /network/); const second = store.persistMessages('session', [{ text: 'retry' }]); await setImmediate(); gate.reject(new Error('network')); await failed; await second; assert.deepEqual(requests[1], [{ text: 'retry' }]); }); test('different sessions do not wait for each other', async () => { const gate = deferred(); const written = []; const store = loadStore(async (id) => { if (id === 'slow') await gate.promise; written.push(id); }); const slow = store.persistMessages('slow', []); await store.persistMessages('fast', []); assert.deepEqual(written, ['fast']); gate.resolve(); await slow; assert.deepEqual(written, ['fast', 'slow']); }); test('clearMessages follows earlier saves through the same session queue', async () => { const gate = deferred(); const cleared = deferred(); const requests = []; let persisted; const store = loadStore(async (_id, messages) => { requests.push(messages); if (requests.length === 1) await gate.promise; persisted = messages; if (requests.length === 2) cleared.resolve(); }); store.saveMessages('session', [{ text: 'old' }]); store.clearMessages('session'); await setImmediate(); assert.equal(requests.length, 1); gate.resolve(); await cleared.promise; assert.deepEqual(persisted, []); }); test('history load waits for both a delayed old PUT and the queued confirmation', async () => { const gate = deferred(); let written = [], writeCount = 0, readCount = 0; const store = loadStore(async (_id, messages) => { writeCount += 1; if (writeCount === 1) await gate.promise; written = messages; }, async () => { readCount += 1; return { messages: written }; }); store.saveMessages('session', [{ confirmHandled: false }]); await setImmediate(); const confirmed = store.persistMessages('session', [{ confirmHandled: true }]); const loaded = store.loadMessagesPreferServer('session'); await setImmediate(); assert.equal(readCount, 0); gate.resolve(); await confirmed; assert.deepEqual(await loaded, [{ confirmHandled: true }]); assert.equal(readCount, 1); }); test('history GET overlapping a completed write is discarded and read again', async () => { const oldGet = deferred(); let written = [{ confirmHandled: false }], readCount = 0; const store = loadStore(async (_id, messages) => { written = messages; }, async () => { readCount += 1; const snapshot = structuredClone(written); if (readCount === 1) await oldGet.promise; return { messages: snapshot }; }); const loaded = store.loadMessagesPreferServer('session'); await setImmediate(); assert.equal(readCount, 1); await store.persistMessages('session', [{ confirmHandled: true }]); oldGet.resolve(); assert.deepEqual(await loaded, [{ confirmHandled: true }]); assert.equal(readCount, 2); }); test('history read errors remain null instead of an empty conversation', async () => { const store = loadStore(async () => {}, async () => { throw new Error('network'); }); assert.equal(await store.loadMessagesPreferServer('session'), null); });