'use strict'; const test = require('node:test'); const assert = require('node:assert/strict'); const { slugify, normalizeSubjectKey, computeFingerprint, validateInsightShape, sanitizeInsights, nextMemoryState, nextStateForAbsentRecord, describeOccurrence, } = require('../src/v2/insights'); const { loadConfig } = require('../src/v2/config'); const config = loadConfig(); test('slugify remove acentos e caracteres especiais', () => { assert.equal(slugify('Emissão de NF-e'), 'emissao_de_nf_e'); assert.equal(slugify(''), 'geral'); assert.equal(slugify(undefined), 'geral'); }); test('normalizeSubjectKey aceita namespace explicito da IA', () => { assert.equal(normalizeSubjectKey('cliente:IMDI', 'CLIENT_CONCENTRATION'), 'cliente:imdi'); }); test('normalizeSubjectKey infere namespace pelo type quando IA nao manda um valido', () => { assert.equal(normalizeSubjectKey('Impressao', 'CATEGORY_SPIKE'), 'categoria:impressao'); assert.equal(normalizeSubjectKey('erro nota fiscal', 'RECURRENCE'), 'tema:erro_nota_fiscal'); }); test('computeFingerprint e deterministico e estavel', () => { const f1 = computeFingerprint('CATEGORY_SPIKE', 'categoria:impressao'); const f2 = computeFingerprint('CATEGORY_SPIKE', 'categoria:impressao'); assert.equal(f1, f2); assert.equal(f1, 'CATEGORY_SPIKE:categoria:impressao'); }); function insightValido(overrides) { return { type: 'CATEGORY_SPIKE', severity: 'medium', title: 'Hardware acima do padrao', summary: 'Volume de hardware ficou acima da referencia recente.', evidence_ticket_ids: [1, 2, 3], confidence: 0.9, reportable: true, subject_key: 'categoria:hardware', ...overrides, }; } test('validateInsightShape aceita um insight bem formado', () => { const r = validateInsightShape(insightValido()); assert.equal(r.valido, true); }); test('validateInsightShape rejeita type fora do enum', () => { const r = validateInsightShape(insightValido({ type: 'INVENTADO' })); assert.equal(r.valido, false); assert.ok(r.erros.includes('type_invalido')); }); test('validateInsightShape rejeita evidence_ticket_ids vazio', () => { const r = validateInsightShape(insightValido({ evidence_ticket_ids: [] })); assert.equal(r.valido, false); assert.ok(r.erros.includes('evidence_ticket_ids_invalido')); }); test('validateInsightShape rejeita confidence fora de 0..1', () => { const r = validateInsightShape(insightValido({ confidence: 1.5 })); assert.equal(r.valido, false); }); test('sanitizeInsights: JSON valido passa e recebe fingerprint', () => { const ctx = { validTicketIds: new Set([1, 2, 3]) }; const { accepted, suppressed } = sanitizeInsights([insightValido()], ctx, config); assert.equal(accepted.length, 1); assert.equal(suppressed.length, 0); assert.equal(accepted[0].fingerprint, 'CATEGORY_SPIKE:categoria:hardware'); }); test('sanitizeInsights: JSON invalido (schema quebrado) nunca quebra a automacao, so e suprimido', () => { const ctx = { validTicketIds: new Set([1]) }; assert.doesNotThrow(() => { const { accepted, suppressed } = sanitizeInsights( [{ nonsense: true }, 'string solta', null, 42], ctx, config ); assert.equal(accepted.length, 0); assert.equal(suppressed.length, 4); }); }); test('sanitizeInsights: insight com confianca abaixo do minimo nao aparece', () => { const ctx = { validTicketIds: new Set([1, 2, 3]) }; const { accepted, suppressed } = sanitizeInsights( [insightValido({ confidence: 0.2 })], ctx, config ); assert.equal(accepted.length, 0); assert.equal(suppressed[0].motivo, 'confianca_abaixo_do_minimo'); }); test('sanitizeInsights: evidencia com ticket id fora do contexto enviado a IA e suprimida (numero nao auditavel)', () => { const ctx = { validTicketIds: new Set([1, 2]) }; // ticket 3 nao foi enviado a IA const { accepted, suppressed } = sanitizeInsights([insightValido()], ctx, config); assert.equal(accepted.length, 0); assert.equal(suppressed[0].motivo, 'evidencia_fora_do_contexto_enviado'); }); test('sanitizeInsights: reportable=false fica suprimido mas auditavel', () => { const ctx = { validTicketIds: new Set([1, 2, 3]) }; const { accepted, suppressed } = sanitizeInsights( [insightValido({ reportable: false })], ctx, config ); assert.equal(accepted.length, 0); assert.equal(suppressed[0].motivo, 'nao_marcado_como_reportable'); }); test('sanitizeInsights respeita MAX_REPORT_INSIGHTS', () => { const ctx = { validTicketIds: new Set([1, 2, 3]) }; const cfg = loadConfig({ MAX_REPORT_INSIGHTS: 2 }); const seis = Array.from({ length: 6 }, (_, i) => insightValido({ subject_key: `categoria:tema_${i}`, confidence: 0.9 - i * 0.01 }) ); const { accepted, suppressed } = sanitizeInsights(seis, ctx, cfg); assert.equal(accepted.length, 2); assert.equal(suppressed.length, 4); assert.ok(suppressed.every((s) => s.motivo === 'limite_maximo_de_insights_atingido')); }); test('sanitizeInsights descarta duplicata de fingerprint na mesma execucao (evita spam no mesmo relatorio)', () => { const ctx = { validTicketIds: new Set([1, 2, 3]) }; const duplicado = [insightValido(), insightValido({ title: 'Outro titulo, mesmo tema' })]; const { accepted, suppressed } = sanitizeInsights(duplicado, ctx, config); assert.equal(accepted.length, 1); assert.equal(suppressed[0].motivo, 'fingerprint_duplicado_na_mesma_execucao'); }); test('nextMemoryState: primeira ocorrencia gera status new', () => { const s = nextMemoryState(null, '2026-08-18'); assert.equal(s.current_status, 'new'); assert.equal(s.occurrences, 1); assert.equal(s.first_seen_at, '2026-08-18'); }); test('nextMemoryState: ocorrencia repetida incrementa occurrences e vira persisting', () => { const anterior = { first_seen_at: '2026-08-18', occurrences: 1, current_status: 'new' }; const s = nextMemoryState(anterior, '2026-08-19'); assert.equal(s.current_status, 'persisting'); assert.equal(s.occurrences, 2); assert.equal(s.first_seen_at, '2026-08-18'); }); test('describeOccurrence varia a frase conforme o estado (nao repete a mesma frase todo dia)', () => { assert.equal(describeOccurrence({ current_status: 'new' }), 'identificado hoje'); assert.equal( describeOccurrence({ current_status: 'persisting', occurrences: 3 }), 'segue acima do padrao pelo 3o dia' ); assert.equal(describeOccurrence({ current_status: 'normalizing' }), 'voltou ao padrao'); assert.equal(describeOccurrence({ current_status: 'resolved' }), null); }); test('nextStateForAbsentRecord: insight que sumiu vira normalizing (reporta 1x)', () => { const anterior = { current_status: 'persisting', occurrences: 3, first_seen_at: '2026-08-17', last_seen_at: '2026-08-19' }; const s = nextStateForAbsentRecord(anterior, '2026-08-20'); assert.equal(s.current_status, 'normalizing'); }); test('nextStateForAbsentRecord: normalizing que continua ausente vira resolved (silencioso)', () => { const anterior = { current_status: 'normalizing', occurrences: 3, first_seen_at: '2026-08-17', last_seen_at: '2026-08-19' }; const s = nextStateForAbsentRecord(anterior, '2026-08-21'); assert.equal(s.current_status, 'resolved'); }); test('nextStateForAbsentRecord: resolved permanece resolved (nao volta a falar sozinho)', () => { const anterior = { current_status: 'resolved', occurrences: 3, first_seen_at: '2026-08-17', last_seen_at: '2026-08-19' }; const s = nextStateForAbsentRecord(anterior, '2026-08-25'); assert.equal(s.current_status, 'resolved'); });