/** * 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 15s–2min (magic) → +1.0', () => expect(inferReward('done', 60_000)).toBe(1.0)); it('done 2–10min (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); }); });