Sessions 5-7a: 955 tests, deployment ready
This commit is contained in:
@@ -0,0 +1,141 @@
|
||||
// Mock the two services trap detection talks to externally.
|
||||
|
||||
const mockLM = { reverse: null, juice: null, lm: null };
|
||||
jest.mock('../../src/services/intelligence/lineMovement', () => ({
|
||||
reverseLineMovement: async () => mockLM.reverse,
|
||||
juiceDegradation: async () => mockLM.juice,
|
||||
getLineMovement: async () => mockLM.lm,
|
||||
}));
|
||||
|
||||
const mockResolutions = { current: [] };
|
||||
jest.mock('../../src/utils/supabase', () => ({
|
||||
getSupabaseServiceClient: () => ({
|
||||
from() {
|
||||
const proxy = {
|
||||
select() { return proxy; },
|
||||
eq() { return proxy; },
|
||||
then(resolve) { return resolve({ data: mockResolutions.current, error: null }); },
|
||||
};
|
||||
return proxy;
|
||||
},
|
||||
}),
|
||||
}));
|
||||
|
||||
const trap = require('../../src/services/intelligence/trapDetection');
|
||||
|
||||
beforeEach(() => {
|
||||
mockLM.reverse = null;
|
||||
mockLM.juice = null;
|
||||
mockLM.lm = null;
|
||||
mockResolutions.current = [];
|
||||
});
|
||||
|
||||
describe('trap signals (individual)', () => {
|
||||
test('reverse_line_movement: inactive without snapshots', async () => {
|
||||
mockLM.reverse = null;
|
||||
const r = await trap.__internals.signalReverseLineMovement({ gameId: 'g', playerName: 'P', statType: 'points' });
|
||||
expect(r.active).toBe(false);
|
||||
});
|
||||
|
||||
test('reverse_line_movement: active and scoring when RLM detected', async () => {
|
||||
mockLM.reverse = { isReverse: true, score: 0.7, publicSide: 'over', lineDirection: 'under', movement: -1 };
|
||||
const r = await trap.__internals.signalReverseLineMovement({ gameId: 'g', playerName: 'P', statType: 'points' });
|
||||
expect(r.active).toBe(true);
|
||||
expect(r.score).toBe(0.7);
|
||||
});
|
||||
|
||||
test('new_context_trap: scales with flag count', () => {
|
||||
const noFlags = trap.__internals.signalNewContextTrap({ gameContext: {} });
|
||||
expect(noFlags.active).toBe(false);
|
||||
const oneFlag = trap.__internals.signalNewContextTrap({ gameContext: { game_in_series: 1 } });
|
||||
expect(oneFlag.score).toBeCloseTo(0.25, 5);
|
||||
const allFlags = trap.__internals.signalNewContextTrap({
|
||||
gameContext: { game_in_series: 1, first_playoff_game: true, new_opponent_in_series: true, new_venue: true },
|
||||
});
|
||||
expect(allFlags.score).toBe(1);
|
||||
});
|
||||
|
||||
test('recency_inflation: scores when L5 hotter than L20', () => {
|
||||
const r = trap.__internals.signalRecencyInflation({ features: { l5_avg: 30, l20_avg: 22 } });
|
||||
expect(r.active).toBe(true);
|
||||
expect(r.score).toBeCloseTo((30 - 22) / 22, 5);
|
||||
});
|
||||
|
||||
test('recency_inflation: inactive without L5/L20', () => {
|
||||
const r = trap.__internals.signalRecencyInflation({ features: { l5_avg: 25 } });
|
||||
expect(r.active).toBe(false);
|
||||
});
|
||||
|
||||
test('juice_degradation: passes through lineMovement signal', async () => {
|
||||
mockLM.juice = { applicable: true, score: 0.4, worstSide: 'over' };
|
||||
const r = await trap.__internals.signalJuiceDegradation({ gameId: 'g', playerName: 'P', statType: 'points' });
|
||||
expect(r.score).toBe(0.4);
|
||||
});
|
||||
|
||||
test('teammate_return_trap: scales with returning usage', () => {
|
||||
const r = trap.__internals.signalTeammateReturnTrap({ gameContext: { returning_teammate_usage_rate: 0.32 } });
|
||||
expect(r.active).toBe(true);
|
||||
expect(r.score).toBeCloseTo(0.16, 5);
|
||||
});
|
||||
|
||||
test('line_consensus_divergence: scores from |line - median| / stddev', () => {
|
||||
const r = trap.__internals.signalLineConsensusDivergence({
|
||||
odds: { playerLine: 26.5, consensus: { median: 24.5, stddev: 1.0 } },
|
||||
});
|
||||
expect(r.active).toBe(true);
|
||||
expect(r.score).toBe(1.0); // |26.5-24.5|/1 = 2.0 capped to 1.0
|
||||
});
|
||||
|
||||
test('historical_hit_rate_paradox: inactive with thin history', async () => {
|
||||
mockResolutions.current = [{ result: 'hit', direction: 'over', line: 25.5 }];
|
||||
const r = await trap.__internals.signalHistoricalHitRateParadox({
|
||||
playerName: 'P', statType: 'points', sport: 'nba', gameId: 'g',
|
||||
});
|
||||
expect(r.active).toBe(false);
|
||||
});
|
||||
|
||||
test('historical_hit_rate_paradox: active when line moves AGAINST a high hit-rate', async () => {
|
||||
mockResolutions.current = Array.from({ length: 25 }, (_, i) => ({
|
||||
result: i < 18 ? 'hit' : 'miss',
|
||||
direction: 'over',
|
||||
line: 25.5,
|
||||
}));
|
||||
mockLM.lm = { movement: -1.0 }; // moved DOWN while player usually OVER
|
||||
const r = await trap.__internals.signalHistoricalHitRateParadox({
|
||||
playerName: 'P', statType: 'points', sport: 'nba', gameId: 'g',
|
||||
});
|
||||
expect(r.active).toBe(true);
|
||||
expect(r.score).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getTrapScore (composite)', () => {
|
||||
test('only ACTIVE signals average — null signals do not dilute', async () => {
|
||||
// Two active signals: new_context_trap (0.25 = 1 flag) + recency_inflation
|
||||
// (~0.36). The other five inactive.
|
||||
const result = await trap.getTrapScore({
|
||||
features: { l5_avg: 30, l20_avg: 22 },
|
||||
gameContext: { game_in_series: 1 },
|
||||
});
|
||||
expect(result.active_count).toBe(2);
|
||||
expect(result.composite).toBeCloseTo((0.25 + (30 - 22) / 22) / 2, 5);
|
||||
expect(result.recommendation).toBe('caution');
|
||||
});
|
||||
|
||||
test('recommendation thresholds', () => {
|
||||
expect(trap.__internals.recommend(0.1)).toBe('proceed');
|
||||
expect(trap.__internals.recommend(0.3)).toBe('caution');
|
||||
expect(trap.__internals.recommend(0.6)).toBe('avoid');
|
||||
});
|
||||
|
||||
test('KAT-scenario: G1 of Finals + recency inflation → avoid/caution', async () => {
|
||||
// High recency (L5 32 vs L20 22 → 0.45) + 2 context flags (0.5).
|
||||
// Composite = (0.45 + 0.5) / 2 = 0.475 → caution.
|
||||
const result = await trap.getTrapScore({
|
||||
features: { l5_avg: 32, l20_avg: 22 },
|
||||
gameContext: { game_in_series: 1, first_playoff_game: true },
|
||||
});
|
||||
expect(result.active_count).toBe(2);
|
||||
expect(['caution', 'avoid']).toContain(result.recommendation);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user