Bug encontrado AO VIVO durante o teste controlado de envio (secao 14): a IA (gpt-5.5) nao e deterministica. Duas chamadas para o MESMO periodo (mesmo report_type+data) geraram insights com textos diferentes, portanto message_hash diferente, portanto send_key diferente — o retry do MESMO periodo passou pela deteccao de duplicata como se fosse outro relatorio. Resultado real: 3 mensagens verdadeiras enviadas ao canal C0BJNBTPFT5 para o periodo 'diario 2026-08-24' antes de eu interromper e corrigir (execucoes 16744, 16746 e uma terceira ja em voo quando a guarda foi revertida). Estado revertido imediatamente (DEV_MODE/SHADOW_MODE=true, workflow desativado) assim que o padrao foi identificado. Correcao: src/v2/sendGuard.js#buildSendKey agora usa so report_type+period_start+period_end+destination (SEM message_hash) — a identidade de um relatorio e o PERIODO, nao o texto exato produzido pela IA. message_hash continua calculado e armazenado, so para diagnostico. As 3 linhas reais gravadas na Data Table dev_glpi_v2_send_log foram normalizadas manualmente (send_key sem sufixo de hash) para que a protecao de duplicata funcione corretamente para qualquer nova tentativa do mesmo periodo daqui pra frente. Testes novos: sendGuard.test.js e pipeline.test.js ganharam testes de regressao reproduzindo exatamente este cenario (retry do mesmo periodo com aiCall/mensagem diferente -> send_key igual -> duplicata bloqueada). Canal Slack real confirmado nesta sessao: D06JK4GEK8E (DM) foi tentado primeiro e falhou com channel_not_found (bot sem acesso); o usuario optou por reaproveitar C0BJNBTPFT5 (canal do workflow irmao Coordenador GLPI, onde o bot ja e membro). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
124 lines
5.2 KiB
JavaScript
124 lines
5.2 KiB
JavaScript
'use strict';
|
|
|
|
const { test } = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
const { canSend, computeMessageHash, buildSendKey, isDuplicate } = require('../src/v2/sendGuard');
|
|
|
|
const BASE = { SHADOW_MODE: false, DEV_MODE: false, SEND_ENABLED: true, FORCE_SEND: false };
|
|
|
|
// --- Matriz de guarda de seguranca completa (secao 15 do ciclo final) ---
|
|
|
|
test('SHADOW_MODE=true nunca envia, mesmo scheduled com tudo mais ligado', () => {
|
|
const r = canSend({ triggerType: 'scheduled', config: { ...BASE, SHADOW_MODE: true } });
|
|
assert.equal(r.podeEnviar, false);
|
|
});
|
|
|
|
test('SHADOW_MODE=true nunca envia, mesmo manual com FORCE_SEND=true', () => {
|
|
const r = canSend({ triggerType: 'manual', config: { ...BASE, SHADOW_MODE: true, FORCE_SEND: true } });
|
|
assert.equal(r.podeEnviar, false);
|
|
});
|
|
|
|
test('DEV_MODE=true sem override nunca envia para producao (scheduled)', () => {
|
|
const r = canSend({ triggerType: 'scheduled', config: { ...BASE, DEV_MODE: true } });
|
|
assert.equal(r.podeEnviar, false);
|
|
});
|
|
|
|
test('DEV_MODE=true bloqueia mesmo manual+FORCE_SEND=true (sem override possivel)', () => {
|
|
const r = canSend({ triggerType: 'manual', config: { ...BASE, DEV_MODE: true, FORCE_SEND: true } });
|
|
assert.equal(r.podeEnviar, false);
|
|
});
|
|
|
|
test('SEND_ENABLED=false nunca envia (scheduled)', () => {
|
|
const r = canSend({ triggerType: 'scheduled', config: { ...BASE, SEND_ENABLED: false } });
|
|
assert.equal(r.podeEnviar, false);
|
|
});
|
|
|
|
test('SEND_ENABLED=false nunca envia (manual + FORCE_SEND=true)', () => {
|
|
const r = canSend({ triggerType: 'manual', config: { ...BASE, SEND_ENABLED: false, FORCE_SEND: true } });
|
|
assert.equal(r.podeEnviar, false);
|
|
});
|
|
|
|
test('scheduled + producao (SEND_ENABLED=true, SHADOW_MODE=false, DEV_MODE=false) pode enviar', () => {
|
|
const r = canSend({ triggerType: 'scheduled', config: { ...BASE } });
|
|
assert.equal(r.podeEnviar, true);
|
|
});
|
|
|
|
test('manual + FORCE_SEND=false nunca envia', () => {
|
|
const r = canSend({ triggerType: 'manual', config: { ...BASE, FORCE_SEND: false } });
|
|
assert.equal(r.podeEnviar, false);
|
|
});
|
|
|
|
test('manual + FORCE_SEND=true pode enviar (sujeito a idempotencia a parte)', () => {
|
|
const r = canSend({ triggerType: 'manual', config: { ...BASE, FORCE_SEND: true } });
|
|
assert.equal(r.podeEnviar, true);
|
|
});
|
|
|
|
test('motivo sempre presente e legivel, mesmo quando pode enviar', () => {
|
|
const r = canSend({ triggerType: 'scheduled', config: { ...BASE } });
|
|
assert.equal(typeof r.motivo, 'string');
|
|
assert.ok(r.motivo.length > 0);
|
|
});
|
|
|
|
// --- Hash / chave / duplicata (secao 13) ---
|
|
|
|
test('computeMessageHash e deterministico para o mesmo texto', () => {
|
|
const a = computeMessageHash('Resumo diario: 11 abertos, 11 fechados');
|
|
const b = computeMessageHash('Resumo diario: 11 abertos, 11 fechados');
|
|
assert.equal(a, b);
|
|
});
|
|
|
|
test('computeMessageHash muda quando o texto muda', () => {
|
|
const a = computeMessageHash('Resumo diario: 11 abertos');
|
|
const b = computeMessageHash('Resumo diario: 12 abertos');
|
|
assert.notEqual(a, b);
|
|
});
|
|
|
|
test('computeMessageHash lida com texto vazio/nulo sem lancar erro', () => {
|
|
assert.doesNotThrow(() => computeMessageHash(null));
|
|
assert.doesNotThrow(() => computeMessageHash(undefined));
|
|
assert.doesNotThrow(() => computeMessageHash(''));
|
|
});
|
|
|
|
test('buildSendKey combina report_type + periodo + destino (SEM message_hash)', () => {
|
|
const key = buildSendKey({
|
|
reportType: 'diario',
|
|
periodStart: '2026-08-24',
|
|
periodEnd: '2026-08-24',
|
|
destination: 'C0BJNBTPFT5',
|
|
});
|
|
assert.equal(key, 'diario|2026-08-24|2026-08-24|C0BJNBTPFT5');
|
|
});
|
|
|
|
test('regressao: send_key IGUAL mesmo com message_hash diferente (bug real de 2026-08-25 — 2 mensagens reais enviadas ao Slack porque a IA nao e deterministica e mudou o texto entre duas chamadas do MESMO periodo, gerando hashes diferentes e escapando da deteccao de duplicata)', () => {
|
|
const base = { reportType: 'diario', periodStart: '2026-08-24', periodEnd: '2026-08-24', destination: 'C0BJNBTPFT5' };
|
|
const keyTentativa1 = buildSendKey(base);
|
|
const keyTentativa2 = buildSendKey(base);
|
|
assert.equal(keyTentativa1, keyTentativa2);
|
|
|
|
const rows = [{ send_key: keyTentativa1, status: 'success' }];
|
|
assert.equal(isDuplicate(rows, keyTentativa2), true);
|
|
});
|
|
|
|
test('isDuplicate detecta uma chave ja registrada com status=success', () => {
|
|
const key = 'diario|2026-08-24|2026-08-24|C0BJNBTPFT5|abc123';
|
|
const rows = [{ send_key: key, status: 'success' }];
|
|
assert.equal(isDuplicate(rows, key), true);
|
|
});
|
|
|
|
test('isDuplicate ignora registros com status diferente de success', () => {
|
|
const key = 'diario|2026-08-24|2026-08-24|C0BJNBTPFT5|abc123';
|
|
const rows = [{ send_key: key, status: 'failed' }, { send_key: key, status: 'blocked_duplicate' }];
|
|
assert.equal(isDuplicate(rows, key), false);
|
|
});
|
|
|
|
test('isDuplicate retorna false quando a lista de logs esta vazia ou ausente', () => {
|
|
assert.equal(isDuplicate([], 'qualquer'), false);
|
|
assert.equal(isDuplicate(null, 'qualquer'), false);
|
|
assert.equal(isDuplicate(undefined, 'qualquer'), false);
|
|
});
|
|
|
|
test('isDuplicate nao confunde chaves de periodos diferentes', () => {
|
|
const rows = [{ send_key: 'diario|2026-08-24|2026-08-24|C0BJNBTPFT5|abc123', status: 'success' }];
|
|
assert.equal(isDuplicate(rows, 'diario|2026-08-25|2026-08-25|C0BJNBTPFT5|abc123'), false);
|
|
});
|