Session 7j: Soccer intelligence - 9 leagues, 11 signals, 6 traps, poller, prefetch, 131 new tests (1173 total)

This commit is contained in:
Kev
2026-06-10 14:50:13 -04:00
parent b9084408bf
commit ad5ea8d5a8
28 changed files with 3175 additions and 49 deletions
@@ -0,0 +1,80 @@
// Verify computeFeaturesForProp routes soccer → soccerFeatureExtractor
// and NBA → existing path. The NBA path's full behavior is covered by
// computeFeatures.test.js (existing).
const mockExtractSoccerFeatures = jest.fn();
jest.mock('../../src/services/intelligence/soccerFeatureExtractor', () => ({
extractSoccerFeatures: (...args) => mockExtractSoccerFeatures(...args),
isSoccerSport: (s) => ['soccer', 'football'].includes(String(s || '').toLowerCase()),
}));
// Mock the rest of the upstream chain — none of it should be called on
// the soccer branch.
jest.mock('../../src/utils/supabase', () => ({
getSupabaseServiceClient: () => ({ from: jest.fn() }),
}));
jest.mock('axios');
jest.mock('../../src/services/intelligence/featureCache', () => ({
getFeatures: jest.fn(),
}));
jest.mock('../../src/services/intelligence/trapDetection', () => ({
getTrapScore: jest.fn(async () => ({ composite: 0.2, signals: {}, active_count: 1, recommendation: 'caution' })),
}));
jest.mock('../../src/services/intelligence/consistencyScore', () => ({
getConsistency: jest.fn(),
}));
jest.mock('../../src/services/intelligence/gameLogService', () => ({
getGameLogs: jest.fn(async () => []),
}));
const { computeFeaturesForProp } = require('../../src/services/intelligence/computeFeatures');
beforeEach(() => {
mockExtractSoccerFeatures.mockReset();
});
describe('computeFeaturesForProp — sport dispatch', () => {
test('sport=soccer routes to soccerFeatureExtractor (NBA path NOT invoked)', async () => {
mockExtractSoccerFeatures.mockResolvedValueOnce({
features: { goals_per_90: 0.4 },
trap: { composite: 0, signals: {}, active_count: 0, recommendation: 'proceed' },
consistency: { consistency: 'unknown', score: null, games: 0 },
prop: { line: 0.5, direction: 'over' },
meta: { player: 'Test', sport: 'soccer', statType: 'goals', errors: [] },
});
const result = await computeFeaturesForProp({
player: 'Test', stat_type: 'goals', line: 0.5, direction: 'over', sport: 'soccer',
});
expect(mockExtractSoccerFeatures).toHaveBeenCalledTimes(1);
expect(result.features.goals_per_90).toBe(0.4);
expect(result.meta.sport).toBe('soccer');
// The branch re-runs trap detection so the trap object is populated.
expect(result.trap.composite).toBeGreaterThanOrEqual(0);
});
test('sport=football is normalized into the soccer branch', async () => {
mockExtractSoccerFeatures.mockResolvedValueOnce({
features: {}, trap: {}, consistency: {}, prop: {}, meta: { sport: 'soccer', errors: [] },
});
await computeFeaturesForProp({ player: 'X', stat_type: 'goals', line: 0.5, sport: 'football' });
expect(mockExtractSoccerFeatures).toHaveBeenCalledTimes(1);
});
test('sport=nba does NOT invoke the soccer extractor', async () => {
const featureCache = require('../../src/services/intelligence/featureCache');
featureCache.getFeatures.mockResolvedValueOnce({ features: { l5_avg: 28 } });
await computeFeaturesForProp({
player: 'Jokic', stat_type: 'points', line: 26.5, direction: 'over', sport: 'nba',
});
expect(mockExtractSoccerFeatures).not.toHaveBeenCalled();
});
test('sport omitted defaults to NBA (legacy contract)', async () => {
const featureCache = require('../../src/services/intelligence/featureCache');
featureCache.getFeatures.mockResolvedValueOnce({ features: { l5_avg: 30 } });
await computeFeaturesForProp({ player: 'A', stat_type: 'points', line: 25, direction: 'over' });
expect(mockExtractSoccerFeatures).not.toHaveBeenCalled();
});
});