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>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
b2aefcd2d7
commit
bdfa675d89
@@ -0,0 +1,169 @@
|
||||
'use strict';
|
||||
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const {
|
||||
formatFallbackMessage,
|
||||
computeOperationalStatus,
|
||||
buildDestaques,
|
||||
formatShortMessage,
|
||||
} = require('../src/v2/format');
|
||||
const { loadConfig } = require('../src/v2/config');
|
||||
|
||||
const config = loadConfig();
|
||||
|
||||
test('formatFallbackMessage reproduz o formato atual da V1 (numeros do exemplo da missao, 20/08)', () => {
|
||||
const row = {
|
||||
tipo_periodo: 'diario',
|
||||
periodo_label: '2026-08-20',
|
||||
abertos: 12,
|
||||
fechados: 11,
|
||||
pendentes: 22,
|
||||
alta_prioridade_pendentes: 3,
|
||||
saldo: -1,
|
||||
categorias_recorrentes: 'Software / SO: 4\nBug / Falha do Sistema: 2',
|
||||
clientes_volume: 'IMDI: 2 chamados\nLaboratorio da Cidade: 2 chamados',
|
||||
};
|
||||
const texto = formatFallbackMessage(row);
|
||||
assert.match(texto, /Resumo diario do suporte GLPI/);
|
||||
assert.match(texto, /Data do relatorio: 2026-08-20/);
|
||||
assert.match(texto, /Chamados abertos no dia: 12/);
|
||||
assert.match(texto, /Chamados fechados no dia: 11/);
|
||||
assert.match(texto, /Pendentes atuais: 22/);
|
||||
assert.match(texto, /Alta prioridade ainda pendentes: 3/);
|
||||
assert.match(texto, /Saldo fechados - abertos: -1/);
|
||||
assert.match(texto, /Software \/ SO: 4/);
|
||||
assert.match(texto, /IMDI: 2 chamados/);
|
||||
});
|
||||
|
||||
test('formatFallbackMessage nunca lanca excecao com dados vazios (fallback tem que ser sempre seguro)', () => {
|
||||
assert.doesNotThrow(() => {
|
||||
formatFallbackMessage({
|
||||
tipo_periodo: 'diario',
|
||||
periodo_label: '2026-08-20',
|
||||
abertos: 0,
|
||||
fechados: 0,
|
||||
pendentes: 0,
|
||||
alta_prioridade_pendentes: 0,
|
||||
saldo: 0,
|
||||
categorias_recorrentes: '',
|
||||
clientes_volume: '',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test('computeOperationalStatus: sem insights e backlog estavel -> NORMAL', () => {
|
||||
const s = computeOperationalStatus({ insightsAceitos: [], tendenciaBacklog: { direcao: 'estavel' } });
|
||||
assert.equal(s, 'NORMAL');
|
||||
});
|
||||
|
||||
test('computeOperationalStatus: insight severity high -> CRITICAL mesmo com backlog estavel', () => {
|
||||
const s = computeOperationalStatus({
|
||||
insightsAceitos: [{ severity: 'high' }],
|
||||
tendenciaBacklog: { direcao: 'estavel' },
|
||||
});
|
||||
assert.equal(s, 'CRITICAL');
|
||||
});
|
||||
|
||||
test('computeOperationalStatus: backlog aumentando sem insights -> ATTENTION', () => {
|
||||
const s = computeOperationalStatus({ insightsAceitos: [], tendenciaBacklog: { direcao: 'aumentando' } });
|
||||
assert.equal(s, 'ATTENTION');
|
||||
});
|
||||
|
||||
test('buildDestaques nao produz mais itens que MAX_REPORT_INSIGHTS', () => {
|
||||
const insightsAceitos = Array.from({ length: 10 }, (_, i) => ({ title: `Insight ${i}`, occurrenceText: null }));
|
||||
const destaques = buildDestaques(
|
||||
{ topCategoria: null, concentracoesCliente: [], altaPrioridadeDelta: null, insightsAceitos, unidade: 'no dia' },
|
||||
config
|
||||
);
|
||||
assert.ok(destaques.length <= config.MAX_REPORT_INSIGHTS);
|
||||
});
|
||||
|
||||
test('buildDestaques nao inventa destaque de alta prioridade quando o valor nao mudou', () => {
|
||||
const destaques = buildDestaques(
|
||||
{
|
||||
topCategoria: null,
|
||||
concentracoesCliente: [],
|
||||
altaPrioridadeDelta: { anterior: 3, atual: 3 },
|
||||
insightsAceitos: [],
|
||||
unidade: 'no dia',
|
||||
},
|
||||
config
|
||||
);
|
||||
assert.equal(destaques.length, 0);
|
||||
});
|
||||
|
||||
test('formatShortMessage: mensagem diaria contem os numeros oficiais e fica curta (sem paragrafos longos)', () => {
|
||||
const { texto, status } = formatShortMessage(
|
||||
{
|
||||
tipo_periodo: 'diario',
|
||||
data_inicio: '2026-08-20',
|
||||
data_fim: '2026-08-20',
|
||||
abertos: 12,
|
||||
fechados: 11,
|
||||
saldo: -1,
|
||||
pendentes: 22,
|
||||
alta_prioridade_pendentes: 3,
|
||||
topCategoria: { nome: 'Software / SO', total: 4 },
|
||||
concentracoesCliente: [],
|
||||
altaPrioridadeDelta: { anterior: 2, atual: 3 },
|
||||
insightsAceitos: [],
|
||||
tendenciaBacklog: { direcao: 'aumentando', delta: 2 },
|
||||
},
|
||||
config
|
||||
);
|
||||
assert.match(texto, /Resumo do suporte — 20\/08/);
|
||||
assert.match(texto, /12 abertos \| 11 fechados \| saldo -1/);
|
||||
assert.match(texto, /22 pendentes \| 3 alta prioridade/);
|
||||
assert.match(texto, /Software \/ SO liderou no dia com 4 chamados\./);
|
||||
assert.match(texto, /subiram de 2 para 3/);
|
||||
assert.equal(status, 'ATTENTION');
|
||||
assert.ok(texto.length < 700, `mensagem deveria ser curta, tinha ${texto.length} chars`);
|
||||
});
|
||||
|
||||
test('formatShortMessage: sem nenhum destaque real, mostra a frase padrao (nao inventa observacao)', () => {
|
||||
const { texto } = formatShortMessage(
|
||||
{
|
||||
tipo_periodo: 'diario',
|
||||
data_inicio: '2026-08-20',
|
||||
data_fim: '2026-08-20',
|
||||
abertos: 5,
|
||||
fechados: 5,
|
||||
saldo: 0,
|
||||
pendentes: 10,
|
||||
alta_prioridade_pendentes: 1,
|
||||
topCategoria: null,
|
||||
concentracoesCliente: [],
|
||||
altaPrioridadeDelta: null,
|
||||
insightsAceitos: [],
|
||||
tendenciaBacklog: { direcao: 'estavel', delta: 0 },
|
||||
},
|
||||
config
|
||||
);
|
||||
assert.match(texto, /Sem anomalias relevantes no periodo\./);
|
||||
});
|
||||
|
||||
test('formatShortMessage: mensagem semanal usa titulo e secao "Leitura"', () => {
|
||||
const { texto } = formatShortMessage(
|
||||
{
|
||||
tipo_periodo: 'semanal',
|
||||
data_inicio: '2026-08-17',
|
||||
data_fim: '2026-08-23',
|
||||
abertos: 51,
|
||||
fechados: 52,
|
||||
saldo: 1,
|
||||
pendentes: 21,
|
||||
alta_prioridade_pendentes: 3,
|
||||
topCategoria: { nome: 'Bug / Falha do Sistema', total: 9 },
|
||||
concentracoesCliente: [],
|
||||
altaPrioridadeDelta: null,
|
||||
insightsAceitos: [],
|
||||
tendenciaBacklog: { direcao: 'estavel', delta: 0 },
|
||||
},
|
||||
config
|
||||
);
|
||||
assert.match(texto, /Resumo semanal do suporte — 17\/08 a 23\/08/);
|
||||
assert.match(texto, /51 abertos \| 52 fechados \| saldo \+1/);
|
||||
assert.match(texto, /Leitura/);
|
||||
assert.match(texto, /→ Semana equilibrada/);
|
||||
});
|
||||
Reference in New Issue
Block a user