import { test } from 'node:test'; import assert from 'node:assert/strict'; import { OUTBOX_AUTOSEND_MS, OUTBOX_MAX_ENTRIES, OutboxEntry, isRetryableSendError, outboxRetryDelayMs, parseOutbox, planRestore, shouldKeepInOutbox, withEntry, withoutEntry, } from './outbox'; const entry = (txnId: string, ts: number, roomId = '!a:hs'): OutboxEntry => ({ txnId, roomId, threadId: null, type: 'm.room.message', content: { msgtype: 'm.text', body: txnId }, ts, }); test('keeps message-like events, not call signalling or redactions', () => { assert.equal(shouldKeepInOutbox('m.room.message', { body: 'hi' }), true); assert.equal(shouldKeepInOutbox('m.reaction', { 'm.relates_to': { event_id: '$x' } }), true); assert.equal(shouldKeepInOutbox('org.matrix.msc3381.poll.start', {}), true); assert.equal(shouldKeepInOutbox('org.matrix.msc4075.rtc.notification', {}), false); assert.equal(shouldKeepInOutbox('m.call.invite', {}), false); assert.equal(shouldKeepInOutbox('m.room.redaction', {}), false); }); test('skips events that point at another unsent message (local id)', () => { assert.equal( shouldKeepInOutbox('m.reaction', { 'm.relates_to': { event_id: '~!a:hs:m123' } }), false, ); assert.equal( shouldKeepInOutbox('m.room.message', { body: 'reply', 'm.relates_to': { 'm.in_reply_to': { event_id: '~!a:hs:m1' } }, }), false, ); assert.equal( shouldKeepInOutbox('m.room.message', { body: 'reply', 'm.relates_to': { 'm.in_reply_to': { event_id: '$real' } }, }), true, ); }); test('retryable: connection loss, timeouts, rate limits, server errors', () => { const connectionError = new Error('fetch failed'); Object.defineProperty(connectionError, 'name', { value: 'ConnectionError' }); assert.equal(isRetryableSendError(connectionError), true); assert.equal(isRetryableSendError({ httpStatus: 429 }), true); assert.equal(isRetryableSendError({ httpStatus: 408 }), true); assert.equal(isRetryableSendError({ httpStatus: 502 }), true); }); test('not retryable: client errors, consent, unknown or missing errors', () => { assert.equal(isRetryableSendError({ httpStatus: 403, errcode: 'M_FORBIDDEN' }), false); assert.equal(isRetryableSendError({ httpStatus: 403, errcode: 'M_CONSENT_NOT_GIVEN' }), false); assert.equal(isRetryableSendError({ httpStatus: 400 }), false); assert.equal(isRetryableSendError(new Error('encryption failed')), false); assert.equal(isRetryableSendError(undefined), false); assert.equal(isRetryableSendError(null), false); }); test('parse: only this user, only well-formed entries, junk tolerated', () => { const good = entry('m1', 1); const raw = JSON.stringify({ userId: '@me:hs', entries: [good, { txnId: 5 }, null] }); assert.deepEqual(parseOutbox(raw, '@me:hs'), [good]); assert.deepEqual(parseOutbox(raw, '@other:hs'), []); assert.deepEqual(parseOutbox('{not json', '@me:hs'), []); assert.deepEqual(parseOutbox(null, '@me:hs'), []); }); test('withEntry: first write wins, sorted, capped (oldest dropped)', () => { const a = entry('a', 2); let list = withEntry([], a); assert.equal(withEntry(list, { ...a, content: { body: 'changed' } }), list); list = withEntry(list, entry('b', 1)); assert.deepEqual( list.map((e) => e.txnId), ['b', 'a'], ); let many: OutboxEntry[] = []; for (let i = 0; i < OUTBOX_MAX_ENTRIES + 5; i += 1) many = withEntry(many, entry(`t${i}`, i)); assert.equal(many.length, OUTBOX_MAX_ENTRIES); assert.equal(many[0].txnId, 't5'); }); test('withoutEntry: removes, and returns the same list when absent', () => { const list = [entry('a', 1), entry('b', 2)]; assert.deepEqual( withoutEntry(list, 'a').map((e) => e.txnId), ['b'], ); assert.equal(withoutEntry(list, 'zzz'), list); }); test('restore plan: drops delivered / left rooms, auto-sends only recent ones', () => { const now = 10 * OUTBOX_AUTOSEND_MS; const recent = entry('recent', now - 60_000); const old = entry('old', now - OUTBOX_AUTOSEND_MS - 1); const delivered = entry('delivered', now - 1000); const left = entry('left', now - 1000, '!left:hs'); const plan = planRestore( [recent, left, old, delivered], now, (roomId) => roomId !== '!left:hs', (e) => e.txnId === 'delivered', ); assert.deepEqual( plan.restore.map((e) => e.txnId), ['old', 'recent'], ); assert.deepEqual([...plan.autoSend], ['recent']); assert.deepEqual(plan.drop.map((e) => e.txnId).sort(), ['delivered', 'left']); }); test('blip retry delay backs off and is capped at a minute', () => { assert.deepEqual( [0, 1, 2, 3, 4, 9].map(outboxRetryDelayMs), [5000, 10000, 20000, 40000, 60000, 60000], ); });