- src/v2/sendGuard.js (novo): canSend() reduz SHADOW_MODE/DEV_MODE/ SEND_ENABLED/FORCE_SEND a uma formula unica testada exaustivamente (secao 15 do ciclo final: matriz completa de guarda de seguranca). computeMessageHash/buildSendKey/isDuplicate implementam a chave logica de idempotencia da secao 13 (report_type+periodo+destino+hash). - src/v2/config.js: SEND_ENABLED, FORCE_SEND, SLACK_DESTINATION. - src/v2/pipeline.js: buildReport agora calcula envio.pode_enviar/ motivo_guarda/duplicata_bloqueada/send_key a partir de triggerType (.mode) e sendLogRows (Data Table de log de envios). - tools/build_v2_workflow*.js: novo Data Table 'Buscar Log de Envios', troca do HTTP Slack com webhook placeholder por node Slack nativo (credencial real lNqwBBrtLfkRqw2X, reaproveitada do projeto irmao Coordenador GLPI), branches de sucesso/falha gravando o log de envio so apos confirmacao real do Slack. - test/sendGuard.test.js, test/workflowStructure.test.js (novos): cobrem a matriz de guarda de seguranca e travam regressao do bug de timeout (executeOnce=true nos 8 nodes de leitura em lote) e do bug de sendGuard.js nao concatenado no Code node gerado. - Corrigido bug real encontrado nesta sessao: sendGuard.js nao estava na lista de modulos concatenados pelo gerador, deixando canSend/ computeMessageHash/buildSendKey undefined em runtime. - Deployado e validado ao vivo no DEV (workflow 3x5kb58TeBCvirm6): n8n_validate_workflow -> valid:true, 0 erros, executeOnce preservado nos 9 nodes de leitura em lote. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
221 lines
7.7 KiB
JavaScript
221 lines
7.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,
|
|
},
|
|
// $execution.mode: 'trigger' simula o Schedule Trigger (producao real);
|
|
// o smoke test usa isso por padrao porque e o caminho mais exercitado
|
|
// pelo relatorio (execucao manual e coberta por sendGuard.test.js).
|
|
$execution: { mode: 'trigger' },
|
|
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,
|
|
SEND_ENABLED: true,
|
|
FORCE_SEND: false,
|
|
SLACK_DESTINATION: 'C0000TESTE',
|
|
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', []);
|
|
registry.set('Data Table - Buscar Log de Envios', []);
|
|
|
|
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('envio:', JSON.stringify(relatorio.envio, null, 2));
|
|
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('\n--- 6) Code - Preparar Log de Envio (Sucesso/Falha) ---');
|
|
registry.set('Code - Motor V2: Relatorio Completo', [{ json: relatorio }]);
|
|
const out6a = runCodeNode('Code - Preparar Log de Envio (Sucesso)', [{ json: {} }]);
|
|
console.log('log sucesso:', JSON.stringify(out6a[0].json));
|
|
const out6b = runCodeNode('Code - Preparar Log de Envio (Falha)', [{ json: { error: { message: 'timeout simulado' } } }]);
|
|
console.log('log falha:', JSON.stringify(out6b[0].json));
|
|
|
|
console.log('\nSMOKE TEST OK — todos os Code nodes gerados executaram sem excecao.');
|