// Tests for durable sequencing — monotonic seqId assignment and deduplication. // Uses bun:test. import { describe, it, expect } from 'bun:test'; // ── Replicated seqId counter from agent-store.svelte.ts ───────────────────── function createSeqCounter() { const counters = new Map(); return { next(sessionId: string): number { const current = counters.get(sessionId) ?? 0; const next = current + 1; counters.set(sessionId, next); return next; }, get(sessionId: string): number { return counters.get(sessionId) ?? 0; }, set(sessionId: string, value: number): void { counters.set(sessionId, value); }, }; } // ── Deduplication logic ───────────────────────────────────────────────────── interface RawMsg { msgId: string; seqId: number; content: string; } function deduplicateMessages(messages: RawMsg[]): { deduplicated: RawMsg[]; maxSeqId: number } { const seqIdSet = new Set(); const deduplicated: RawMsg[] = []; let maxSeqId = 0; for (const m of messages) { const sid = m.seqId ?? 0; if (sid > 0 && seqIdSet.has(sid)) continue; if (sid > 0) seqIdSet.add(sid); if (sid > maxSeqId) maxSeqId = sid; deduplicated.push(m); } return { deduplicated, maxSeqId }; } // ── Tests ─────────────────────────────────────────────────────────────────── describe('seqId monotonic assignment', () => { it('starts at 1', () => { const counter = createSeqCounter(); expect(counter.next('s1')).toBe(1); }); it('never decreases', () => { const counter = createSeqCounter(); let prev = 0; for (let i = 0; i < 100; i++) { const next = counter.next('s1'); expect(next).toBeGreaterThan(prev); prev = next; } }); it('each call returns unique value', () => { const counter = createSeqCounter(); const ids = new Set(); for (let i = 0; i < 50; i++) { ids.add(counter.next('s1')); } expect(ids.size).toBe(50); }); it('independent per session', () => { const counter = createSeqCounter(); expect(counter.next('a')).toBe(1); expect(counter.next('b')).toBe(1); expect(counter.next('a')).toBe(2); expect(counter.next('b')).toBe(2); }); }); describe('deduplication', () => { it('removes messages with duplicate seqIds', () => { const messages: RawMsg[] = [ { msgId: '1', seqId: 1, content: 'hello' }, { msgId: '2', seqId: 2, content: 'world' }, { msgId: '3', seqId: 1, content: 'hello-dup' }, // duplicate ]; const { deduplicated } = deduplicateMessages(messages); expect(deduplicated).toHaveLength(2); expect(deduplicated.map(m => m.msgId)).toEqual(['1', '2']); }); it('keeps first occurrence of duplicate seqId', () => { const messages: RawMsg[] = [ { msgId: 'a', seqId: 5, content: 'first' }, { msgId: 'b', seqId: 5, content: 'second' }, ]; const { deduplicated } = deduplicateMessages(messages); expect(deduplicated).toHaveLength(1); expect(deduplicated[0].msgId).toBe('a'); }); it('preserves messages with seqId 0 (unsequenced)', () => { const messages: RawMsg[] = [ { msgId: 'x', seqId: 0, content: 'legacy' }, { msgId: 'y', seqId: 0, content: 'legacy2' }, ]; const { deduplicated } = deduplicateMessages(messages); expect(deduplicated).toHaveLength(2); }); it('returns correct maxSeqId', () => { const messages: RawMsg[] = [ { msgId: '1', seqId: 3, content: 'a' }, { msgId: '2', seqId: 7, content: 'b' }, { msgId: '3', seqId: 5, content: 'c' }, ]; const { maxSeqId } = deduplicateMessages(messages); expect(maxSeqId).toBe(7); }); }); describe('restore resumes from max seqId', () => { it('counter resumes after restoring maxSeqId', () => { const counter = createSeqCounter(); // Simulate: restored messages had max seqId 42 counter.set('session-1', 42); expect(counter.next('session-1')).toBe(43); expect(counter.next('session-1')).toBe(44); }); it('handles empty restore (maxSeqId 0)', () => { const counter = createSeqCounter(); counter.set('session-1', 0); expect(counter.next('session-1')).toBe(1); }); });