Files
SLACK_COORD_RELATORIO/tools/smoke_test_generated_nodes.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

205 lines
6.7 KiB
JavaScript

'use strict';
/**
* Smoke test do workflow GERADO (nao dos modulos fonte, que ja tem testes
* de unidade em test/*.test.js). Executa, com Node puro + vm, exatamente o
* texto que sera colado em cada Code node do n8n, simulando $()/$input/$json
* com dados de exemplo (mesmos numeros de referencia da missao, 20/08/2026).
*
* Uso: node tools/smoke_test_generated_nodes.js
*/
const fs = require('fs');
const path = require('path');
const vm = require('vm');
const WF_PATH = path.join(__dirname, '..', 'Resumo Operacional GLPI v2 - IA.n8n.json');
const workflow = JSON.parse(fs.readFileSync(WF_PATH, 'utf8'));
function nodeByName(name) {
const n = workflow.nodes.find((x) => x.name === name);
if (!n) throw new Error(`node nao encontrado no workflow gerado: ${name}`);
return n;
}
/** registry simula a memoria de execucao do n8n: nome do node -> lista de items {json} */
const registry = new Map();
function makeDollar() {
return function $(nodeName) {
const items = registry.get(nodeName);
if (!items) throw new Error(`$('${nodeName}') chamado antes do node rodar (ordem errada no smoke test)`);
return {
first: () => items[0],
all: () => items,
};
};
}
function runCodeNode(nodeName, inputItems, extraGlobals) {
const node = nodeByName(nodeName);
const code = node.parameters.jsCode;
const sandbox = {
$: makeDollar(),
$json: inputItems[0] ? inputItems[0].json : {},
$input: {
first: () => inputItems[0],
all: () => inputItems,
},
console,
Date,
JSON,
Math,
Number,
String,
Boolean,
Array,
Object,
Map,
Set,
RegExp,
...extraGlobals,
};
vm.createContext(sandbox);
const wrapped = `(function(){ ${code} \n})()`;
const result = vm.runInContext(wrapped, sandbox, { filename: nodeName });
registry.set(nodeName, result);
return result;
}
// -----------------------------------------------------------------------
// Fixtures (mesmos numeros de referencia da missao: 20/08/2026)
// -----------------------------------------------------------------------
const configItem = [
{
json: {
data_alvo: '2026-08-20',
slack_webhook_url: 'https://hooks.slack.com/services/TESTE',
AI_ENABLED: true,
AI_MODEL: 'gpt-5.5',
AI_MAX_TICKETS: 40,
AI_MAX_CONTEXT_CHARS: 12000,
AI_MIN_CONFIDENCE: 0.6,
AI_TIMEOUT_MS: 30000,
HISTORICAL_BASELINE_DAYS: 20,
HISTORICAL_BASELINE_WEEKS: 4,
MAX_REPORT_INSIGHTS: 5,
DEV_MODE: true,
SHADOW_MODE: true,
EXTENDED_METRICS_ENABLED: false,
ANOMALY_MULTIPLIER: 2.0,
ANOMALY_MIN_DELTA: 3,
CLIENT_CONCENTRATION_MIN_COUNT: 3,
CLIENT_CONCENTRATION_MIN_SHARE: 0.4,
PENDING_AGE_ATTENTION_DAYS: 15,
TICKET_CONTEXT_SQL_LIMIT: 300,
},
},
];
console.log('--- 1) Code - Periodo e Queries SQL ---');
const out1 = runCodeNode('Code - Periodo e Queries SQL', configItem);
console.log('tipo_periodo:', out1[0].json.period.tipo_periodo, '| periodo_label:', out1[0].json.period.periodo_label);
console.log('query indicadores (trecho):', out1[0].json.queries.indicadores.slice(0, 80).replace(/\n/g, ' '), '...');
registry.set('MySQL - Indicadores do Periodo', [
{
json: {
tipo_periodo: 'manual_diario',
periodo_label: '2026-08-20',
data_inicio: '2026-08-20',
data_fim: '2026-08-20',
abertos: 12,
fechados: 11,
saldo: -1,
pendentes: 22,
alta_prioridade_pendentes: 3,
categorias_recorrentes: 'Software / SO: 4\nBug / Falha do Sistema: 2',
clientes_volume: 'IMDI: 2 chamados\nLaboratorio da Cidade: 2 chamados',
},
},
]);
registry.set('MySQL - Ranking Categorias', [
{ json: { id: 10, nome: 'Software / SO', total: 4 } },
{ json: { id: 11, nome: 'Bug / Falha do Sistema', total: 2 } },
]);
registry.set('MySQL - Ranking Clientes', [
{ json: { id: 20, nome: 'IMDI', total: 2 } },
{ json: { id: 21, nome: 'Laboratorio da Cidade', total: 2 } },
]);
registry.set(
'MySQL - Contexto de Chamados',
Array.from({ length: 5 }, (_, i) => ({
json: {
id: i + 1,
title: `Chamado de teste ${i + 1}`,
category: 'Software / SO',
client: 'IMDI',
priority: 3,
status: 5,
opened_at: '2026-08-20',
closed_at: null,
solved_at: null,
description: 'Descricao de teste para o smoke test.',
solution: null,
},
}))
);
registry.set('MySQL - Baseline Diario Categoria', [
{ dia: '2026-08-13', id: 10, total: 1 },
{ dia: '2026-08-14', id: 10, total: 1 },
].map((r) => ({ json: r })));
registry.set('MySQL - Baseline Diario Cliente', [
{ dia: '2026-08-13', id: 20, total: 1 },
].map((r) => ({ json: r })));
registry.set('Data Table - Buscar Memoria de Insights', []);
registry.set('Data Table - Buscar Historico Anterior', []);
console.log('\n--- 2) Code - Preparar Contexto IA ---');
const out2 = runCodeNode('Code - Preparar Contexto IA', [{ json: {} }]);
console.log('tickets no contexto:', out2[0].json.ticketContext.tickets.length);
console.log('modelo solicitado:', out2[0].json.openai_request_body.model);
console.log('\n--- 3) Code - Normalizar Resposta IA (simulando resposta OpenAI valida) ---');
const respostaIaSimulada = {
choices: [
{
message: {
content: JSON.stringify({
period_assessment: { status: 'stable', summary: 'Operacao dentro do padrao.' },
insights: [
{
type: 'CATEGORY_SPIKE',
severity: 'medium',
title: 'Software/SO acima do padrao',
summary: 'Volume de Software/SO ficou acima da media recente.',
evidence_ticket_ids: [1, 2],
confidence: 0.85,
reportable: true,
subject_key: 'categoria:software_so',
},
],
}),
},
},
],
};
const out3 = runCodeNode('Code - Normalizar Resposta IA', [{ json: respostaIaSimulada }]);
console.log('aiCall:', out3[0].json.aiCall.attempted, out3[0].json.aiCall.error);
console.log('\n--- 4) Code - Motor V2: Relatorio Completo ---');
const out4 = runCodeNode('Code - Motor V2: Relatorio Completo', [{ json: out3[0].json }]);
const relatorio = out4[0].json;
console.log('status_operacional:', relatorio.status_operacional);
console.log('insights_aceitos:', relatorio.insights_aceitos.length);
console.log('observabilidade:', JSON.stringify(relatorio.observabilidade, null, 2));
console.log('\n=== mensagem_v1 (fallback / legado) ===\n' + relatorio.mensagem_v1);
console.log('\n=== mensagem_v2 (V2 curta) ===\n' + relatorio.mensagem_v2);
console.log('\n--- 5) Code - Preparar Historico ---');
const out5 = runCodeNode('Code - Preparar Historico', [{ json: relatorio }]);
console.log(JSON.stringify(out5[0].json, null, 2));
console.log('\nSMOKE TEST OK — todos os Code nodes gerados executaram sem excecao.');