107 lines
3.8 KiB
JavaScript
107 lines
3.8 KiB
JavaScript
const mockSnaps = { current: [] };
|
|
jest.mock('../../src/utils/supabase', () => ({
|
|
getSupabaseServiceClient: () => ({
|
|
from() {
|
|
const proxy = {
|
|
select() { return proxy; },
|
|
eq() { return proxy; },
|
|
order() { return Promise.resolve({ data: mockSnaps.current, error: null }); },
|
|
};
|
|
return proxy;
|
|
},
|
|
}),
|
|
}));
|
|
|
|
const lm = require('../../src/services/intelligence/lineMovement');
|
|
|
|
beforeEach(() => {
|
|
mockSnaps.current = [];
|
|
});
|
|
|
|
describe('getLineMovement', () => {
|
|
test('returns null when fewer than two snapshots', async () => {
|
|
mockSnaps.current = [{ line: 25.5, over_odds: -110, under_odds: -110, snapshot_at: 't1' }];
|
|
expect(await lm.getLineMovement('g', 'P', 'points')).toBeNull();
|
|
});
|
|
|
|
test('reports opening and current line correctly', async () => {
|
|
mockSnaps.current = [
|
|
{ line: 25.5, over_odds: -110, under_odds: -110, snapshot_at: 't1' },
|
|
{ line: 25.5, over_odds: -120, under_odds: +100, snapshot_at: 't2' },
|
|
{ line: 26.0, over_odds: -130, under_odds: +110, snapshot_at: 't3' },
|
|
];
|
|
const result = await lm.getLineMovement('g', 'P', 'points');
|
|
expect(result).toMatchObject({
|
|
opening_line: 25.5,
|
|
current_line: 26.0,
|
|
movement: 0.5,
|
|
direction: 'up',
|
|
snapshots_count: 3,
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('reverseLineMovement', () => {
|
|
test('detects RLM when public is on over but line moves to under', async () => {
|
|
mockSnaps.current = [
|
|
{ line: 25.5, over_odds: -110, under_odds: -110, snapshot_at: 't1' },
|
|
{ line: 24.5, over_odds: -130, under_odds: +110, snapshot_at: 't2' },
|
|
];
|
|
const r = await lm.reverseLineMovement('g', 'P', 'points');
|
|
expect(r.isReverse).toBe(true);
|
|
expect(r.score).toBeGreaterThan(0);
|
|
expect(r.publicSide).toBe('over');
|
|
expect(r.lineDirection).toBe('under');
|
|
});
|
|
|
|
test('returns isReverse=false when line moved with public', async () => {
|
|
mockSnaps.current = [
|
|
{ line: 25.5, over_odds: -110, under_odds: -110, snapshot_at: 't1' },
|
|
{ line: 26.5, over_odds: -130, under_odds: +110, snapshot_at: 't2' },
|
|
];
|
|
const r = await lm.reverseLineMovement('g', 'P', 'points');
|
|
expect(r.isReverse).toBe(false);
|
|
expect(r.score).toBe(0);
|
|
});
|
|
|
|
test('returns null on flat movement', async () => {
|
|
mockSnaps.current = [
|
|
{ line: 25.5, over_odds: -110, under_odds: -110, snapshot_at: 't1' },
|
|
{ line: 25.5, over_odds: -115, under_odds: -105, snapshot_at: 't2' },
|
|
];
|
|
expect(await lm.reverseLineMovement('g', 'P', 'points')).toBeNull();
|
|
});
|
|
|
|
test('uses provided publicBetPct over odds-direction heuristic', async () => {
|
|
mockSnaps.current = [
|
|
{ line: 25.5, over_odds: -110, under_odds: -110, snapshot_at: 't1' },
|
|
{ line: 26.5, over_odds: -110, under_odds: -110, snapshot_at: 't2' },
|
|
];
|
|
const r = await lm.reverseLineMovement('g', 'P', 'points', 30);
|
|
expect(r.isReverse).toBe(true);
|
|
expect(r.publicSide).toBe('under');
|
|
expect(r.lineDirection).toBe('over');
|
|
});
|
|
});
|
|
|
|
describe('juiceDegradation', () => {
|
|
test('flags juice change when the line itself stayed put', async () => {
|
|
mockSnaps.current = [
|
|
{ line: 25.5, over_odds: -110, under_odds: -110, snapshot_at: 't1' },
|
|
{ line: 25.5, over_odds: -130, under_odds: +110, snapshot_at: 't2' },
|
|
];
|
|
const r = await lm.juiceDegradation('g', 'P', 'points');
|
|
expect(r.applicable).toBe(true);
|
|
expect(r.score).toBeGreaterThan(0);
|
|
});
|
|
|
|
test('not applicable when line moved significantly', async () => {
|
|
mockSnaps.current = [
|
|
{ line: 25.5, over_odds: -110, under_odds: -110, snapshot_at: 't1' },
|
|
{ line: 28.5, over_odds: -120, under_odds: +100, snapshot_at: 't2' },
|
|
];
|
|
const r = await lm.juiceDegradation('g', 'P', 'points');
|
|
expect(r.applicable).toBe(false);
|
|
});
|
|
});
|