Files
vyndr/tests/unit/accuracyTracker.test.js
T

119 lines
4.4 KiB
JavaScript

const mockState = { rows: new Map(), upserts: [] };
function mockKeyOf(s, g, p) { return `${s}|${g}|${p}`; }
jest.mock('../../src/utils/supabase', () => ({
getSupabaseServiceClient: () => ({
from() {
const ctx = { filters: {} };
const proxy = {
select() { return proxy; },
eq(col, val) { ctx.filters[col] = val; return proxy; },
maybeSingle() {
const row = mockState.rows.get(mockKeyOf(ctx.filters.sport, ctx.filters.grade, ctx.filters.period));
return Promise.resolve({ data: row || null, error: null });
},
upsert(row) {
mockState.upserts.push(row);
mockState.rows.set(mockKeyOf(row.sport, row.grade, row.period), { ...row });
return Promise.resolve({ error: null });
},
then(resolve) {
// For getAccuracyDashboard — list all rows with period filter.
const period = ctx.filters.period;
const all = Array.from(mockState.rows.values()).filter((r) => !period || r.period === period);
return resolve({ data: all, error: null });
},
};
return proxy;
},
}),
}));
const at = require('../../src/services/intelligence/accuracyTracker');
beforeEach(() => {
mockState.rows.clear();
mockState.upserts.length = 0;
});
describe('accuracyTracker.recordResolution', () => {
test('increments hit + total_graded across all three periods', async () => {
await at.recordResolution('nba', 'A', 'hit');
expect(mockState.upserts).toHaveLength(3); // all_time + 30d + 7d
for (const u of mockState.upserts) {
expect(u.total_graded).toBe(1);
expect(u.total_hit).toBe(1);
expect(u.total_miss).toBe(0);
}
});
test('push/void don\'t count toward hit rate', async () => {
await at.recordResolution('nba', 'A', 'push');
await at.recordResolution('nba', 'A', 'void');
const row = mockState.rows.get('nba|A|all_time');
expect(row.total_hit).toBe(0);
expect(row.total_miss).toBe(0);
expect(row.total_push).toBe(1);
expect(row.total_void).toBe(1);
expect(row.hit_rate).toBeNull();
});
test('hit_rate is hits/(hits+misses) only', async () => {
for (let i = 0; i < 7; i += 1) await at.recordResolution('nba', 'A', 'hit');
for (let i = 0; i < 3; i += 1) await at.recordResolution('nba', 'A', 'miss');
const row = mockState.rows.get('nba|A|all_time');
expect(row.hit_rate).toBeCloseTo(0.7, 5);
});
});
describe('accuracyTracker — baseline lock', () => {
test('baseline locks at 100 decisive resolutions', async () => {
for (let i = 0; i < 99; i += 1) await at.recordResolution('nba', 'A', 'hit');
let row = mockState.rows.get('nba|A|all_time');
expect(row.baseline_locked).toBe(false);
await at.recordResolution('nba', 'A', 'hit');
row = mockState.rows.get('nba|A|all_time');
expect(row.baseline_locked).toBe(true);
expect(row.baseline_hit_rate).toBe(1.0);
});
test('only locks the all_time period', async () => {
for (let i = 0; i < 100; i += 1) await at.recordResolution('nba', 'A', 'hit');
expect(mockState.rows.get('nba|A|all_time').baseline_locked).toBe(true);
expect(mockState.rows.get('nba|A|last_30d').baseline_locked).toBe(false);
});
test('pushes do not contribute to baseline trigger', async () => {
for (let i = 0; i < 99; i += 1) await at.recordResolution('nba', 'A', 'hit');
await at.recordResolution('nba', 'A', 'push');
expect(mockState.rows.get('nba|A|all_time').baseline_locked).toBe(false);
});
});
describe('accuracyTracker.getAccuracy', () => {
test('reports expected hit rate by grade', async () => {
const r = await at.getAccuracy('nba', 'A+', 'all_time');
expect(r.expected).toBe(0.65);
});
test('returns deltas relative to baseline once locked', async () => {
for (let i = 0; i < 80; i += 1) await at.recordResolution('nba', 'A', 'hit');
for (let i = 0; i < 20; i += 1) await at.recordResolution('nba', 'A', 'miss');
const r = await at.getAccuracy('nba', 'A', 'all_time');
expect(r.locked).toBe(true);
expect(r.baseline).toBeCloseTo(0.8, 5);
expect(r.delta).toBe(0);
});
});
describe('accuracyTracker.getAllAccuracy', () => {
test('returns one entry per known grade tier', async () => {
const all = await at.getAllAccuracy('nba');
const tiers = all.map((r) => r.grade);
expect(tiers).toContain('A+');
expect(tiers).toContain('F');
expect(all.length).toBe(Object.keys(at.EXPECTED_HIT_RATES).length);
});
});