test: cover NATS bridge + Todoist scheduler; ADR-0010
- bus.test.ts: 4 cases for the new onPublish hook contract - nats.test.ts: stream creation idempotency + JSON publish bridge - scheduler.test.ts: startup delay, fan-out, per-user failure isolation - ADR-0010 documents the bridge-don't-replace decision and the Todoist scheduler isolation, plus open follow-ups (#98 ml/serving consumer, #54 protobuf migration, graceful shutdown, metrics) - README/overview/services README reflect the bridged event substrate - CLAUDE.md gains a "don't nats.publish() directly" rule - .env.example documents NATS_URL + TODOIST_SYNC_INTERVAL_MS Verified in deployment 2026-04-18: api -> nats bridge connects on boot, signals + feedback streams created, scheduler tick logs "todoist sync: 1 ok, 0 failed (1 users)" within 10s. Closes #21, #22. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -171,3 +171,51 @@ describe('EventBus — singleton bus export', () => {
|
||||
expect(bus).toBeInstanceOf(Bus);
|
||||
});
|
||||
});
|
||||
|
||||
describe('EventBus — onPublish hook (NATS bridge contract)', () => {
|
||||
it('invokes registered hook with subject + payload on every publish', () => {
|
||||
const b = makeBus();
|
||||
const hook = vi.fn();
|
||||
b.onPublish(hook);
|
||||
|
||||
const payload = { userId: 'u', count: 2, syncedAt: 'now' };
|
||||
b.publish('signals.task.synced', payload);
|
||||
|
||||
expect(hook).toHaveBeenCalledOnce();
|
||||
expect(hook).toHaveBeenCalledWith('signals.task.synced', payload);
|
||||
});
|
||||
|
||||
it('fires multiple hooks in registration order', () => {
|
||||
const b = makeBus();
|
||||
const calls: string[] = [];
|
||||
b.onPublish(() => calls.push('a'));
|
||||
b.onPublish(() => calls.push('b'));
|
||||
|
||||
b.publish('signals.task.synced', { userId: 'u', count: 0, syncedAt: '' });
|
||||
expect(calls).toEqual(['a', 'b']);
|
||||
});
|
||||
|
||||
it('hook fires alongside subscribers — both receive the publish', () => {
|
||||
const b = makeBus();
|
||||
const hook = vi.fn();
|
||||
const sub = vi.fn();
|
||||
b.onPublish(hook);
|
||||
b.subscribe('signals.task.synced', sub);
|
||||
|
||||
b.publish('signals.task.synced', { userId: 'u', count: 1, syncedAt: '' });
|
||||
expect(hook).toHaveBeenCalledOnce();
|
||||
expect(sub).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it('a throwing hook still bubbles up — bus does not silently swallow', () => {
|
||||
// Documents current behaviour: hooks run inside publish(), so a thrown
|
||||
// error escapes. The NATS adapter therefore catches inside the hook.
|
||||
const b = makeBus();
|
||||
b.onPublish(() => {
|
||||
throw new Error('boom');
|
||||
});
|
||||
expect(() =>
|
||||
b.publish('signals.task.synced', { userId: 'u', count: 0, syncedAt: '' }),
|
||||
).toThrow('boom');
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user