fix(poller): stop infinite re-enqueue of unsendable notifications

In shadow/dry-run/kill-switch modes, when the target has no token/recipient/
chat, or when not in the canary cohort, the handlers returned without marking
the notification, so the poller (pendingFilter on notified:{$ne:true})
re-selected the same batch every cycle forever (observed: fcm:500 every 10s).

Add a service-owned per-channel marker processedBy.<channel>, set in every
terminal 'won't/can't send' branch, and exclude marked docs in pendingFilter.
Unlike the shared notified/emailNotified/telegramNotified flags, processedBy is
invisible to the old system, so it never suppresses a real send in prod shadow.

- types.ts: NotificationDoc.processedBy?: Partial<Record<Channel, Date>>
- poller.ts: pendingFilter excludes processedBy.<channel>
- handlers.ts: markProcessed() in no-token/no-recipient/no-chat, decideSend=false,
  dead-token and telegram-blocked branches
- fake-repo: dotted-path support in matching and $set/$unset
- tests: regression test + processedBy assertions (86 passing)
This commit is contained in:
vjrj 2026-07-23 23:47:13 +02:00
parent 3596f7abed
commit 6d1ba439a2
6 changed files with 115 additions and 15 deletions

View file

@ -28,8 +28,8 @@ function fakeQueues() {
describe('pendingFilter', () => {
it('uses the CORRECT field names (old cron had a typo)', () => {
expect(pendingFilter('fcm')).toEqual({ type: 'mobile', notified: { $ne: true } });
expect(pendingFilter('email')).toEqual({ type: 'web', emailNotified: { $ne: true } });
expect(pendingFilter('fcm')).toEqual({ type: 'mobile', notified: { $ne: true }, 'processedBy.fcm': { $exists: false } });
expect(pendingFilter('email')).toEqual({ type: 'web', emailNotified: { $ne: true }, 'processedBy.email': { $exists: false } });
});
});