- Real bug found by code review while implementing this cycle: "Code - Reusar Classificacao em Cache" always hardcoded is_fallback: false when reconstructing a classification from memory, even when the cached classification was itself AI_FALLBACK. On a cache hit for a previously fallback-classified ticket, this would have let it skip applyFallbackPolicy's exclusion entirely (the policy checks aiClassification.is_fallback first). Fixed to derive is_fallback from the cached ai_classification value. - select-top10.node.js now carries `resolucao` (ia | cache | fallback_cap) through to the scored item, previously dropped -- needed to tell cache hits apart from real AI calls in observability. - Code - Comparar Legacy x V2 now computes the full observability summary (candidate_count, cache_hits/misses, ai_requested/success/errors, ai_fallback_count and by-reason breakdown, classification_counts, insufficient_context_count, actionable_real_count, critical_override_count, top10_count, blocked_count, coordination_count) and the data-quality review (buildDataQualityReview over INSUFFICIENT_CONTEXT tickets), both stored in the shadow_runs `summary` JSON column -- no new Data Table needed. - Removed the debug webhook entirely (Debug - Ver Painel, Data Table - Buscar Shadow Runs Recentes debug branch, Code - Montar Painel Debug, Debug - Responder) and the now-unused debug-panel.node.js driver. Workflow went from 37 to 33 nodes. Debugging goes through n8n execution data, the Data Tables, and the shadow diff instead, as requested. - Fixed glpi_base_url: was still V1's own placeholder (https://glpi.suaempresa.com.br, never a real value, not report anonymization) -- confirmed the real domain (https://glpi.gtigrupo.com.br) from the live, active GLPI x WhatsApp workflow's own GLPI API config in the same n8n instance. SQL still only ever receives it through {GLPI_BASE_URL} substitution from the Config node, never hardcoded. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
77 lines
3.5 KiB
JavaScript
77 lines
3.5 KiB
JavaScript
const { test } = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
const { buildWorkflow } = require('./build-workflow');
|
|
|
|
test('workflow montado tem nome e nodes esperados', () => {
|
|
const wf = buildWorkflow();
|
|
assert.equal(wf.name, '[DEV] Coordenador GLPI - Chamados Abertos v2 (Shadow)');
|
|
const names = wf.nodes.map((n) => n.name);
|
|
assert.ok(names.includes('Code - Montar SQL Candidatos'));
|
|
});
|
|
|
|
test('Code - Montar SQL Candidatos tem a logica de config e o SQL inlined (sem marcador de build)', () => {
|
|
const wf = buildWorkflow();
|
|
const node = wf.nodes.find((n) => n.name === 'Code - Montar SQL Candidatos');
|
|
assert.ok(node.parameters.jsCode.includes('function parseConfig'));
|
|
assert.ok(node.parameters.jsCode.includes('WITH ultima_interacao'));
|
|
assert.ok(!node.parameters.jsCode.includes('__BUILD__'));
|
|
assert.ok(!node.parameters.jsCode.includes('__SQL_CANDIDATOS_V2__'));
|
|
});
|
|
|
|
test('nenhum node de Code sobra com marcador nao resolvido', () => {
|
|
const wf = buildWorkflow();
|
|
for (const node of wf.nodes) {
|
|
if (node.type === 'n8n-nodes-base.code') {
|
|
assert.ok(!node.parameters.jsCode.includes('__BUILD__'), `node ${node.name} tem marcador nao resolvido`);
|
|
}
|
|
}
|
|
});
|
|
|
|
test('todo jsCode gerado e sintaticamente valido (pega identificador duplicado, chaves erradas, etc.)', () => {
|
|
const wf = buildWorkflow();
|
|
for (const node of wf.nodes) {
|
|
if (node.type === 'n8n-nodes-base.code') {
|
|
assert.doesNotThrow(() => {
|
|
// eslint-disable-next-line no-new-func
|
|
new Function('$input', '$env', '$', node.parameters.jsCode);
|
|
}, `node ${node.name} tem jsCode com erro de sintaxe`);
|
|
}
|
|
}
|
|
});
|
|
|
|
test('nenhum jsCode gerado contem "require(" sobrando (modulos devem estar totalmente inlined)', () => {
|
|
const wf = buildWorkflow();
|
|
for (const node of wf.nodes) {
|
|
if (node.type === 'n8n-nodes-base.code') {
|
|
assert.ok(!/\brequire\(/.test(node.parameters.jsCode), `node ${node.name} ainda tem require() nao resolvido`);
|
|
}
|
|
}
|
|
});
|
|
|
|
test('workflow gerado NAO tem nenhum node de webhook de debug', () => {
|
|
const wf = buildWorkflow();
|
|
const names = wf.nodes.map((n) => n.name);
|
|
assert.ok(!names.includes('Debug - Ver Painel'));
|
|
assert.ok(!names.includes('Debug - Responder'));
|
|
assert.ok(!names.includes('Code - Montar Painel Debug'));
|
|
const webhookNodes = wf.nodes.filter((n) => n.type === 'n8n-nodes-base.webhook');
|
|
assert.equal(webhookNodes.length, 0, 'nenhum node webhook deveria sobrar no workflow gerado');
|
|
});
|
|
|
|
test('glpi_base_url no Config nao e mais o placeholder do V1', () => {
|
|
const wf = buildWorkflow();
|
|
const configNode = wf.nodes.find((n) => n.name === 'Config - Coordenador GLPI v2');
|
|
const glpiField = configNode.parameters.assignments.assignments.find((a) => a.name === 'glpi_base_url');
|
|
assert.notEqual(glpiField.value, 'https://glpi.suaempresa.com.br');
|
|
assert.equal(glpiField.value, 'https://glpi.gtigrupo.com.br');
|
|
});
|
|
|
|
test('link_glpi na query de candidatos usa a variavel de config, nao um dominio fixo no SQL', () => {
|
|
const fs = require('node:fs');
|
|
const path = require('node:path');
|
|
const sql = fs.readFileSync(path.join(__dirname, '..', 'sql', 'candidatos_v2_glpi.sql'), 'utf8');
|
|
assert.ok(sql.includes("CONCAT('{GLPI_BASE_URL}'"), 'o SQL deve continuar usando o placeholder substituido pelo node, nunca um dominio hardcoded');
|
|
assert.ok(!sql.includes('suaempresa.com.br'));
|
|
assert.ok(!sql.includes('gtigrupo.com.br'), 'o dominio real deve vir so da config, nunca hardcoded no SQL');
|
|
});
|