96 lines
3.7 KiB
JavaScript
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);
|
|
});
|
|
});
|