Files
SLACK_COORD_RELATORIO/test/anomaly.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

76 lines
2.7 KiB
JavaScript

'use strict';
const test = require('node:test');
const assert = require('node:assert/strict');
const {
mean,
median,
isAnomaly,
detectarSpikes,
detectarConcentracaoCliente,
calcularTendenciaBacklog,
} = require('../src/v2/anomaly');
const { loadConfig } = require('../src/v2/config');
const config = loadConfig();
test('median calcula corretamente para lista par e impar', () => {
assert.equal(median([1, 2, 3]), 2);
assert.equal(median([1, 2, 3, 4]), 2.5);
assert.equal(median([]), 0);
});
test('mean calcula a media simples', () => {
assert.equal(mean([1, 2, 3]), 2);
assert.equal(mean([]), 0);
});
test('isAnomaly: exige multiplicador E delta minimo (evita falso positivo trivial 0 -> 1)', () => {
// baseline 0, atual 1: multiplicador ok (sem historico usa MIN_DELTA), mas delta(1) < MIN_DELTA(3)
assert.equal(isAnomaly(1, 0, config), false);
// baseline 2.1, atual 7 (exemplo da missao: hardware hoje 7, media 20du 2.1)
assert.equal(isAnomaly(7, 2.1, config), true);
// baseline 5, atual 6: nao dobra o baseline
assert.equal(isAnomaly(6, 5, config), false);
});
test('detectarSpikes retorna somente categorias/clientes que estouraram o baseline, com numeros auditaveis', () => {
const atuais = [
{ chave: 'Hardware', atual: 7 },
{ chave: 'Software', atual: 4 },
];
const baseline = new Map([
['Hardware', [2, 2, 3, 1, 2]], // media 2.0
['Software', [4, 3, 4, 5, 4]], // media 4.0 (estavel)
]);
const spikes = detectarSpikes(atuais, baseline, config);
assert.equal(spikes.length, 1);
assert.equal(spikes[0].chave, 'Hardware');
assert.equal(spikes[0].baseline_media, 2);
});
test('detectarConcentracaoCliente exige contagem minima E participacao minima', () => {
const clientes = [
{ nome: 'IMDI', total: 5 },
{ nome: 'CTO', total: 2 },
];
// total periodo 10: IMDI = 50% (>=40%) e >=3 chamados -> concentracao
const r = detectarConcentracaoCliente(clientes, 10, config);
assert.equal(r.length, 1);
assert.equal(r[0].cliente, 'IMDI');
assert.equal(r[0].share, 0.5);
});
test('detectarConcentracaoCliente nao aponta cliente com poucos chamados mesmo com share alto', () => {
const clientes = [{ nome: 'Cliente Pequeno', total: 2 }];
const r = detectarConcentracaoCliente(clientes, 2, config); // 100% mas so 2 chamados
assert.equal(r.length, 0);
});
test('calcularTendenciaBacklog classifica direcao corretamente', () => {
assert.equal(calcularTendenciaBacklog(22, 20).direcao, 'aumentando');
assert.equal(calcularTendenciaBacklog(18, 20).direcao, 'diminuindo');
assert.equal(calcularTendenciaBacklog(20, 20).direcao, 'estavel');
assert.equal(calcularTendenciaBacklog(20, null).direcao, 'indisponivel');
});