Add worker safety rails; split evaluateCampaignHealth into pure read + mutating apply
Two issues in the campaign worker path:
1. runCampaignQueue had none of the safety rails run.mjs's automatic sender
has: no outreach.ativo gate, no business-hours window, no jitter between
sends. A campaign left "rodando" would send around the clock even with
outreach disabled or outside business hours, and would hammer the same
WhatsApp number back-to-back. Added the same outreach.ativo/horaInicio/
horaFim checks (duplicated, not imported, to keep the modules
independent) plus a randomized 3-8s jitter between real send attempts.
Sleep delay is injectable via an optional sleepMs param so tests can
pass sleepMs: 0.
2. evaluateCampaignHealth mutated state (via applyPause) on every call,
including from the read-only GET /api/campaigns/:id/saude route -- so
simply opening the campaign detail panel in the UI could silently pause
a healthy-looking campaign. Split it into a pure evaluateCampaignHealth
(computes {pause, reason}, never writes) and a new applyCampaignHealth
(evaluates, then persists the pause if warranted). The worker now calls
applyCampaignHealth; the GET /saude route keeps calling the pure
evaluateCampaignHealth.
Tests: worker sends nothing with outreach.ativo=false or outside business
hours; evaluateCampaignHealth confirmed not to mutate status even when it
would return pause:true; existing mutation assertions moved onto
applyCampaignHealth.
This commit is contained in:
@@ -2,7 +2,7 @@ import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { openDb, createLeadInserter } from '../src/db.mjs';
|
||||
import { createCampaign, setCampaignStatus } from '../src/outreach/campaign-store.mjs';
|
||||
import { evaluateCampaignHealth, variantStats } from '../src/outreach/campaign-metrics.mjs';
|
||||
import { evaluateCampaignHealth, applyCampaignHealth, variantStats } from '../src/outreach/campaign-metrics.mjs';
|
||||
|
||||
function inserirLead(db, dedup) {
|
||||
const inserir = createLeadInserter(db);
|
||||
@@ -63,11 +63,41 @@ test('evaluateCampaignHealth pausa por high_failure_rate quando erro > 10% com 1
|
||||
const resultado = evaluateCampaignHealth(db, campanha.id);
|
||||
assert.equal(resultado.pause, true);
|
||||
assert.equal(resultado.reason, 'high_failure_rate');
|
||||
// evaluateCampaignHealth e read-only: so retorna o veredito, nao mexe no banco.
|
||||
const semMutacao = db.prepare('SELECT status FROM outreach_campaigns WHERE id = ?').get(campanha.id);
|
||||
assert.equal(semMutacao.status, 'rodando');
|
||||
|
||||
// A mutacao (pausar de verdade) so acontece via applyCampaignHealth.
|
||||
const aplicado = applyCampaignHealth(db, campanha.id);
|
||||
assert.equal(aplicado.pause, true);
|
||||
assert.equal(aplicado.reason, 'high_failure_rate');
|
||||
const atualizada = db.prepare('SELECT status FROM outreach_campaigns WHERE id = ?').get(campanha.id);
|
||||
assert.equal(atualizada.status, 'pausado');
|
||||
db.close();
|
||||
});
|
||||
|
||||
test('evaluateCampaignHealth nunca muda outreach_campaigns.status, mesmo quando retornaria pause:true (GET /saude nao deve pausar como efeito colateral de uma leitura)', () => {
|
||||
const db = openDb(':memory:');
|
||||
const campanha = campanhaRodando(db);
|
||||
const variant = db.prepare('SELECT id FROM outreach_campaign_variants WHERE campaign_id = ?').get(campanha.id);
|
||||
for (let i = 0; i < 8; i += 1) {
|
||||
const lead = inserirLead(db, `puro-ok${i}`);
|
||||
simularEnvio(db, campanha.id, variant.id, lead.id, lead.whatsapp, 'enviado');
|
||||
}
|
||||
for (let i = 0; i < 2; i += 1) {
|
||||
const lead = inserirLead(db, `puro-err${i}`);
|
||||
simularEnvio(db, campanha.id, variant.id, lead.id, lead.whatsapp, 'erro');
|
||||
}
|
||||
|
||||
for (let i = 0; i < 5; i += 1) {
|
||||
const resultado = evaluateCampaignHealth(db, campanha.id);
|
||||
assert.equal(resultado.pause, true, 'deveria continuar sinalizando pause:true a cada chamada');
|
||||
}
|
||||
const aindaRodando = db.prepare('SELECT status FROM outreach_campaigns WHERE id = ?').get(campanha.id);
|
||||
assert.equal(aindaRodando.status, 'rodando', 'chamar evaluateCampaignHealth repetidamente nao deveria ter pausado a campanha');
|
||||
db.close();
|
||||
});
|
||||
|
||||
test('evaluateCampaignHealth nao pausa por falha com menos de 10 tentativas no dia', () => {
|
||||
const db = openDb(':memory:');
|
||||
const campanha = campanhaRodando(db);
|
||||
|
||||
Reference in New Issue
Block a user