From 82428d7bf91c1f750dfa85c152599d96a128657a Mon Sep 17 00:00:00 2001 From: saravanakumardb1 Date: Mon, 13 Apr 2026 11:42:14 -0700 Subject: [PATCH] fix(backend): fix sync throw isolation in event bus + add 6 tests Promise.allSettled only catches rejected promises, not synchronous throws. Wrap handler calls in Promise.resolve().then() to isolate sync errors. Add 6 unit tests covering delivery, unsubscribe, error isolation, singleton, reset, and removeAll. --- backend/src/lib/event-bus.test.ts | 51 +++++++++++++++++++++++++++++++ backend/src/lib/event-bus.ts | 2 +- 2 files changed, 52 insertions(+), 1 deletion(-) create mode 100644 backend/src/lib/event-bus.test.ts diff --git a/backend/src/lib/event-bus.test.ts b/backend/src/lib/event-bus.test.ts new file mode 100644 index 0000000..dda5349 --- /dev/null +++ b/backend/src/lib/event-bus.test.ts @@ -0,0 +1,51 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { getEventBus, _resetEventBus } from './event-bus.js'; + +describe('DomainEventBus', () => { + beforeEach(() => { _resetEventBus(); }); + + it('delivers event to subscriber', async () => { + const bus = getEventBus(); + const handler = vi.fn(); + bus.on('timer.created', handler); + await bus.emit('timer.created', { timerId: 't1', userId: 'u1', label: 'Test' }); + expect(handler).toHaveBeenCalledOnce(); + }); + + it('unsubscribe stops delivery', async () => { + const bus = getEventBus(); + const handler = vi.fn(); + const unsub = bus.on('timer.created', handler); + unsub(); + await bus.emit('timer.created', { timerId: 't1', userId: 'u1', label: 'Test' }); + expect(handler).not.toHaveBeenCalled(); + }); + + it('failing handler does not block others', async () => { + const bus = getEventBus(); + const good = vi.fn(); + bus.on('timer.created', () => { throw new Error('boom'); }); + bus.on('timer.created', good); + await bus.emit('timer.created', { timerId: 't1', userId: 'u1', label: 'Test' }); + expect(good).toHaveBeenCalledOnce(); + }); + + it('singleton returns same instance', () => { + expect(getEventBus()).toBe(getEventBus()); + }); + + it('_resetEventBus creates fresh instance', () => { + const a = getEventBus(); + _resetEventBus(); + expect(getEventBus()).not.toBe(a); + }); + + it('removeAll clears handlers', async () => { + const bus = getEventBus(); + const handler = vi.fn(); + bus.on('timer.created', handler); + bus.removeAll(); + await bus.emit('timer.created', { timerId: 't1', userId: 'u1', label: 'Test' }); + expect(handler).not.toHaveBeenCalled(); + }); +}); diff --git a/backend/src/lib/event-bus.ts b/backend/src/lib/event-bus.ts index c0a615a..0026f8d 100644 --- a/backend/src/lib/event-bus.ts +++ b/backend/src/lib/event-bus.ts @@ -35,7 +35,7 @@ class DomainEventBus { async emit(event: K, payload: ChronoMindEventMap[K]): Promise { const fns = this.handlers.get(event); if (!fns || fns.size === 0) return; - await Promise.allSettled([...fns].map(fn => fn(payload))); + await Promise.allSettled([...fns].map(fn => Promise.resolve().then(() => fn(payload)))); } removeAll(): void { this.handlers.clear(); }