Files
oO/services/api/src/routes/__tests__/recommender.unit.test.ts
alvis c65bedcf68 feat(api): orchestrator cutover — replace bandit with multi-agent pipeline (ADR-0013 step 6)
POST /recommend now calls ml/serving /recommend with pre-computed agent
snippets + task context instead of /generate + /score/egreedy/v2. Falls
back to a random signal candidate when ml/serving is unavailable.

Removes: remotePolicy, fetchLlmCandidates, sendRewardWithRetry,
candidateCache, pickPromptVersion. Feedback handler keeps inferReward +
tipFeedback writes for observability; reward delivery to the bandit is gone.
tipScores.policy is now 'orchestrator'; promptVersion is 'v4-orchestrator'.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-04 10:37:15 +00:00

40 lines
2.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Pure-function unit tests for recommender logic — no DB, no HTTP.
* These can import directly from the module without any mocking.
*/
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import { inferReward, dueAgeDays } from '../recommender.js';
describe('inferReward', () => {
it('dismiss → -1', () => expect(inferReward('dismiss', null)).toBe(-1.0));
it('snooze → +0.1', () => expect(inferReward('snooze', null)).toBe(0.1));
it('helpful → +0.5', () => expect(inferReward('helpful', null)).toBe(0.5));
it('not_helpful → -0.5', () => expect(inferReward('not_helpful', null)).toBe(-0.5));
it('done with null dwell → +0.5', () => expect(inferReward('done', null)).toBe(0.5));
it('done < 15s (reflex) → -0.3', () => expect(inferReward('done', 5_000)).toBe(-0.3));
it('done 15s2min (magic) → +1.0', () => expect(inferReward('done', 60_000)).toBe(1.0));
it('done 210min (good) → +0.6', () => expect(inferReward('done', 300_000)).toBe(0.6));
it('done > 10min (eventual) → +0.3', () => expect(inferReward('done', 700_000)).toBe(0.3));
it('done exactly 15s (boundary) → magic zone', () => expect(inferReward('done', 15_000)).toBe(1.0));
it('done exactly 2min (boundary) → good zone', () => expect(inferReward('done', 120_000)).toBe(0.6));
});
describe('dueAgeDays', () => {
it('null due → 0', () => expect(dueAgeDays(null)).toBe(0));
it('empty object → 0', () => expect(dueAgeDays({})).toBe(0));
it('future date → 0 (clamped)', () => {
const future = new Date(Date.now() + 86_400_000).toISOString();
expect(dueAgeDays({ datetime: future })).toBe(0);
});
it('past date → positive age', () => {
const twoDaysAgo = new Date(Date.now() - 2 * 86_400_000).toISOString();
const age = dueAgeDays({ datetime: twoDaysAgo });
expect(age).toBeGreaterThan(1.9);
expect(age).toBeLessThan(2.1);
});
it('date-only field used when datetime absent', () => {
const yesterday = new Date(Date.now() - 86_400_000).toISOString().slice(0, 10);
expect(dueAgeDays({ date: yesterday })).toBeGreaterThan(0);
});
});