Sessions 5-7a: 955 tests, deployment ready

This commit is contained in:
Kev
2026-06-08 18:35:13 -04:00
parent 06b82624a2
commit 1fa04dc776
371 changed files with 49366 additions and 955 deletions
+261
View File
@@ -0,0 +1,261 @@
const { gradeMlbProp, calculateMlbEdge, isMlbStatType } = require('../../src/services/mlbGrader');
const { evaluateMlbKillConditions, classifyLineMove, checkWeather } = require('../../src/services/mlbKillConditions');
const { MLB_PARKS, getParkByTeam } = require('../../src/constants/mlbParks');
jest.mock('axios');
const axios = require('axios');
describe('mlbGrader', () => {
describe('grade thresholds', () => {
test('Grade A when edge >= 5%', () => {
const result = gradeMlbProp({
player: 'Aaron Judge',
stat_type: 'home_runs',
line: 0.5,
direction: 'over',
seasonAvg: 0.7,
recentAvg: 0.8,
});
expect(result.grade).toBe('A');
expect(result.edge_pct).toBeGreaterThanOrEqual(5);
});
test('Grade B when edge 3-4%', () => {
// seasonAvg=5.15, line=5, direction=over => seasonEdge=(5.15-5)/5*100=3%
// recentAvg=5.2, line=5 => recentEdge=(5.2-5)/5*100=4%
// composite = 3*0.6 + 4*0.4 = 1.8+1.6 = 3.4
const result = gradeMlbProp({
player: 'Test Player',
stat_type: 'strikeouts',
line: 5,
direction: 'over',
seasonAvg: 5.15,
recentAvg: 5.2,
});
expect(result.grade).toBe('B');
expect(result.edge_pct).toBeGreaterThanOrEqual(3);
expect(result.edge_pct).toBeLessThan(5);
});
test('Grade C when edge 1-2%', () => {
// seasonAvg=5.05, line=5, direction=over => seasonEdge=1%
// recentAvg=5.1 => recentEdge=2%
// composite = 1*0.6 + 2*0.4 = 0.6+0.8 = 1.4
const result = gradeMlbProp({
player: 'Test Player',
stat_type: 'hits',
line: 5,
direction: 'over',
seasonAvg: 5.05,
recentAvg: 5.1,
});
expect(result.grade).toBe('C');
expect(result.edge_pct).toBeGreaterThanOrEqual(1);
expect(result.edge_pct).toBeLessThan(3);
});
test('Grade D when negative edge', () => {
const result = gradeMlbProp({
player: 'Test Player',
stat_type: 'hits',
line: 2,
direction: 'over',
seasonAvg: 1.5,
recentAvg: 1.3,
});
expect(result.grade).toBe('D');
expect(result.edge_pct).toBeLessThan(1);
});
});
describe('isMlbStatType', () => {
test('returns true for valid hitting stat', () => {
expect(isMlbStatType('hits')).toBe(true);
expect(isMlbStatType('home_runs')).toBe(true);
expect(isMlbStatType('stolen_bases')).toBe(true);
});
test('returns true for valid pitching stat', () => {
expect(isMlbStatType('strikeouts')).toBe(true);
expect(isMlbStatType('earned_runs')).toBe(true);
expect(isMlbStatType('pitches_thrown')).toBe(true);
});
test('returns false for invalid stat type', () => {
expect(isMlbStatType('three_pointers')).toBe(false);
expect(isMlbStatType('touchdowns')).toBe(false);
expect(isMlbStatType('')).toBe(false);
});
});
describe('calculateMlbEdge', () => {
test('calculates positive edge for over', () => {
const edge = calculateMlbEdge(6, 5, 'over');
expect(edge).toBe(20);
});
test('calculates positive edge for under', () => {
const edge = calculateMlbEdge(4, 5, 'under');
expect(edge).toBe(20);
});
test('returns 0 for null inputs', () => {
expect(calculateMlbEdge(null, 5, 'over')).toBe(0);
expect(calculateMlbEdge(5, null, 'over')).toBe(0);
});
});
});
describe('mlbKillConditions', () => {
function makeContext(overrides = {}) {
return {
inLineup: true,
pitcherScratched: false,
weather: { wind_speed: 5, wind_direction: 'OUT', temp: 75, humidity: 50 },
platoonDelta: 5,
paVsHandedness: 100,
lineMovement: 0,
hoursFromOpen: 1,
parkFactor: 1.0,
rainProbability: 10,
onInjuryReport: false,
...overrides,
};
}
test('LINEUP_OUT triggers when player not in lineup', () => {
const result = evaluateMlbKillConditions(makeContext({ inLineup: false }));
expect(result.some(c => c.code === 'LINEUP_OUT')).toBe(true);
});
test('PITCHER_SCRATCH triggers when pitcher scratched', () => {
const result = evaluateMlbKillConditions(makeContext({ pitcherScratched: true }));
expect(result.some(c => c.code === 'PITCHER_SCRATCH')).toBe(true);
});
test('WIND_IN triggers at 15mph+ blowing in', () => {
const result = evaluateMlbKillConditions(makeContext({
weather: { wind_speed: 18, wind_direction: 'IN', temp: 75, humidity: 50 },
}));
expect(result.some(c => c.code === 'WIND_IN')).toBe(true);
});
test('PLATOON_DISADVANTAGE triggers when delta > 12%', () => {
const result = evaluateMlbKillConditions(makeContext({ platoonDelta: 15 }));
expect(result.some(c => c.code === 'PLATOON_DISADVANTAGE')).toBe(true);
});
test('SMALL_SAMPLE triggers under 50 PA', () => {
const result = evaluateMlbKillConditions(makeContext({ paVsHandedness: 30 }));
expect(result.some(c => c.code === 'SMALL_SAMPLE')).toBe(true);
});
test('LINE_MOVE_AGAINST triggers at 0.5+ movement', () => {
const result = evaluateMlbKillConditions(makeContext({ lineMovement: 0.7, hoursFromOpen: 1 }));
expect(result.some(c => c.code === 'LINE_MOVE_AGAINST')).toBe(true);
});
test('PARK_SUPPRESSOR triggers below 0.90', () => {
const result = evaluateMlbKillConditions(makeContext({ parkFactor: 0.85 }));
expect(result.some(c => c.code === 'PARK_SUPPRESSOR')).toBe(true);
});
test('WEATHER_RAIN triggers above 50% probability', () => {
const result = evaluateMlbKillConditions(makeContext({ rainProbability: 65 }));
expect(result.some(c => c.code === 'WEATHER_RAIN')).toBe(true);
});
test('INJURY_REPORT triggers when on injury report', () => {
const result = evaluateMlbKillConditions(makeContext({ onInjuryReport: true }));
expect(result.some(c => c.code === 'INJURY_REPORT')).toBe(true);
});
test('HUMIDITY_SUPPRESSOR triggers at humidity > 80% and temp < 60F', () => {
const result = evaluateMlbKillConditions(makeContext({
weather: { wind_speed: 5, wind_direction: 'OUT', temp: 55, humidity: 85 },
}));
expect(result.some(c => c.code === 'HUMIDITY_SUPPRESSOR')).toBe(true);
});
});
describe('classifyLineMove', () => {
test('returns sharp for movement within first 2 hours', () => {
expect(classifyLineMove(0.7, 1)).toBe('sharp');
expect(classifyLineMove(-0.5, 0.5)).toBe('sharp');
});
test('returns public for movement after 4 hours', () => {
expect(classifyLineMove(0.6, 5)).toBe('public');
expect(classifyLineMove(-0.8, 6)).toBe('public');
});
test('returns null for movement under 0.5', () => {
expect(classifyLineMove(0.3, 1)).toBeNull();
});
});
describe('checkWeather', () => {
beforeEach(() => {
jest.clearAllMocks();
});
test('falls back to open-meteo on api.weather.gov timeout', async () => {
// Mock weather.gov to timeout
axios.get.mockImplementation((url) => {
if (url.includes('weather.gov')) {
return Promise.reject(new Error('timeout of 3000ms exceeded'));
}
// open-meteo fallback
return Promise.resolve({
data: {
hourly: {
temperature_2m: Array(24).fill(72),
relative_humidity_2m: Array(24).fill(50),
wind_speed_10m: Array(24).fill(10),
wind_direction_10m: Array(24).fill(180),
precipitation_probability: Array(24).fill(20),
},
},
});
});
const result = await checkWeather([40.8296, -73.9262], 3000);
expect(result.wind_speed).toBe(10);
expect(result.temp).toBe(72);
// Verify weather.gov was attempted first
expect(axios.get).toHaveBeenCalledWith(
expect.stringContaining('weather.gov'),
expect.any(Object)
);
});
});
describe('mlbParks', () => {
test('has exactly 30 entries', () => {
expect(Object.keys(MLB_PARKS).length).toBe(30);
});
test('getParkByTeam returns correct park for NYY', () => {
const park = getParkByTeam('NYY');
expect(park).not.toBeNull();
expect(park.name).toBe('Yankee Stadium');
expect(park.coords).toEqual([40.8296, -73.9262]);
});
test('getParkByTeam returns correct park for LAD', () => {
const park = getParkByTeam('LAD');
expect(park.name).toBe('Dodger Stadium');
});
test('getParkByTeam returns null for invalid team', () => {
expect(getParkByTeam('XXX')).toBeNull();
});
test('every park has name, coords, and team', () => {
for (const [key, park] of Object.entries(MLB_PARKS)) {
expect(park.name).toBeDefined();
expect(park.coords).toHaveLength(2);
expect(park.team).toBeDefined();
}
});
});