Files
SLACK_COORD_RELATORIO/logic/score.test.js
T
GustavoandClaude Sonnet 5 e0f4b9a5d0 fix(v2): WAITING_*/blocked classes never enter Top10 regardless of contextChanged
Real bug confirmed in the prior cycle's Run A: WAITING_VENDOR tickets
entered the Top10 solely because contextChanged=true (a fresh AI call
happened this run) -- contextChanged should only decide whether to
reanalyze (logic/cache.js), never whether an already-still-blocked
ticket can compete.

logic/cooldown.js: removed the `&& !contextChanged` condition from the
BLOCKING_CLASSIFICATIONS check in computeCooldown -- a ticket whose
CURRENT classification (fresh or cached) is in BLOCKING_CLASSIFICATIONS
is now excluded unconditionally. Also expanded BLOCKING_CLASSIFICATIONS
to the full list required this cycle: WAITING_REQUESTER, WAITING_VENDOR,
WAITING_APPROVAL, SCHEDULED, DEPENDENCY_BLOCKED, PROJECT_LONG_TERM,
DUPLICATE_OR_OBSOLETE (WAITING_APPROVAL/PROJECT_LONG_TERM/
DUPLICATE_OR_OBSOLETE previously only had a score penalty, not exclusion
-- WAITING_APPROVAL has positive base score points, so it could
genuinely win a Top10 slot before this fix). The critical-override escape
hatch (SLA vencido + prioridade alta, etc.) is untouched -- still the
only way a blocked classification can surface.

logic/cooldown.test.js: rewrote the one test that was actually asserting
the buggy behavior ("mudanca de contexto real desbloqueia mesmo em
estado de espera" -> now proves the opposite, with a comment explaining
why); added tests for the chronology-driven unblock case (ticket moves
out of a blocking classification entirely) and the two newly-blocking
classes.
logic/score.test.js: added end-to-end tests through computeScore()
proving WAITING_VENDOR and WAITING_APPROVAL are excludedFromTop10 even
with contextChanged=true.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-24 20:50:12 -03:00

103 lines
6.1 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');
});
test('BUG REAL comprovado na Run A anterior: WAITING_VENDOR fica fora do Top10 mesmo com contextChanged=true -- missao "finalizacao para producao" secao 10', () => {
const result = computeScore({ ticket: ticket({}), aiClassification: ai({ classification: 'WAITING_VENDOR', actionable_today: false, confidence: 0.8 }), memory: null, contextChanged: true, config: CONFIG, now: NOW });
assert.equal(result.excludedFromTop10, true);
assert.equal(result.exclusionReason, 'blocked_waiting_vendor');
});
test('WAITING_APPROVAL (pontuacao base positiva) tambem fica fora do Top10 -- antes so tinha penalidade de score, nao exclusao', () => {
const result = computeScore({ ticket: ticket({}), aiClassification: ai({ classification: 'WAITING_APPROVAL', actionable_today: false, confidence: 0.8 }), memory: null, contextChanged: true, config: CONFIG, now: NOW });
assert.equal(result.excludedFromTop10, true);
});