Corrige bug real de idempotencia: message_hash nao deve fazer parte do send_key

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>
This commit is contained in:
Automacao GLPI
2026-08-25 09:44:44 -03:00
co-authored by Claude Sonnet 5
parent f6f12c53d8
commit 9475e68cb1
6 changed files with 79 additions and 17 deletions
+39
View File
@@ -359,3 +359,42 @@ test('envio.send_key permanece estavel para o MESMO periodo/mensagem em duas cha
const b = buildReport(baseInput({ triggerType: 'scheduled', sendLogRows: [] }), cfg);
assert.equal(a.envio.send_key, b.envio.send_key);
});
test('regressao: retry do MESMO periodo com IA nao-deterministica (texto/hash diferentes) ainda e bloqueado como duplicata (bug real de 2026-08-25: 2 mensagens reais enviadas ao Slack antes desta correcao — execucoes 16744 e 16746, mesmo periodo 2026-08-24, hashes diferentes porque a IA mudou os insights entre as duas chamadas)', () => {
const cfg = loadConfig({ SHADOW_MODE: false, DEV_MODE: false, SEND_ENABLED: true, SLACK_DESTINATION: 'C0BJNBTPFT5' });
// 1a chamada: IA retorna um insight
const primeira = buildReport(
baseInput({
triggerType: 'scheduled',
sendLogRows: [],
aiCall: { attempted: true, error: null, content: aiContentWith([
{ type: 'CATEGORY_SPIKE', severity: 'medium', title: 'Titulo A', summary: 'Resumo A',
evidence_ticket_ids: [1], confidence: 0.9, reportable: true, subject_key: 'categoria:a' },
]) },
}),
cfg
);
assert.equal(primeira.envio.pode_enviar, true);
// 2a chamada (retry do MESMO periodo): IA retorna um insight DIFERENTE
// (titulo/summary diferentes -> mensagem_v2 diferente -> message_hash
// diferente). O send_key (identidade do relatorio) tem que continuar
// sendo o MESMO, pois e o MESMO periodo/destino.
const segunda = buildReport(
baseInput({
triggerType: 'scheduled',
sendLogRows: [{ send_key: primeira.envio.send_key, status: 'success' }],
aiCall: { attempted: true, error: null, content: aiContentWith([
{ type: 'CATEGORY_SPIKE', severity: 'medium', title: 'Titulo B totalmente diferente', summary: 'Resumo B',
evidence_ticket_ids: [1], confidence: 0.9, reportable: true, subject_key: 'categoria:a' },
]) },
}),
cfg
);
assert.notEqual(primeira.envio.message_hash, segunda.envio.message_hash, 'pre-condicao do teste: os textos precisam ser diferentes');
assert.equal(segunda.envio.send_key, primeira.envio.send_key, 'send_key deve ser o mesmo apesar do texto diferente');
assert.equal(segunda.envio.pode_enviar, false);
assert.equal(segunda.envio.duplicata_bloqueada, true);
});
+12 -3
View File
@@ -79,15 +79,24 @@ test('computeMessageHash lida com texto vazio/nulo sem lancar erro', () => {
assert.doesNotThrow(() => computeMessageHash(''));
});
test('buildSendKey combina os 5 campos da chave logica de idempotencia', () => {
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',
messageHash: 'abc123',
});
assert.equal(key, 'diario|2026-08-24|2026-08-24|C0BJNBTPFT5|abc123');
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', () => {