import { describe, test, expect, beforeEach } from 'bun:test' import { EventBus, getEventBus, removeEventBus, getAllEventBuses, } from '../transport/event-bus' describe('EventBus', () => { let bus: EventBus beforeEach(() => { bus = new EventBus() }) describe('publish', () => { test('publishes event with seqNum starting at 1', () => { const event = bus.publish({ id: 'e1', sessionId: 's1', type: 'user', payload: { content: 'hello' }, direction: 'outbound', }) expect(event.seqNum).toBe(1) expect(event.createdAt).toBeGreaterThan(0) }) test('increments seqNum on each publish', () => { bus.publish({ id: 'e1', sessionId: 's1', type: 'user', payload: {}, direction: 'outbound', }) bus.publish({ id: 'e2', sessionId: 's1', type: 'assistant', payload: {}, direction: 'inbound', }) const event = bus.publish({ id: 'e3', sessionId: 's1', type: 'result', payload: {}, direction: 'inbound', }) expect(event.seqNum).toBe(3) }) test('throws when publishing to a closed bus', () => { bus.close() expect(() => bus.publish({ id: 'e1', sessionId: 's1', type: 'user', payload: {}, direction: 'outbound', }), ).toThrow('EventBus is closed') }) }) describe('subscribe', () => { test('receives published events', () => { const received: unknown[] = [] bus.subscribe(event => received.push(event)) bus.publish({ id: 'e1', sessionId: 's1', type: 'user', payload: { content: 'hi' }, direction: 'outbound', }) expect(received).toHaveLength(1) expect((received[0] as any).payload).toEqual({ content: 'hi' }) }) test('unsubscribe stops receiving events', () => { const received: unknown[] = [] const unsub = bus.subscribe(event => received.push(event)) unsub() bus.publish({ id: 'e1', sessionId: 's1', type: 'user', payload: {}, direction: 'outbound', }) expect(received).toHaveLength(0) }) test('multiple subscribers all receive events', () => { const r1: unknown[] = [] const r2: unknown[] = [] bus.subscribe(e => r1.push(e)) bus.subscribe(e => r2.push(e)) bus.publish({ id: 'e1', sessionId: 's1', type: 'user', payload: {}, direction: 'outbound', }) expect(r1).toHaveLength(1) expect(r2).toHaveLength(1) }) test('subscriber error does not affect other subscribers', () => { const received: unknown[] = [] bus.subscribe(() => { throw new Error('boom') }) bus.subscribe(e => received.push(e)) bus.publish({ id: 'e1', sessionId: 's1', type: 'user', payload: {}, direction: 'outbound', }) expect(received).toHaveLength(1) }) test('subscriberCount', () => { expect(bus.subscriberCount()).toBe(0) const unsub1 = bus.subscribe(() => {}) expect(bus.subscriberCount()).toBe(1) const unsub2 = bus.subscribe(() => {}) expect(bus.subscriberCount()).toBe(2) unsub1() expect(bus.subscriberCount()).toBe(1) }) }) describe('getEventsSince', () => { test('returns events after given seqNum', () => { bus.publish({ id: 'e1', sessionId: 's1', type: 'user', payload: {}, direction: 'outbound', }) bus.publish({ id: 'e2', sessionId: 's1', type: 'assistant', payload: {}, direction: 'inbound', }) bus.publish({ id: 'e3', sessionId: 's1', type: 'result', payload: {}, direction: 'inbound', }) const events = bus.getEventsSince(1) expect(events).toHaveLength(2) expect(events[0].seqNum).toBe(2) expect(events[1].seqNum).toBe(3) }) test('returns empty for seqNum beyond last', () => { bus.publish({ id: 'e1', sessionId: 's1', type: 'user', payload: {}, direction: 'outbound', }) expect(bus.getEventsSince(1)).toHaveLength(0) }) test('returns all events when seqNum is 0', () => { bus.publish({ id: 'e1', sessionId: 's1', type: 'user', payload: {}, direction: 'outbound', }) bus.publish({ id: 'e2', sessionId: 's1', type: 'assistant', payload: {}, direction: 'inbound', }) expect(bus.getEventsSince(0)).toHaveLength(2) }) }) describe('getLastSeqNum', () => { test('returns 0 for empty bus', () => { expect(bus.getLastSeqNum()).toBe(0) }) test('returns last seqNum after publishes', () => { bus.publish({ id: 'e1', sessionId: 's1', type: 'user', payload: {}, direction: 'outbound', }) bus.publish({ id: 'e2', sessionId: 's1', type: 'user', payload: {}, direction: 'outbound', }) expect(bus.getLastSeqNum()).toBe(2) }) }) describe('close', () => { test('clears subscribers and prevents publishing', () => { bus.subscribe(() => {}) bus.close() expect(bus.subscriberCount()).toBe(0) expect(() => bus.publish({ id: 'e1', sessionId: 's1', type: 'user', payload: {}, direction: 'outbound', }), ).toThrow() }) }) describe('hasEventWithUuid', () => { test('returns true for a published message uuid', () => { bus.publish({ id: 'e1', sessionId: 's1', type: 'assistant', payload: { uuid: 'u-1', content: 'hello' }, direction: 'inbound', }) expect(bus.hasEventWithUuid('u-1')).toBe(true) expect(bus.hasEventWithUuid('u-2')).toBe(false) }) test('retains uuid dedup state after event-window eviction', () => { // Long sessions evict old events from the retained window — the uuid // set must survive so reconnect replays can't double-publish. const uuid = 'old-uuid' bus.publish({ id: 'e0', sessionId: 's1', type: 'assistant', payload: { uuid, content: 'old' }, direction: 'inbound', }) // Push enough events past the retention limit to evict e0 for (let i = 1; i <= 5001; i++) { bus.publish({ id: `e${i}`, sessionId: 's1', type: 'assistant', payload: { uuid: `filler-${i}`, content: 'x' }, direction: 'inbound', }) } // The old event is gone from the retained window… const retained = bus.getEventsSince(0) expect(retained.some(e => e.id === 'e0')).toBe(false) // …but its uuid is still known expect(bus.hasEventWithUuid(uuid)).toBe(true) expect(bus.hasEventWithUuid('filler-5001')).toBe(true) }) test('uuid dedup ring is bounded — oldest uuids eventually evicted', () => { // 20001 publishes exceed MAX_SEEN_UUIDS_PER_BUS → the first uuid falls // out of the ring (proves memory stays bounded on very long sessions). const firstUuid = 'first-uuid' bus.publish({ id: 'e0', sessionId: 's1', type: 'assistant', payload: { uuid: firstUuid }, direction: 'inbound', }) for (let i = 1; i <= 20001; i++) { bus.publish({ id: `e${i}`, sessionId: 's1', type: 'assistant', payload: { uuid: `uuid-${i}` }, direction: 'inbound', }) } expect(bus.hasEventWithUuid(firstUuid)).toBe(false) expect(bus.hasEventWithUuid('uuid-20001')).toBe(true) }) test('ignores payloads without a string uuid', () => { bus.publish({ id: 'e1', sessionId: 's1', type: 'assistant', payload: { content: 'no uuid here' }, direction: 'inbound', }) expect(bus.hasEventWithUuid('')).toBe(false) }) test('close clears uuid dedup state', () => { bus.publish({ id: 'e1', sessionId: 's1', type: 'assistant', payload: { uuid: 'u-1' }, direction: 'inbound', }) expect(bus.hasEventWithUuid('u-1')).toBe(true) bus.close() expect(bus.hasEventWithUuid('u-1')).toBe(false) }) }) }) describe('EventBus registry', () => { beforeEach(() => { // Clean up global registry for (const [key] of getAllEventBuses()) { removeEventBus(key) } }) describe('getEventBus', () => { test('creates new bus for unknown session', () => { const bus = getEventBus('s1') expect(bus).toBeInstanceOf(EventBus) expect(getAllEventBuses().has('s1')).toBe(true) }) test('returns same bus for same session', () => { const bus1 = getEventBus('s1') const bus2 = getEventBus('s1') expect(bus1).toBe(bus2) }) }) describe('removeEventBus', () => { test('removes and closes bus', () => { const bus = getEventBus('s2') removeEventBus('s2') expect(getAllEventBuses().has('s2')).toBe(false) expect(() => bus.publish({ id: 'e1', sessionId: 's2', type: 'user', payload: {}, direction: 'outbound', }), ).toThrow() }) test('no-op for non-existent bus', () => { expect(() => removeEventBus('nonexistent')).not.toThrow() }) }) describe('getAllEventBuses', () => { test('returns all registered buses', () => { getEventBus('a') getEventBus('b') expect(getAllEventBuses().size).toBeGreaterThanOrEqual(2) }) }) })