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
+250
View File
@@ -0,0 +1,250 @@
// Mock Redis cache — populate per-test to simulate prefetched data.
const mockCacheStore = new Map();
jest.mock('../../src/utils/redis', () => ({
cacheGet: async (k) => (mockCacheStore.has(k) ? mockCacheStore.get(k) : null),
cacheSet: async (k, v) => { mockCacheStore.set(k, v); return true; },
cacheDel: async (k) => { mockCacheStore.delete(k); return true; },
isDegraded: () => false,
}));
const { normalizeName } = require('../../src/utils/normalize');
const extractor = require('../../src/services/intelligence/soccerFeatureExtractor');
beforeEach(() => {
mockCacheStore.clear();
});
function primePlayer(name, profile) {
mockCacheStore.set(`soccer:player:${normalizeName(name)}`, profile);
}
describe('soccerFeatureExtractor', () => {
describe('isSoccerSport', () => {
test('accepts soccer + football, rejects nba/mlb/random', () => {
expect(extractor.isSoccerSport('soccer')).toBe(true);
expect(extractor.isSoccerSport('SOCCER')).toBe(true);
expect(extractor.isSoccerSport('football')).toBe(true);
expect(extractor.isSoccerSport('nba')).toBe(false);
expect(extractor.isSoccerSport('mlb')).toBe(false);
expect(extractor.isSoccerSport(null)).toBe(false);
expect(extractor.isSoccerSport(undefined)).toBe(false);
});
});
describe('cache-miss path (everything null)', () => {
test('returns engine1-shaped result with errors flagged, no throw', async () => {
const result = await extractor.extractSoccerFeatures({
player: 'Unknown Player', stat_type: 'goals', line: 0.5, direction: 'over',
});
expect(result).toHaveProperty('features');
expect(result).toHaveProperty('trap');
expect(result).toHaveProperty('consistency');
expect(result).toHaveProperty('prop');
expect(result).toHaveProperty('meta');
// Critical: numeric features default to null (NOT 0 — 0 would
// confuse engine1 into thinking we have a real signal).
expect(result.features.goals_per_90).toBeNull();
expect(result.features.xg_delta).toBeNull();
expect(result.features.minutes_per_game).toBeNull();
// Errors surface the misses.
expect(result.meta.errors).toContain('player_not_found_in_cache');
expect(result.meta.errors).toContain('team_not_resolved');
});
test('does not throw on null player input — surfaces error', async () => {
const result = await extractor.extractSoccerFeatures({
stat_type: 'goals', line: 0.5,
});
expect(result.meta.errors).toEqual(
expect.arrayContaining([expect.stringMatching(/missing required fields/)])
);
});
});
describe('player profile resolution', () => {
test('reads cached profile by normalized name', async () => {
primePlayer('Harry Kane', {
team: 'England', goals_per_90: 0.85, assists_per_90: 0.15,
minutes_per_game: 84, start_rate: 0.95, season_per_90: 0.78,
recent_form_per_90: 1.05,
});
const result = await extractor.extractSoccerFeatures({
player: 'Harry Kane', stat_type: 'goals', line: 0.5, direction: 'over',
});
expect(result.features.goals_per_90).toBe(0.85);
expect(result.features.minutes_per_game).toBe(84);
expect(result.features.start_rate).toBe(0.95);
// engine1-canonical mapping: l5_avg from recent_form_per_90,
// l20_avg from season_per_90.
expect(result.features.l5_avg).toBe(1.05);
expect(result.features.l20_avg).toBe(0.78);
expect(result.meta.teamAbbr).toBe('England');
expect(result.meta.errors).not.toContain('player_not_found_in_cache');
});
});
describe('xG regression risk', () => {
test('fires when actual goals significantly outpace expected', async () => {
primePlayer('Striker A', {
team: 'France', goals_per_90: 1.2, xg_per_90: 0.7, xg_delta: 0.71,
});
const r = await extractor.extractSoccerFeatures({
player: 'Striker A', stat_type: 'goals', line: 1.5, direction: 'over',
});
expect(r.features.xg_regression_risk).toBe(true);
expect(r.features.xg_delta).toBeCloseTo(0.71);
});
test('does NOT fire when xG and goals track each other', async () => {
primePlayer('Striker B', { team: 'France', goals_per_90: 0.8, xg_per_90: 0.78, xg_delta: 0.025 });
const r = await extractor.extractSoccerFeatures({
player: 'Striker B', stat_type: 'goals', line: 0.5, direction: 'over',
});
expect(r.features.xg_regression_risk).toBe(false);
});
test('does NOT fire when xG data is missing', async () => {
primePlayer('Striker C', { team: 'France' });
const r = await extractor.extractSoccerFeatures({
player: 'Striker C', stat_type: 'goals', line: 0.5, direction: 'over',
});
expect(r.features.xg_regression_risk).toBe(false);
});
});
describe('venue + altitude + home-continent overlay', () => {
test('Estadio Azteca venue surfaces high altitude impact', async () => {
primePlayer('Player X', { team: 'England', goals_per_90: 0.5 });
mockCacheStore.set('soccer:nextmatch:England', {
opponent: 'USA', venue: 'Estadio Azteca', isHome: false, referee: 'Bjorn Kuipers',
});
const r = await extractor.extractSoccerFeatures({
player: 'Player X', stat_type: 'goals', line: 0.5, direction: 'over',
});
expect(r.features.venue_altitude_ft).toBeGreaterThan(7000);
expect(r.features.altitude_impact).toBe('high');
expect(r.features.home_continent).toBe(false); // England isn't CONCACAF
expect(r.features.home_away).toBe(0.0); // away match
});
test('USA at MetLife Stadium → home-continent true, altitude none', async () => {
primePlayer('Christian Pulisic', { team: 'USA', goals_per_90: 0.3 });
mockCacheStore.set('soccer:nextmatch:USA', {
opponent: 'Brazil', venue: 'MetLife Stadium', isHome: true, referee: null,
});
const r = await extractor.extractSoccerFeatures({
player: 'Christian Pulisic', stat_type: 'goals', line: 0.5, direction: 'over',
});
expect(r.features.home_continent).toBe(true);
expect(r.features.altitude_impact).toBe('none');
expect(r.features.home_away).toBe(1.0);
// Static-data lookup catches the penalty/corner role.
expect(r.features.is_penalty_taker).toBe(true);
});
});
describe('referee profile overlay', () => {
test('reads cards/penalties per game for upcoming referee', async () => {
primePlayer('Card Heavy', { team: 'Argentina', goals_per_90: 0.1 });
mockCacheStore.set('soccer:nextmatch:Argentina', {
opponent: 'Brazil', venue: 'MetLife Stadium', isHome: true, referee: 'Anthony Taylor',
});
mockCacheStore.set('soccer:referee:Anthony Taylor', {
cards_per_game: 5.4, penalties_per_game: 0.6,
});
const r = await extractor.extractSoccerFeatures({
player: 'Card Heavy', stat_type: 'cards', line: 0.5, direction: 'over',
});
expect(r.features.referee_cards_per_game).toBeCloseTo(5.4);
expect(r.features.referee_penalties_per_game).toBeCloseTo(0.6);
expect(r.features.referee_name).toBe('Anthony Taylor');
});
});
describe('rest_days from last fixture', () => {
test('computes days since last finished fixture', async () => {
primePlayer('Worn Out', { team: 'Brazil', goals_per_90: 0.6 });
const threeDaysAgo = new Date(Date.now() - 3 * 24 * 3600 * 1000).toISOString();
mockCacheStore.set('soccer:lastfixture:Brazil', { utcDate: threeDaysAgo });
const r = await extractor.extractSoccerFeatures({
player: 'Worn Out', stat_type: 'goals', line: 0.5, direction: 'over',
});
expect(r.features.rest_days).toBe(3);
});
test('returns null when no last fixture cached', async () => {
primePlayer('Fresh', { team: 'Croatia', goals_per_90: 0.4 });
const r = await extractor.extractSoccerFeatures({
player: 'Fresh', stat_type: 'goals', line: 0.5, direction: 'over',
});
expect(r.features.rest_days).toBeNull();
});
test('returns null for malformed utcDate', async () => {
primePlayer('Edge Case', { team: 'Portugal', goals_per_90: 0.4 });
mockCacheStore.set('soccer:lastfixture:Portugal', { utcDate: 'not-a-date' });
const r = await extractor.extractSoccerFeatures({
player: 'Edge Case', stat_type: 'goals', line: 0.5, direction: 'over',
});
expect(r.features.rest_days).toBeNull();
});
});
describe('opponent defense overlay', () => {
test('reads team defense aggregates from cache', async () => {
primePlayer('Forward', { team: 'England', goals_per_90: 0.6 });
mockCacheStore.set('soccer:nextmatch:England', {
opponent: 'Italy', venue: "Levi's Stadium", isHome: true, referee: null,
});
mockCacheStore.set('soccer:teamdefense:wc:Italy', {
goals_conceded_per_game: 0.4, clean_sheet_rate: 0.55,
defensive_rank: 3, defensive_rank_norm: 0.05, // top defense
});
const r = await extractor.extractSoccerFeatures({
player: 'Forward', stat_type: 'goals', line: 0.5, direction: 'over',
});
expect(r.features.opp_goals_conceded_per_game).toBeCloseTo(0.4);
expect(r.features.opp_clean_sheet_rate).toBeCloseTo(0.55);
expect(r.features.opp_rank_stat).toBeCloseTo(0.05); // engine1 reads this
});
});
describe('tournament history overlay', () => {
test('marks designated tournament players', async () => {
primePlayer('Lionel Messi', { team: 'Argentina', goals_per_90: 0.8 });
const r = await extractor.extractSoccerFeatures({
player: 'Lionel Messi', stat_type: 'goals', line: 0.5, direction: 'over',
});
expect(r.features.tournament_player).toBe(true);
expect(r.features.wc_goals_career).toBeGreaterThan(0);
});
test('non-tournament players get false', async () => {
primePlayer('Rookie One', { team: 'USA', goals_per_90: 0.2 });
const r = await extractor.extractSoccerFeatures({
player: 'Rookie One', stat_type: 'goals', line: 0.5, direction: 'over',
});
expect(r.features.tournament_player).toBe(false);
expect(r.features.wc_goals_career).toBeNull();
});
});
describe('shape compatibility with engine1', () => {
test('returns the same top-level keys as the NBA path', async () => {
primePlayer('Compat Test', { team: 'USA', goals_per_90: 0.3 });
const r = await extractor.extractSoccerFeatures({
player: 'Compat Test', stat_type: 'goals', line: 0.5, direction: 'over',
});
expect(Object.keys(r).sort()).toEqual(
['consistency', 'features', 'meta', 'prop', 'trap'].sort()
);
// meta carries the same NBA-path field names so callers can read
// teamAbbr / opponentAbbr / errors uniformly.
expect(r.meta).toHaveProperty('teamAbbr');
expect(r.meta).toHaveProperty('opponentAbbr');
expect(r.meta).toHaveProperty('errors');
expect(r.meta).toHaveProperty('sport', 'soccer');
});
});
});