Files
SLACK_COORD_RELATORIO/logic/cooldown.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

119 lines
6.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('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);
});