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
+229
View File
@@ -0,0 +1,229 @@
// Soccer-branch tests for trapDetection. NBA path is covered by the
// existing trapDetection.test.js; we only need to verify the soccer
// signals fire on the right conditions and that the sport dispatch
// keeps the two trap sets isolated.
//
// Soccer signals are synchronous and pure over `input.features` — no
// Redis or DB mocks required.
const trap = require('../../src/services/intelligence/trapDetection');
function soccerInput(features = {}, statType = 'goals') {
return {
sport: 'soccer',
statType,
features: { stat_type: statType, ...features },
odds: { playerLine: 0.5, consensus: null },
};
}
describe('trapDetection — soccer branch', () => {
describe('getTrapScore dispatches on sport', () => {
test('soccer input runs ONLY the soccer signals (no NBA signals fire)', async () => {
const r = await trap.getTrapScore(soccerInput({ goals_per_90: 0.5 }));
const names = Object.keys(r.signals);
// The NBA names should be absent; the soccer names should be present.
expect(names).not.toContain('reverse_line_movement');
expect(names).not.toContain('historical_hit_rate_paradox');
expect(names).toContain('xg_regression');
expect(names).toContain('altitude_risk');
expect(names).toContain('rotation_risk');
expect(names).toContain('minute_discount');
expect(names).toContain('referee_card_bias');
expect(names).toContain('strong_defense');
});
test('nba input still runs the NBA signal set unchanged', async () => {
const r = await trap.getTrapScore({
sport: 'nba',
gameId: 'gX', playerName: 'A', statType: 'points',
odds: { playerLine: 25.5 },
});
const names = Object.keys(r.signals);
expect(names).toContain('reverse_line_movement');
expect(names).toContain('historical_hit_rate_paradox');
expect(names).not.toContain('xg_regression');
});
});
describe('signalXgRegression', () => {
test('fires when xg_delta > 0.3', () => {
const result = trap.__internals.signalXgRegression(
soccerInput({ xg_delta: 0.6 })
);
expect(result.active).toBe(true);
expect(result.score).toBeGreaterThan(0);
expect(result.explanation).toMatch(/above expected goals/);
});
test('does NOT fire when xg_delta is near zero', () => {
const result = trap.__internals.signalXgRegression(
soccerInput({ xg_delta: 0.05 })
);
expect(result.active).toBe(true);
expect(result.score).toBe(0);
});
test('inactive when xg_delta is null', () => {
const result = trap.__internals.signalXgRegression(soccerInput({ xg_delta: null }));
expect(result.active).toBe(false);
});
});
describe('signalAltitudeRisk', () => {
test('fires for non-host-continent team at high altitude', () => {
const r = trap.__internals.signalAltitudeRisk(
soccerInput({ altitude_impact: 'high', home_continent: false, venue_altitude_ft: 7349 })
);
expect(r.active).toBe(true);
expect(r.score).toBeGreaterThan(0);
expect(r.explanation).toMatch(/altitude/);
});
test('host-continent team gets a pass (acclimated)', () => {
const r = trap.__internals.signalAltitudeRisk(
soccerInput({ altitude_impact: 'high', home_continent: true })
);
expect(r.active).toBe(false);
});
test('moderate or no altitude → inactive', () => {
expect(trap.__internals.signalAltitudeRisk(
soccerInput({ altitude_impact: 'moderate', home_continent: false })
).active).toBe(false);
expect(trap.__internals.signalAltitudeRisk(
soccerInput({ altitude_impact: 'none' })
).active).toBe(false);
});
});
describe('signalRotationRisk', () => {
test('fires for low start_rate + short rest', () => {
const r = trap.__internals.signalRotationRisk(
soccerInput({ start_rate: 0.5, rest_days: 1 })
);
expect(r.active).toBe(true);
expect(r.score).toBeGreaterThan(0);
});
test('does NOT fire when start_rate is high', () => {
const r = trap.__internals.signalRotationRisk(
soccerInput({ start_rate: 0.95, rest_days: 1 })
);
expect(r.active).toBe(true);
expect(r.score).toBe(0);
});
test('does NOT fire when rest_days is sufficient', () => {
const r = trap.__internals.signalRotationRisk(
soccerInput({ start_rate: 0.5, rest_days: 5 })
);
expect(r.active).toBe(true);
expect(r.score).toBe(0);
});
test('inactive when fields missing', () => {
const r = trap.__internals.signalRotationRisk(soccerInput({}));
expect(r.active).toBe(false);
});
});
describe('signalMinuteDiscount', () => {
test('fires when minutes_per_game < 70', () => {
const r = trap.__internals.signalMinuteDiscount(soccerInput({ minutes_per_game: 55 }));
expect(r.active).toBe(true);
expect(r.score).toBeGreaterThan(0);
});
test('does NOT fire when minutes_per_game >= 70', () => {
const r = trap.__internals.signalMinuteDiscount(soccerInput({ minutes_per_game: 88 }));
expect(r.active).toBe(true);
expect(r.score).toBe(0);
});
});
describe('signalRefereeCardBias (POSITIVE)', () => {
test('marks positive signal for card-heavy ref on a CARDS prop', () => {
const r = trap.__internals.signalRefereeCardBias(
soccerInput({ referee_cards_per_game: 6.2, referee_name: 'Anthony Taylor' }, 'cards')
);
expect(r.positive).toBe(true);
expect(r.active).toBe(false); // explicit: positive signals do NOT count active
expect(r.score).toBe(0);
expect(r.explanation).toMatch(/favorable for card over/);
});
test('does NOT mark positive for a non-cards stat type', () => {
const r = trap.__internals.signalRefereeCardBias(
soccerInput({ referee_cards_per_game: 6.2 }, 'goals')
);
expect(r.positive).not.toBe(true);
expect(r.active).toBe(false);
});
test('composite EXCLUDES positive signals even when many fire', async () => {
// Drop one trap + one positive — composite should reflect ONLY the trap.
const r = await trap.getTrapScore(soccerInput(
{
xg_delta: 0.6, // trap fires
referee_cards_per_game: 6.5, // positive fires
},
'cards', // makes the positive applicable
));
expect(r.active_count).toBe(1); // only the xG regression counts
expect(r.composite).toBeGreaterThan(0);
});
});
describe('signalStrongDefense', () => {
test('fires for top-5 defense on goals over', () => {
const r = trap.__internals.signalStrongDefense(
soccerInput({ opp_defensive_rank: 3 }, 'goals')
);
expect(r.active).toBe(true);
expect(r.score).toBeGreaterThan(0);
});
test('fires for top-5 defense on shots_on_target', () => {
const r = trap.__internals.signalStrongDefense(
soccerInput({ opp_defensive_rank: 5 }, 'shots_on_target')
);
expect(r.active).toBe(true);
});
test('inactive for non-scoring stat types', () => {
const r = trap.__internals.signalStrongDefense(
soccerInput({ opp_defensive_rank: 3 }, 'cards')
);
expect(r.active).toBe(false);
});
test('does NOT fire when defense is mid-table', () => {
const r = trap.__internals.signalStrongDefense(
soccerInput({ opp_defensive_rank: 18 }, 'goals')
);
expect(r.active).toBe(true);
expect(r.score).toBe(0);
});
});
describe('composite scoring', () => {
test('multiple soccer traps → composite >= 0.5 → avoid', async () => {
const r = await trap.getTrapScore(soccerInput(
{
xg_delta: 0.5,
altitude_impact: 'high',
home_continent: false,
start_rate: 0.5,
rest_days: 1,
opp_defensive_rank: 3,
},
'goals',
));
expect(r.composite).toBeGreaterThanOrEqual(0.5);
expect(r.recommendation).toBe('avoid');
});
test('no soccer traps → composite 0 → proceed', async () => {
const r = await trap.getTrapScore(soccerInput({
xg_delta: 0.05,
altitude_impact: 'none',
start_rate: 0.95,
rest_days: 5,
minutes_per_game: 85,
opp_defensive_rank: 20,
}, 'goals'));
expect(r.composite).toBe(0);
expect(r.recommendation).toBe('proceed');
});
});
});