Files
Gustavo Santos d21c4596e1 chore: import relatorio (subtree)
git-subtree-dir: relatorio
git-subtree-mainline: e322cec92b
git-subtree-split: 34bcb132f3
2026-10-01 14:26:02 -03:00

189 lines
7.4 KiB
JavaScript

'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');
});