'use strict'; const test = require('node:test'); const assert = require('node:assert/strict'); const { redact, truncate, buildTicketContext, buildJsonSchema, parseAiResponse, } = require('../src/v2/aiSchema'); const { loadConfig } = require('../src/v2/config'); const config = loadConfig(); test('redact remove padroes comuns de segredo antes de mandar para a IA', () => { const texto = 'acesso com senha: abc123 e token=xyz999'; const limpo = redact(texto); assert.doesNotMatch(limpo, /abc123/); assert.doesNotMatch(limpo, /xyz999/); assert.match(limpo, /\[REDACTED\]/); }); test('truncate corta texto longo e mantem texto curto intacto', () => { assert.equal(truncate('abc', 10), 'abc'); assert.equal(truncate('a'.repeat(20), 5), 'aaaaa…'); }); test('buildTicketContext respeita AI_MAX_TICKETS', () => { const cfg = loadConfig({ AI_MAX_TICKETS: 3 }); const tickets = Array.from({ length: 10 }, (_, i) => ({ id: i + 1, title: `Chamado ${i}`, priority: 3, opened_at: '2026-08-20', })); const ctx = buildTicketContext(tickets, cfg); assert.equal(ctx.tickets.length, 3); assert.equal(ctx.total_disponivel, 10); assert.equal(ctx.truncado, true); }); test('buildTicketContext prioriza prioridade mais alta quando precisa cortar', () => { const cfg = loadConfig({ AI_MAX_TICKETS: 1 }); const tickets = [ { id: 1, title: 'baixa', priority: 1, opened_at: '2026-08-20' }, { id: 2, title: 'alta', priority: 5, opened_at: '2026-08-19' }, ]; const ctx = buildTicketContext(tickets, cfg); assert.equal(ctx.tickets[0].id, 2); }); test('buildTicketContext nunca ultrapassa AI_MAX_CONTEXT_CHARS mesmo com muitos tickets pequenos', () => { const cfg = loadConfig({ AI_MAX_TICKETS: 1000, AI_MAX_CONTEXT_CHARS: 500 }); const tickets = Array.from({ length: 200 }, (_, i) => ({ id: i + 1, title: `Chamado numero ${i} com um titulo mediano para ocupar espaco`, description: 'descricao razoavelmente longa para ajudar a estourar o limite de contexto configurado', priority: 3, opened_at: '2026-08-20', })); const ctx = buildTicketContext(tickets, cfg); assert.ok(JSON.stringify(ctx.tickets).length <= cfg.AI_MAX_CONTEXT_CHARS); assert.ok(ctx.tickets.length < 200); }); test('buildTicketContext nao envia campos alem do minimo necessario (minimizacao de contexto)', () => { const ctx = buildTicketContext( [{ id: 1, title: 'x', priority: 1, opened_at: '2026-08-20', extraCampoSensivel: 'nao deveria ir' }], config ); assert.equal(ctx.tickets[0].extraCampoSensivel, undefined); }); test('buildJsonSchema define os tipos de insight da missao e exige evidencia', () => { const schema = buildJsonSchema(); assert.equal(schema.strict, true); assert.ok(schema.schema.properties.insights.items.required.includes('evidence_ticket_ids')); assert.ok(schema.schema.properties.insights.items.properties.type.enum.includes('CATEGORY_SPIKE')); }); test('parseAiResponse aceita JSON valido', () => { const r = parseAiResponse(JSON.stringify({ period_assessment: { status: 'stable', summary: 'ok' }, insights: [] })); assert.equal(r.ok, true); assert.equal(r.data.period_assessment.status, 'stable'); }); test('parseAiResponse tolera cercas de markdown ```json ... ```', () => { const bruto = '```json\n{"period_assessment":{"status":"stable","summary":"ok"},"insights":[]}\n```'; const r = parseAiResponse(bruto); assert.equal(r.ok, true); }); test('parseAiResponse nunca lanca excecao com JSON quebrado, so retorna ok:false', () => { assert.doesNotThrow(() => { const r = parseAiResponse('{ isso nao é json valido'); assert.equal(r.ok, false); assert.match(r.error, /json_invalido/); }); }); test('parseAiResponse rejeita resposta vazia/undefined sem lancar excecao', () => { assert.doesNotThrow(() => { assert.equal(parseAiResponse(undefined).ok, false); assert.equal(parseAiResponse('').ok, false); assert.equal(parseAiResponse(null).ok, false); }); }); test('parseAiResponse rejeita objeto sem o formato esperado (falta insights[])', () => { const r = parseAiResponse(JSON.stringify({ foo: 'bar' })); assert.equal(r.ok, false); assert.equal(r.error, 'formato_inesperado'); });