const { test } = require('node:test'); const assert = require('node:assert/strict'); const { computeCooldown, applyFallbackPolicy, isCriticalOverride } = require('./cooldown'); const CONFIG = { cooldownDays: 1 }; const NOW = new Date('2026-08-24T08:00:00Z').getTime(); const ONE_DAY = 24 * 60 * 60 * 1000; function ticket(overrides) { return Object.assign({ sla_vencido: false, prioridade: 2, urgencia: 2 }, overrides); } test('WAITING_VENDOR sem mudanca de contexto fica bloqueado (nao repete diariamente)', () => { const memory = { consecutive_days_sent: 2, last_sent_at: NOW - ONE_DAY, next_review_at: NOW + ONE_DAY }; const result = computeCooldown({ ticket: ticket({}), classification: 'WAITING_VENDOR', contextChanged: false, memory, now: NOW, config: CONFIG }); assert.equal(result.blocked, true); }); test('enviado ontem sem mudanca recebe penalidade crescente', () => { const memory = { consecutive_days_sent: 3, last_sent_at: NOW - ONE_DAY, next_review_at: null }; const result = computeCooldown({ ticket: ticket({}), classification: 'ACTIONABLE_NOW', contextChanged: false, memory, now: NOW, config: CONFIG }); assert.ok(result.penalty > 0); assert.equal(result.blocked, false); }); test('BUG REAL corrigido: WAITING_VENDOR continua bloqueado do Top10 mesmo com contextChanged=true -- missao "finalizacao para producao" secao 10', () => { // Antes deste ciclo, contextChanged=true desbloqueava um chamado que CONTINUAVA // classificado como WAITING_VENDOR apos a reanalise -- foi exatamente o que aconteceu na // Run A anterior. contextChanged so deve decidir SE reanalisamos (logic/cache.js), nunca // SE um chamado ainda bloqueado pode competir no Top10. const memory = { consecutive_days_sent: 2, last_sent_at: NOW - ONE_DAY, next_review_at: NOW + ONE_DAY }; const result = computeCooldown({ ticket: ticket({}), classification: 'WAITING_VENDOR', contextChanged: true, memory, now: NOW, config: CONFIG }); assert.equal(result.blocked, true); assert.equal(result.reason, 'blocked_waiting_vendor'); }); test('chamado sai de WAITING_VENDOR para ACTIONABLE_NOW apos evento real (cronologia) -- deixa de ser bloqueado', () => { // A reclassificacao em si (peca chegou, virou ACTIONABLE_NOW) e responsabilidade do prompt/ // IA; aqui validamos que, uma vez que a classificacao ja NAO esta mais em // BLOCKING_CLASSIFICATIONS, o cooldown para de bloquear -- sem depender de contextChanged. const memory = { consecutive_days_sent: 2, last_sent_at: NOW - ONE_DAY, next_review_at: NOW + ONE_DAY }; const result = computeCooldown({ ticket: ticket({}), classification: 'ACTIONABLE_NOW', contextChanged: true, memory, now: NOW, config: CONFIG }); assert.equal(result.blocked, false); }); test('WAITING_APPROVAL tambem fica fora do Top10 incondicionalmente -- classe adicionada nesta missao', () => { const result = computeCooldown({ ticket: ticket({}), classification: 'WAITING_APPROVAL', contextChanged: true, memory: null, now: NOW, config: CONFIG }); assert.equal(result.blocked, true); assert.equal(result.reason, 'blocked_waiting_approval'); }); test('PROJECT_LONG_TERM e DUPLICATE_OR_OBSOLETE tambem ficam fora do Top10 incondicionalmente', () => { const r1 = computeCooldown({ ticket: ticket({}), classification: 'PROJECT_LONG_TERM', contextChanged: true, memory: null, now: NOW, config: CONFIG }); const r2 = computeCooldown({ ticket: ticket({}), classification: 'DUPLICATE_OR_OBSOLETE', contextChanged: true, memory: null, now: NOW, config: CONFIG }); assert.equal(r1.blocked, true); assert.equal(r2.blocked, true); }); test('NEEDS_COORDINATION sempre bloqueado para o Top 10 diario', () => { const result = computeCooldown({ ticket: ticket({}), classification: 'NEEDS_COORDINATION', contextChanged: false, memory: null, now: NOW, config: CONFIG }); assert.equal(result.blocked, true); assert.equal(result.reason, 'needs_coordination'); }); test('chamado critico (SLA vencido + prioridade alta) quebra o cooldown', () => { const memory = { consecutive_days_sent: 4, last_sent_at: NOW - ONE_DAY, next_review_at: NOW + ONE_DAY }; const critico = ticket({ sla_vencido: true, prioridade: 5 }); const result = computeCooldown({ ticket: critico, classification: 'WAITING_VENDOR', contextChanged: false, memory, now: NOW, config: CONFIG }); assert.equal(result.blocked, false); assert.equal(result.breakCooldown, true); }); test('sem memoria previa nunca bloqueia por cooldown', () => { const result = computeCooldown({ ticket: ticket({}), classification: 'ACTIONABLE_NOW', contextChanged: true, memory: null, now: NOW, config: CONFIG }); assert.equal(result.blocked, false); assert.equal(result.penalty, 0); }); test('sem tecnico + prioridade alta tambem conta como critico (mesmo sem SLA vencido)', () => { assert.equal(isCriticalOverride(ticket({ sem_tecnico: true, prioridade: 5, sla_vencido: false })), true); }); test('chamado comum (nao critico) NAO conta como override', () => { assert.equal(isCriticalOverride(ticket({ sla_vencido: true, prioridade: 2 })), false); assert.equal(isCriticalOverride(ticket({ sem_tecnico: true, prioridade: 2 })), false); }); test('AI_FALLBACK comum (nao critico) fica bloqueado por politica -- nao compete como acionavel confirmado', () => { const result = applyFallbackPolicy({ aiClassification: { is_fallback: true, classification: 'AI_FALLBACK' }, ticket: ticket({}) }); assert.equal(result.blocked, true); assert.equal(result.decisionReason, 'ai_fallback_no_override'); }); test('AI_FALLBACK critico (SLA vencido + prioridade alta) entra por excecao explicita', () => { const critico = ticket({ sla_vencido: true, prioridade: 5 }); const result = applyFallbackPolicy({ aiClassification: { is_fallback: true, classification: 'AI_FALLBACK' }, ticket: critico }); assert.equal(result.blocked, false); assert.equal(result.decisionReason, 'ai_fallback_critical_override'); }); test('INSUFFICIENT_CONTEXT comum fica fora da competicao operacional normal', () => { const result = applyFallbackPolicy({ aiClassification: { is_fallback: false, classification: 'INSUFFICIENT_CONTEXT' }, ticket: ticket({}) }); assert.equal(result.blocked, true); assert.equal(result.decisionReason, 'insufficient_context_no_override'); }); test('INSUFFICIENT_CONTEXT critico entra somente pela excecao definida', () => { const critico = ticket({ sem_tecnico: true, prioridade: 4 }); const result = applyFallbackPolicy({ aiClassification: { is_fallback: false, classification: 'INSUFFICIENT_CONTEXT' }, ticket: critico }); assert.equal(result.blocked, false); assert.equal(result.decisionReason, 'insufficient_context_critical_override'); }); test('classificacao real da IA (nao fallback, nao INSUFFICIENT_CONTEXT) nunca e bloqueada por esta politica', () => { const result = applyFallbackPolicy({ aiClassification: { is_fallback: false, classification: 'ACTIONABLE_NOW' }, ticket: ticket({}) }); assert.equal(result.blocked, false); assert.equal(result.decisionReason, null); });