tcef-notifications/test/poller.test.ts
vjrj d1fc6798dc Fase 1c: canal Telegram (telegram-worker) — codigo migrado, inerte
Envio de alertas Telegram desde el servicio (docs/legacy-telegram.md): por alerta,
texto Markdown (content + firelink i18n) + ubicacion, como node-red.

- types: canal 'telegram', notif.type 'telegram', campo telegramNotified
- telegram/message.ts: buildTelegramMessages (firelink es/en/gl + location)
- telegram/client.ts: cliente Bot API (fetch), interpretResponse (429 retry_after,
  403 blocked)
- telegram/rate.ts: throttle por chat (1 msg/s) + computeWaitMs puro
- handlers.processTelegramJob: idempotencia (notification_sends canal telegram),
  modos dry-run/shadow/canary/full, throttle, 429->retry, bloqueo->sub inactiva
- poller/queue/index: canal telegram integrado; worker con rate limiter global
  BullMQ (25/s por bot); cliente por idioma (es/gl->ES, en->EN)
- config: bloque telegram (tokens es/en, server, globalPerSec, perChatMinMs)

85 tests en verde, typecheck y build OK. Sin TELEGRAM_TOKEN_* configurados el
worker no se activa (todo inerte). Falta: extraer tokens de bot de flows_cred +
rollout con el usuario. Los flujos conversacionales de los bots siguen en node-red.
2026-07-13 18:49:52 +02:00

81 lines
3 KiB
TypeScript

import { beforeEach, describe, expect, it } from 'vitest';
import type { Queue } from 'bullmq';
import type { Repos } from '../src/db.js';
import type { Config } from '../src/config.js';
import { createPoller } from '../src/poller.js';
import { pendingFilter } from '../src/poller.js';
import type { Channel, SendJob } from '../src/types.js';
import { aNotif, freshRepos, silentLog, testCfg } from './helpers.js';
let repos: Repos;
const baseCfg: Config = testCfg();
beforeEach(async () => {
repos = await freshRepos();
});
/** Fake queue recording add() calls. */
function fakeQueues() {
const added: Record<Channel, SendJob[]> = { fcm: [], email: [] };
const make = (ch: Channel) =>
({
async add(_name: string, job: SendJob) {
added[ch].push(job);
},
}) as unknown as Queue<SendJob>;
return { queues: { fcm: make('fcm'), email: make('email') } as Record<Channel, Queue<SendJob>>, added };
}
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 } });
});
});
describe('poller.runOnce', () => {
it('enqueues only pending docs of owned channels and claims them', async () => {
await repos.notifications.insertMany([
aNotif({ type: 'mobile' }), // pending fcm
aNotif({ type: 'mobile', notified: true }), // already done
aNotif({ type: 'web' }), // pending email
aNotif({ type: 'web', emailNotified: true }), // already done
]);
const { queues, added } = fakeQueues();
const poller = createPoller(baseCfg, repos, queues, silentLog);
const res = await poller.runOnce();
expect(res).toEqual({ fcm: 1, email: 1, telegram: 0 });
expect(added.fcm).toHaveLength(1);
expect(added.email).toHaveLength(1);
// claimedBy provenance set
expect(await repos.notifications.countDocuments({ claimedBy: baseCfg.serviceName })).toBe(2);
});
it('only polls owned channels', async () => {
await repos.notifications.insertMany([aNotif({ type: 'mobile' }), aNotif({ type: 'web' })]);
const { queues, added } = fakeQueues();
const cfg = { ...baseCfg, ownedChannels: ['fcm'] as Channel[] };
const poller = createPoller(cfg, repos, queues, silentLog);
const res = await poller.runOnce();
expect(res.fcm).toBe(1);
expect(added.email).toHaveLength(0);
});
it('circuit breaker halts on oversized batch', async () => {
const many = Array.from({ length: 5 }, () => aNotif({ type: 'mobile' }));
await repos.notifications.insertMany(many);
const { queues, added } = fakeQueues();
const cfg = { ...baseCfg, ownedChannels: ['fcm'] as Channel[], limits: { ...baseCfg.limits, maxPerBatch: 3 } };
const poller = createPoller(cfg, repos, queues, silentLog);
const res = await poller.runOnce();
// Batch of 5 > max 3 -> nothing enqueued, poller halted.
expect(res.fcm).toBe(0);
expect(added.fcm).toHaveLength(0);
});
});