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
5.3 KiB
JavaScript
92 lines
5.3 KiB
JavaScript
const { test } = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
const { computeScore } = require('./score');
|
|
|
|
const CONFIG = { aiConfidenceThreshold: 0.6, cooldownDays: 1 };
|
|
const NOW = new Date('2026-08-24T08:00:00Z').getTime();
|
|
|
|
function ticket(overrides) {
|
|
return Object.assign({
|
|
id: 1, sla_vencido: false, sem_tecnico: false, dias_sem_interacao: 5, dias_aberto: 5,
|
|
prioridade: 3, urgencia: 3, score_criticidade: 100,
|
|
}, overrides);
|
|
}
|
|
|
|
function ai(overrides) {
|
|
return Object.assign({
|
|
classification: 'ACTIONABLE_NOW', actionable_today: true, confidence: 0.9,
|
|
is_fallback: false, reason: 'x', evidence: [],
|
|
}, overrides);
|
|
}
|
|
|
|
test('chamado acionavel + SLA vencido + prioridade alta cai no grupo critico_acionavel com score alto', () => {
|
|
const t = ticket({ sla_vencido: true, prioridade: 5, urgencia: 5 });
|
|
const result = computeScore({ ticket: t, aiClassification: ai({}), memory: null, contextChanged: true, config: CONFIG, now: NOW });
|
|
assert.equal(result.group, 'critico_acionavel');
|
|
assert.ok(result.score > 200);
|
|
assert.equal(result.excludedFromTop10, false);
|
|
});
|
|
|
|
test('WAITING_REQUESTER sem mudanca nao monopoliza (score baixo)', () => {
|
|
const t = ticket({});
|
|
const acionavel = computeScore({ ticket: t, aiClassification: ai({}), memory: null, contextChanged: true, config: CONFIG, now: NOW });
|
|
const esperando = computeScore({ ticket: t, aiClassification: ai({ classification: 'WAITING_REQUESTER' }), memory: null, contextChanged: true, config: CONFIG, now: NOW });
|
|
assert.ok(esperando.score < acionavel.score);
|
|
});
|
|
|
|
test('SCHEDULED nao entra antes da data (excluido do Top10)', () => {
|
|
const result = computeScore({ ticket: ticket({}), aiClassification: ai({ classification: 'SCHEDULED' }), memory: null, contextChanged: true, config: CONFIG, now: NOW });
|
|
assert.equal(result.excludedFromTop10, true);
|
|
assert.equal(result.exclusionReason, 'scheduled_not_due');
|
|
});
|
|
|
|
test('sem tecnico e acionavel recebe bonus e grupo sem_tecnico', () => {
|
|
const t = ticket({ sem_tecnico: true, sla_vencido: false });
|
|
const result = computeScore({ ticket: t, aiClassification: ai({ classification: 'UNASSIGNED_ACTIONABLE' }), memory: null, contextChanged: true, config: CONFIG, now: NOW });
|
|
assert.equal(result.group, 'sem_tecnico');
|
|
assert.ok(result.breakdown.semTecnicoBonus > 0);
|
|
});
|
|
|
|
test('confianca baixa reduz o peso da acionabilidade', () => {
|
|
const t = ticket({});
|
|
const altaConfianca = computeScore({ ticket: t, aiClassification: ai({ confidence: 0.9 }), memory: null, contextChanged: true, config: CONFIG, now: NOW });
|
|
const baixaConfianca = computeScore({ ticket: t, aiClassification: ai({ confidence: 0.2 }), memory: null, contextChanged: true, config: CONFIG, now: NOW });
|
|
assert.ok(baixaConfianca.breakdown.actionabilityPoints < altaConfianca.breakdown.actionabilityPoints);
|
|
});
|
|
|
|
test('NEEDS_COORDINATION e excluido do Top10 diario', () => {
|
|
const result = computeScore({ ticket: ticket({}), aiClassification: ai({ classification: 'NEEDS_COORDINATION' }), memory: null, contextChanged: true, config: CONFIG, now: NOW });
|
|
assert.equal(result.excludedFromTop10, true);
|
|
});
|
|
|
|
test('fallback nao recebe bonus de acionabilidade (comportamento seguro tipo V1)', () => {
|
|
const result = computeScore({ ticket: ticket({}), aiClassification: ai({ classification: 'AI_FALLBACK', is_fallback: true }), memory: null, contextChanged: true, config: CONFIG, now: NOW });
|
|
assert.equal(result.breakdown.actionabilityPoints, 0);
|
|
});
|
|
|
|
test('AI_FALLBACK comum e excluido do Top10 (nao pode competir como se fosse acionavel confirmado)', () => {
|
|
const result = computeScore({ ticket: ticket({}), aiClassification: ai({ classification: 'AI_FALLBACK', is_fallback: true, confidence: 0 }), memory: null, contextChanged: true, config: CONFIG, now: NOW });
|
|
assert.equal(result.excludedFromTop10, true);
|
|
assert.equal(result.exclusionReason, 'ai_fallback_no_override');
|
|
});
|
|
|
|
test('AI_FALLBACK em chamado critico (SLA vencido + prioridade alta) entra por excecao explicita', () => {
|
|
const critico = ticket({ sla_vencido: true, prioridade: 5, urgencia: 5 });
|
|
const result = computeScore({ ticket: critico, aiClassification: ai({ classification: 'AI_FALLBACK', is_fallback: true, confidence: 0 }), memory: null, contextChanged: true, config: CONFIG, now: NOW });
|
|
assert.equal(result.excludedFromTop10, false);
|
|
assert.equal(result.decisionReason, 'ai_fallback_critical_override');
|
|
});
|
|
|
|
test('INSUFFICIENT_CONTEXT comum fica fora da competicao operacional normal (excluido do Top10)', () => {
|
|
const result = computeScore({ ticket: ticket({}), aiClassification: ai({ classification: 'INSUFFICIENT_CONTEXT', confidence: 0.3 }), memory: null, contextChanged: true, config: CONFIG, now: NOW });
|
|
assert.equal(result.excludedFromTop10, true);
|
|
assert.equal(result.exclusionReason, 'insufficient_context_no_override');
|
|
});
|
|
|
|
test('INSUFFICIENT_CONTEXT critico entra somente pela excecao definida', () => {
|
|
const critico = ticket({ sem_tecnico: true, prioridade: 4 });
|
|
const result = computeScore({ ticket: critico, aiClassification: ai({ classification: 'INSUFFICIENT_CONTEXT', confidence: 0.3 }), memory: null, contextChanged: true, config: CONFIG, now: NOW });
|
|
assert.equal(result.excludedFromTop10, false);
|
|
assert.equal(result.decisionReason, 'insufficient_context_critical_override');
|
|
});
|