fix(signals): add missing source field to TaskSyncedEvent (#78)

TaskSyncedPayload in shared-types and ml/serving schemas both require
source, but TaskSyncedEvent in bus.ts and the todoist publish call both
omitted it — causing the JetStream consumer to nak every task.synced
message on validation failure.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-25 17:15:32 +00:00
parent 45416000f9
commit 352469162d
4 changed files with 11 additions and 10 deletions

View File

@@ -56,7 +56,7 @@ describe('EventBus — delivery', () => {
it('does not throw when publishing with no subscribers', () => {
const b = makeBus();
expect(() =>
b.publish('signals.task.synced', { userId: 'u', count: 3, syncedAt: '' }),
b.publish('signals.task.synced', { userId: 'u', source: 'todoist', count: 3, syncedAt: '' }),
).not.toThrow();
});
@@ -101,7 +101,7 @@ describe('EventBus — ring buffer / tail()', () => {
it('tail() filters by subject prefix', () => {
const b = makeBus();
b.publish('signals.tip.served', { userId: 'u', tipId: 't', policy: 'p', servedAt: '' });
b.publish('signals.task.synced', { userId: 'u', count: 1, syncedAt: '' });
b.publish('signals.task.synced', { userId: 'u', source: 'todoist', count: 1, syncedAt: '' });
const tipEvents = b.tail({ subject: 'signals.tip' });
expect(tipEvents.every((e) => e.subject.startsWith('signals.tip'))).toBe(true);
@@ -178,7 +178,7 @@ describe('EventBus — onPublish hook (NATS bridge contract)', () => {
const hook = vi.fn();
b.onPublish(hook);
const payload = { userId: 'u', count: 2, syncedAt: 'now' };
const payload = { userId: 'u', source: 'todoist', count: 2, syncedAt: 'now' };
b.publish('signals.task.synced', payload);
expect(hook).toHaveBeenCalledOnce();
@@ -191,7 +191,7 @@ describe('EventBus — onPublish hook (NATS bridge contract)', () => {
b.onPublish(() => calls.push('a'));
b.onPublish(() => calls.push('b'));
b.publish('signals.task.synced', { userId: 'u', count: 0, syncedAt: '' });
b.publish('signals.task.synced', { userId: 'u', source: 'todoist', count: 0, syncedAt: '' });
expect(calls).toEqual(['a', 'b']);
});
@@ -202,7 +202,7 @@ describe('EventBus — onPublish hook (NATS bridge contract)', () => {
b.onPublish(hook);
b.subscribe('signals.task.synced', sub);
b.publish('signals.task.synced', { userId: 'u', count: 1, syncedAt: '' });
b.publish('signals.task.synced', { userId: 'u', source: 'todoist', count: 1, syncedAt: '' });
expect(hook).toHaveBeenCalledOnce();
expect(sub).toHaveBeenCalledOnce();
});
@@ -215,7 +215,7 @@ describe('EventBus — onPublish hook (NATS bridge contract)', () => {
throw new Error('boom');
});
expect(() =>
b.publish('signals.task.synced', { userId: 'u', count: 0, syncedAt: '' }),
b.publish('signals.task.synced', { userId: 'u', source: 'todoist', count: 0, syncedAt: '' }),
).toThrow('boom');
});
});

View File

@@ -106,7 +106,7 @@ describe('connectNats — bridge bus → JetStream', () => {
await connectNats('nats://test:4222');
const payload = { userId: 'u1', count: 7, syncedAt: '2026-01-01T00:00:00Z' };
const payload = { userId: 'u1', source: 'todoist', count: 7, syncedAt: '2026-01-01T00:00:00Z' };
bus.publish('signals.task.synced', payload);
// Allow the queued microtask in the hook to flush.
@@ -130,7 +130,7 @@ describe('connectNats — bridge bus → JetStream', () => {
const errSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
expect(() =>
bus.publish('signals.task.synced', { userId: 'u', count: 0, syncedAt: '' }),
bus.publish('signals.task.synced', { userId: 'u', source: 'todoist', count: 0, syncedAt: '' }),
).not.toThrow();
// Wait a tick for the rejected promise's catch to run.
@@ -156,7 +156,7 @@ describe('Bus.onPublish contract — used by NATS bridge', () => {
const b = new Bus();
const hook = vi.fn();
b.onPublish(hook);
b.publish('signals.task.synced', { userId: 'u', count: 0, syncedAt: '' });
b.publish('signals.task.synced', { userId: 'u', source: 'todoist', count: 0, syncedAt: '' });
expect(hook).toHaveBeenCalledOnce();
});
});

View File

@@ -45,6 +45,7 @@ export type RewardDeliveryFailedEvent = {
export type TaskSyncedEvent = {
userId: string;
source: string; // e.g. 'todoist'
count: number;
syncedAt: string;
};

View File

@@ -88,7 +88,7 @@ export class TodoistSignalSource implements SignalSource {
});
this.cache.set(userId, { signals, fetchedAt: Date.now() });
bus.publish('signals.task.synced', { userId, count: signals.length, syncedAt: now });
bus.publish('signals.task.synced', { userId, source: 'todoist', count: signals.length, syncedAt: now });
return signals;
}