import { beforeEach, describe, expect, it } from 'vitest'; import { ObjectId } from 'mongodb'; import type { Repos } from '../src/db.js'; import type { Config } from '../src/config.js'; import { createMatcherRunner } from '../src/matcher/runner.js'; import { pollNewFires, advanceCheckpoint, newestCreatedAt } from '../src/matcher/ingest.js'; import type { MatchContext } from '../src/matcher/matcher.js'; import type { FireDoc } from '../src/matcher/types.js'; import { makeRepos } from './fake-repo.js'; import { silentLog, testCfg } from './helpers.js'; let repos: Repos; beforeEach(() => { repos = makeRepos(); }); function matcherCfg(mode: 'off' | 'shadow' | 'full', over: Partial = {}): Config { return testCfg({ matcher: { mode, audienceTypes: ['web', 'mobile'], pollIntervalMs: 1000, batchSize: 2000, ironPassword: 'x'.repeat(32), ...over } }); } function fireDoc(over: Partial = {}): FireDoc { return { _id: new ObjectId(), lat: 40.4, lon: -3.7, when: new Date('2026-07-13T12:00:00Z'), type: 'viirs', createdAt: new Date('2026-07-13T12:05:00Z'), ...over }; } describe('ingest', () => { it('polls fires created after the checkpoint, ascending', async () => { await repos.activefires.insertMany([ fireDoc({ createdAt: new Date('2026-07-13T10:00:00Z') }), fireDoc({ createdAt: new Date('2026-07-13T12:00:00Z') }), fireDoc({ createdAt: new Date('2026-07-13T13:00:00Z') }), ] as never); await advanceCheckpoint(repos, new Date('2026-07-13T11:00:00Z')); const cfg = matcherCfg('shadow'); const fires = await pollNewFires(cfg, repos); expect(fires).toHaveLength(2); // only the 12:00 and 13:00 ones expect(fires[0]!.createdAt! < fires[1]!.createdAt!).toBe(true); }); it('newestCreatedAt returns the max', () => { const max = newestCreatedAt([ fireDoc({ createdAt: new Date('2026-07-13T12:00:00Z') }), fireDoc({ createdAt: new Date('2026-07-13T14:00:00Z') }), ]); expect(max).toEqual(new Date('2026-07-13T14:00:00Z')); }); }); describe('matcher runner', () => { // Deterministic context so we don't need geo indexes in the fake. function fakeCtx(over: Partial = {}): MatchContext { return { candidateSubs: async () => [ { _id: new ObjectId(), location: { lat: 40.4, lon: -3.7 }, geo: { type: 'Point', coordinates: [-3.7, 40.4] }, distance: 50, owner: 'u1', type: 'mobile' }, ], existsNotifNear: async () => false, userLang: async () => 'es', ironPassword: 'x'.repeat(32), audienceTypes: new Set(['web', 'mobile']), now: () => new Date('2026-07-13T12:06:00Z'), ...over, }; } it('does nothing when mode is off', async () => { const cfg = matcherCfg('off'); const r = createMatcherRunner(cfg, repos, silentLog, fakeCtx()); const res = await r.runOnce(); expect(res).toEqual({ fires: 0, generated: 0 }); }); it('shadow mode writes to notifications_shadow, not notifications', async () => { await repos.activefires.insertOne(fireDoc() as never); await advanceCheckpoint(repos, new Date('2026-07-13T00:00:00Z')); const cfg = matcherCfg('shadow'); const r = createMatcherRunner(cfg, repos, silentLog, fakeCtx()); const res = await r.runOnce(); expect(res.generated).toBe(1); expect(await repos.notificationsShadow.countDocuments({})).toBe(1); expect(await repos.notifications.countDocuments({})).toBe(0); // checkpoint advanced const state = await repos.matcherState.findOne({ _id: 'fires' }); expect(state?.checkpoint).toEqual(fireDoc().createdAt); }); it('full mode writes to notifications', async () => { await repos.activefires.insertOne(fireDoc() as never); await advanceCheckpoint(repos, new Date('2026-07-13T00:00:00Z')); const cfg = matcherCfg('full'); const r = createMatcherRunner(cfg, repos, silentLog, fakeCtx()); const res = await r.runOnce(); expect(res.generated).toBe(1); expect(await repos.notifications.countDocuments({})).toBe(1); const n = await repos.notifications.findOne({}); expect(n?.type).toBe('mobile'); expect(n?.sealed).toContain('Fe26.2'); }); it('generates nothing for fires with no matching subs', async () => { await repos.activefires.insertOne(fireDoc() as never); await advanceCheckpoint(repos, new Date('2026-07-13T00:00:00Z')); const cfg = matcherCfg('shadow'); const r = createMatcherRunner(cfg, repos, silentLog, fakeCtx({ candidateSubs: async () => [] })); const res = await r.runOnce(); expect(res.generated).toBe(0); }); });