Session 7j: Soccer intelligence - 9 leagues, 11 signals, 6 traps, poller, prefetch, 131 new tests (1173 total)
This commit is contained in:
@@ -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();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user