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

96 lines
3.7 KiB
JavaScript

const mockState = { grade: null, closing: null, updated: [] };
jest.mock('../../src/utils/supabase', () => ({
getSupabaseServiceClient: () => ({
from(table) {
const proxy = {
select() { return proxy; },
eq() { return proxy; },
gte() { return Promise.resolve({ data: mockState.summaryRows || [], error: null }); },
not() { return proxy; },
maybeSingle() {
if (table === 'grade_history') return Promise.resolve({ data: mockState.grade, error: null });
if (table === 'closing_lines') return Promise.resolve({ data: mockState.closing, error: null });
return Promise.resolve({ data: null, error: null });
},
update(patch) {
mockState.updated.push({ table, patch });
return { eq() { return Promise.resolve({ error: null }); } };
},
then(resolve) { return resolve({ data: mockState.summaryRows || [], error: null }); },
};
return proxy;
},
}),
}));
const clv = require('../../src/services/intelligence/clvTracker');
beforeEach(() => {
mockState.grade = null;
mockState.closing = null;
mockState.summaryRows = [];
mockState.updated.length = 0;
});
describe('rawCLV', () => {
test('over: closing > graded → positive CLV', () => {
expect(clv.rawCLV('over', 25.5, 27.5)).toBe(2.0);
});
test('over: closing < graded → negative CLV', () => {
expect(clv.rawCLV('over', 25.5, 24.5)).toBe(-1.0);
});
test('under: closing < graded → positive CLV (we were right, line moved our way)', () => {
expect(clv.rawCLV('under', 25.5, 23.5)).toBe(2.0);
});
test('under: closing > graded → negative', () => {
expect(clv.rawCLV('under', 25.5, 27.5)).toBe(-2.0);
});
test('invalid input → null', () => {
expect(clv.rawCLV('over', null, 27)).toBeNull();
expect(clv.rawCLV('over', 25, 'abc')).toBeNull();
});
});
describe('computeCLV', () => {
test('returns null when grade not found', async () => {
mockState.grade = null;
const result = await clv.computeCLV('missing');
expect(result).toBeNull();
});
test('returns clv:null with reason when no closing line', async () => {
mockState.grade = { id: 'g1', game_id: 'gm', sport: 'nba', stat_type: 'points', line: 25.5, direction: 'over', player_id: '1' };
mockState.closing = null;
const result = await clv.computeCLV('g1');
expect(result.clv).toBeNull();
expect(result.reason).toBe('no_closing_line');
});
test('persists CLV when both lines exist', async () => {
mockState.grade = { id: 'g2', game_id: 'gm', sport: 'nba', stat_type: 'points', line: 25.5, direction: 'over', player_id: '1' };
mockState.closing = { id: 'cl-1', pinnacle_line: 27.5 };
const result = await clv.computeCLV('g2');
expect(result.clv).toBe(2.0);
expect(mockState.updated.some((u) => u.patch.clv === 2.0)).toBe(true);
expect(mockState.updated[0].patch.closing_line_id).toBe('cl-1');
});
test('falls back to player_name when player_id missing', async () => {
mockState.grade = { id: 'g3', game_id: 'gm', sport: 'nba', stat_type: 'points', line: 25.5, direction: 'under', player_id: null, player_name: 'Test' };
mockState.closing = { id: 'cl-2', pinnacle_line: 23.5 };
const result = await clv.computeCLV('g3');
expect(result.clv).toBe(2.0);
});
});
describe('batchComputeCLV', () => {
test('returns one entry per id, continues on individual failures', async () => {
mockState.grade = { id: 'gx', game_id: 'gm', sport: 'nba', stat_type: 'points', line: 25.5, direction: 'over', player_id: '1' };
mockState.closing = { id: 'cl-x', pinnacle_line: 26.5 };
const out = await clv.batchComputeCLV(['gx', 'gx', 'gx']);
expect(out).toHaveLength(3);
expect(out[0].clv).toBe(1.0);
});
});