Files
SLACK_COORD_RELATORIO/test/period.test.js
T
Automacao GLPIandClaude Sonnet 5 bdfa675d89 Adiciona V2 do relatorio GLPI: camada deterministica + IA com fallback
Evolui a automacao (V1 preservada, intocada) para uma V2 com:

- Camada deterministica (src/v2): calendario operacional (porta fiel da
  logica da V1), queries SQL somente-leitura (indicadores, ranking com
  IDs, contexto de chamados, baseline historico), deteccao de anomalia
  (spike de categoria/cliente, concentracao, tendencia de backlog),
  memoria/fingerprint de insights com transicao de estado (new ->
  persisting -> normalizing -> resolved), mensagens curtas (fallback V1 e
  V2). 97 testes automatizados (node --test).

- Camada de IA (structured output via OpenAI, schema JSON validado em
  codigo, nunca confia em numeros vindos da IA): prompt de "Analista
  Operacional", fallback automatico e seguro quando a IA falha/expira/
  retorna JSON invalido.

- Workflow n8n "[DEV] GLPI - Relatorio Operacional IA v2" (id
  3x5kb58TeBCvirm6, INATIVO) criado e validado ao vivo no DEV
  (devn8n.gtigrupo.com.br) via webhook temporario (removido apos os
  testes) com SHADOW_MODE/DEV_MODE ligados — nenhuma mensagem real
  enviada, nenhuma escrita no GLPI.

- tools/build_v2_workflow.js + build_v2_workflow_assemble.js: geram o
  workflow n8n a partir de src/v2/*.js (Code nodes remotos nao importam
  arquivos locais), mantendo uma unica fonte de verdade testada.

Bugs reais encontrados e corrigidos durante a validacao ao vivo:
- glpi_tickets nao tem coluna "solution" nesta versao do schema.
- Nodes MySQL/DataTable sem "Execute Once" executavam 1x por item
  recebido do node anterior; a cadeia amplificava ate ~76 mil execucoes
  na query de baseline por cliente, causando o timeout real observado.
  Corrigido com executeOnce=true nos 8 nodes afetados e reescrita da
  query de baseline para nao depender de JOIN com tabela de dimensao.
- IF "Pode Enviar" usava o operador booleano invertido (checava "e
  true" em vez de "e false"), o que quase permitia envio real ao Slack
  mesmo com SHADOW_MODE/DEV_MODE ligados.
- gpt-5.5 exige max_completion_tokens (nao max_tokens) e gasta o
  orcamento em tokens de raciocinio; ajustado para 3000 tokens +
  reasoning_effort=low.

Validado ao vivo contra dados reais do GLPI DEV (20/08/2026): IA gerou
insights reais com evidencia auditavel, memoria persistiu entre
execucoes (occurrences incrementando, normalizacao detectada),
resultado 200 confirmando envio suprimido (shadow mode).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-25 08:49:45 -03:00

114 lines
4.6 KiB
JavaScript

'use strict';
const test = require('node:test');
const assert = require('node:assert/strict');
const { resolvePeriod, businessDaysWindowBefore, parseDate, formatDate } = require('../src/v2/period');
const { loadConfig } = require('../src/v2/config');
const config = loadConfig();
// Ancoras reais usadas na missao (agosto/2026):
// 17/08 = segunda, 18/08 = terca, 19/08 = quarta, 20/08 = quinta,
// 21/08 = sexta, 22/08 = sabado, 23/08 = domingo, 24/08 = segunda.
function d(dia) {
return new Date(2026, 7, dia);
}
test('terca (18/08) cobre o relatorio diario de segunda (17/08)', () => {
const r = resolvePeriod('', d(18), config);
assert.equal(r.tipo_periodo, 'diario');
assert.equal(r.data_inicio, '2026-08-17');
assert.equal(r.data_fim, '2026-08-17');
});
test('quarta (19/08) cobre o relatorio diario de terca (18/08)', () => {
const r = resolvePeriod('', d(19), config);
assert.equal(r.tipo_periodo, 'diario');
assert.equal(r.data_inicio, '2026-08-18');
assert.equal(r.data_fim, '2026-08-18');
});
test('quinta (20/08) cobre o relatorio diario de quarta (19/08)', () => {
const r = resolvePeriod('', d(20), config);
assert.equal(r.tipo_periodo, 'diario');
assert.equal(r.data_inicio, '2026-08-19');
assert.equal(r.data_fim, '2026-08-19');
});
test('sexta (21/08) cobre o relatorio diario de quinta (20/08)', () => {
const r = resolvePeriod('', d(21), config);
assert.equal(r.tipo_periodo, 'diario');
assert.equal(r.data_inicio, '2026-08-20');
assert.equal(r.data_fim, '2026-08-20');
});
test('segunda (24/08) cobre o relatorio semanal consolidado de 17/08 a 23/08', () => {
const r = resolvePeriod('', d(24), config);
assert.equal(r.tipo_periodo, 'semanal');
assert.equal(r.data_inicio, '2026-08-17');
assert.equal(r.data_fim, '2026-08-23');
assert.equal(r.periodo_label, '2026-08-17 a 2026-08-23');
});
test('sabado/domingo nao fazem parte do calendario operacional (nao ha schedule nesses dias, mas a funcao deve continuar pura)', () => {
// Sabado (22/08): getDay() = 6, cai no "senao" (diario) porque so segunda e tratada
// como especial. Isso e esperado: o Schedule Trigger real (cron 1-5) nunca dispara
// aos sabados/domingos, entao este caso nunca ocorre em producao/DEV.
const r = resolvePeriod('', d(22), config);
assert.equal(r.tipo_periodo, 'diario');
assert.equal(r.data_inicio, '2026-08-21');
});
test('data_alvo manual (YYYY-MM-DD) forca modo diario manual, independente do dia da semana atual', () => {
const r = resolvePeriod('2026-08-20', d(24), config);
assert.equal(r.tipo_periodo, 'manual_diario');
assert.equal(r.data_inicio, '2026-08-20');
assert.equal(r.data_fim, '2026-08-20');
});
test('data_alvo manual aceita formato dd/mm/aa', () => {
const r = resolvePeriod('20/08/26', d(24), config);
assert.equal(r.data_inicio, '2026-08-20');
});
test('data_alvo manual aceita formato dd/mm/aaaa', () => {
const r = resolvePeriod('20/08/2026', d(24), config);
assert.equal(r.data_inicio, '2026-08-20');
});
test('data_alvo invalida (texto livre) cai no modo automatico', () => {
const r = resolvePeriod('data invalida', d(19), config);
assert.equal(r.tipo_periodo, 'diario');
assert.equal(r.data_inicio, '2026-08-18');
});
test('parseDate rejeita string vazia', () => {
assert.equal(parseDate(''), null);
assert.equal(parseDate(undefined), null);
});
test('businessDaysWindowBefore conta somente dias uteis (pula fim de semana)', () => {
// antes de 24/08 (segunda): 5 dias uteis = 21,20,19,18,17 (sex,qui,qua,ter,seg)
const janela = businessDaysWindowBefore(d(24), 5);
assert.equal(formatDate(janela.start), '2026-08-17');
assert.equal(formatDate(janela.end), '2026-08-21');
assert.equal(janela.businessDays, 5);
});
test('baseline diario expoe janela rolling e last5BusinessDays sem usar datas incorretas', () => {
const r = resolvePeriod('', d(19), config); // periodo = 18/08 (terca)
assert.equal(r.baseline.immediatePrevious.start, r.baseline.immediatePrevious.end);
assert.ok(r.baseline.rolling.start < r.baseline.rolling.end || r.baseline.rolling.start <= r.baseline.rolling.end);
assert.ok(r.baseline.last5BusinessDays.end < r.data_inicio);
assert.ok(r.baseline.rolling.end < r.data_inicio);
});
test('baseline semanal expoe janela de 4 semanas anteriores sem sobrepor o periodo atual', () => {
const r = resolvePeriod('', d(24), config); // periodo = 17 a 23/08
assert.equal(r.baseline.immediatePrevious.start, '2026-08-10');
assert.equal(r.baseline.immediatePrevious.end, '2026-08-16');
assert.equal(r.baseline.last4Weeks.start, '2026-07-20');
assert.equal(r.baseline.last4Weeks.end, '2026-08-16');
assert.ok(r.baseline.last4Weeks.end < r.data_inicio);
});