const { test } = require('node:test'); const assert = require('node:assert/strict'); const { validateAiClassification, VALID_CLASSIFICATIONS, AI_CLASSIFIER_VERSION } = require('./classification'); function validRaw(overrides) { return Object.assign({ classification: 'ACTIONABLE_NOW', actionable_today: true, confidence: 0.91, blocker_type: null, waiting_for: null, next_action: 'Validar equipamento e registrar resultado.', suggested_action_type: 'technical_action', business_risk: 'medium', reason: 'Existe acao tecnica pendente.', evidence: ['ultimo apontamento indica necessidade de validacao'], }, overrides); } test('objeto valido passa e normaliza campos', () => { const result = validateAiClassification(validRaw({})); assert.equal(result.valid, true); assert.equal(result.data.classification, 'ACTIONABLE_NOW'); assert.equal(result.data.is_fallback, false); }); test('classification invalida falha com erro claro', () => { const result = validateAiClassification(validRaw({ classification: 'ALGO_INVENTADO' })); assert.equal(result.valid, false); assert.ok(result.errors.some((e) => e.includes('classification'))); }); test('JSON invalido/null nao lanca excecao', () => { assert.doesNotThrow(() => validateAiClassification(null)); assert.doesNotThrow(() => validateAiClassification('nao e objeto')); assert.equal(validateAiClassification(null).valid, false); }); test('next_action vazia ou so espacos falha (nao vazia e requisito minimo de qualidade)', () => { assert.equal(validateAiClassification(validRaw({ next_action: '' })).valid, false); assert.equal(validateAiClassification(validRaw({ next_action: ' ' })).valid, false); }); test('confidence fora de 0..1 falha', () => { const result = validateAiClassification(validRaw({ confidence: 1.5 })); assert.equal(result.valid, false); }); test('evidence e limitado a 5 itens', () => { const result = validateAiClassification(validRaw({ evidence: ['a', 'b', 'c', 'd', 'e', 'f', 'g'] })); assert.equal(result.data.evidence.length, 5); }); test('todas as classes do dominio sao aceitas', () => { for (const classification of VALID_CLASSIFICATIONS) { const result = validateAiClassification(validRaw({ classification })); assert.equal(result.valid, true, `classe ${classification} deveria ser valida`); } }); test('AI_FALLBACK nao esta no dominio de classes que a IA pode retornar', () => { assert.ok(!VALID_CLASSIFICATIONS.includes('AI_FALLBACK')); }); test('blocker_type/waiting_for como string "null" sao normalizados para null de verdade', () => { const result = validateAiClassification(validRaw({ blocker_type: 'null', waiting_for: 'None' })); assert.equal(result.valid, true); assert.equal(result.data.blocker_type, null); assert.equal(result.data.waiting_for, null); }); test('blocker_type/waiting_for como "n/a" ou string vazia tambem viram null', () => { const result = validateAiClassification(validRaw({ blocker_type: 'N/A', waiting_for: '' })); assert.equal(result.data.blocker_type, null); assert.equal(result.data.waiting_for, null); }); test('blocker_type/waiting_for com valor real nao sao afetados pela normalizacao', () => { const result = validateAiClassification(validRaw({ blocker_type: 'fornecedor', waiting_for: 'Patricia' })); assert.equal(result.data.blocker_type, 'fornecedor'); assert.equal(result.data.waiting_for, 'Patricia'); }); test('AI_CLASSIFIER_VERSION esta definida e e carimbada em toda classificacao valida', () => { assert.equal(typeof AI_CLASSIFIER_VERSION, 'number'); const result = validateAiClassification(validRaw({})); assert.equal(result.data.classifier_version, AI_CLASSIFIER_VERSION); }); test('tecnico interno (aparece em TECNICO do chamado) nunca vira WAITING_VENDOR -- missao secao 6', () => { const ticket = { tecnico_atribuido: 'Rafael Brito' }; const result = validateAiClassification( validRaw({ classification: 'WAITING_VENDOR', waiting_for: 'Rafael Brito', actionable_today: false }), ticket, ); assert.equal(result.valid, false); assert.ok(result.errors.some((e) => e.includes('tecnico interno'))); }); test('WAITING_VENDOR com fornecedor externo de verdade (nao aparece em TECNICO) continua valido', () => { const ticket = { tecnico_atribuido: 'Rafael Brito' }; const result = validateAiClassification( validRaw({ classification: 'WAITING_VENDOR', waiting_for: 'fornecimento de baterias', actionable_today: false }), ticket, ); assert.equal(result.valid, true); }); test('WAITING_VENDOR sem ticket informado (uso legado/sem contexto) nao quebra a validacao', () => { const result = validateAiClassification( validRaw({ classification: 'WAITING_VENDOR', waiting_for: 'fornecedor XPTO', actionable_today: false }), ); assert.equal(result.valid, true); }); test('next_action generico ("Revisar chamado") e rejeitado para ACTIONABLE_NOW -- missao secao 9', () => { const result = validateAiClassification(validRaw({ classification: 'ACTIONABLE_NOW', next_action: 'Revisar chamado' })); assert.equal(result.valid, false); assert.ok(result.errors.some((e) => e.includes('generico'))); }); test('next_action generico ("Acompanhar") e rejeitado para UNASSIGNED_ACTIONABLE', () => { const result = validateAiClassification(validRaw({ classification: 'UNASSIGNED_ACTIONABLE', next_action: 'Acompanhar', actionable_today: true })); assert.equal(result.valid, false); }); test('next_action concreto ("Realizar inventario das maquinas") passa para ACTIONABLE_NOW', () => { const result = validateAiClassification(validRaw({ classification: 'ACTIONABLE_NOW', next_action: 'Realizar inventário das máquinas' })); assert.equal(result.valid, true); }); test('next_action curto tipo "aguardar resposta" e permitido em classes WAITING_* (nao exige verbo de acao do suporte)', () => { const result = validateAiClassification(validRaw({ classification: 'WAITING_REQUESTER', next_action: 'Aguardar resposta do cliente', actionable_today: false })); assert.equal(result.valid, true); });