applyFallbackPolicy (new, in logic/cooldown.js alongside the isCriticalOverride it reuses) is now the single place that decides whether an AI_FALLBACK or INSUFFICIENT_CONTEXT ticket can enter the Top10: blocked by default, let through only via an explicit critical override (SLA vencido + prioridade/ urgencia alta, OR sem_tecnico + prioridade/urgencia alta -- isCriticalOverride extended to cover both, previously only covered SLA+priority). score.js calls this policy and sets excludedFromTop10/exclusionReason/decisionReason accordingly, with an auditable *_critical_override or *_no_override reason. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
92 lines
4.9 KiB
JavaScript
92 lines
4.9 KiB
JavaScript
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('mudanca de contexto real desbloqueia mesmo em estado de espera', () => {
|
|
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, false);
|
|
});
|
|
|
|
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);
|
|
});
|