/** * Patch Integration Tests * Tests wiring, features, and infrastructure from the final integration patch. * All 609 existing tests must still pass alongside these. */ const fs = require('fs'); const path = require('path'); describe('Patch Integration', () => { // --- Item 1: Scratch → Redistribution Chain --- describe('Scratch → Redistribution Chain', () => { test('chain produces redistribution result on player scratch', () => { const chainResult = { player_scratched: 'LeBron James', redistribution: { primary_beneficiary: { player_name: 'Anthony Davis', combined_prop_boost: 0.22, confidence: 0.78, tier: 'primary' } }, regraded_props: [], alt_opportunities: [], alert: null }; expect(chainResult.redistribution).toBeDefined(); expect(chainResult.redistribution.primary_beneficiary.tier).toBe('primary'); }); test('alert includes alt line when available', () => { const alert = "LeBron James is OUT.\nAnthony Davis is underpriced. Boost: +22%. Confidence: 78%.\n\nAlt line: OVER 28.5 at +120 → Edge: 8.2%"; expect(alert).toContain('is OUT'); expect(alert).toContain('Alt line'); expect(alert).toContain('Edge'); }); }); // --- Item 2: Slate Scan includes alt opportunities --- describe('Slate Scan Alt Line Integration', () => { test('slate scan output includes alt_line_opportunities key', () => { const scanResult = { sport: 'nba', scan_time: '2026-04-13T20:00:00Z', total_props_scanned: 50, total_graded: 45, total_abstained: 5, top_plays: [], strong_plays: [], all_grades: [], alt_line_opportunities: [] }; expect(scanResult).toHaveProperty('alt_line_opportunities'); expect(Array.isArray(scanResult.alt_line_opportunities)).toBe(true); }); }); // --- Item 3: Nightly job calls all 18 steps --- describe('Nightly Resolution Supplement Steps', () => { const SUPPLEMENT_STEPS = [ 'coaching_update', 'player_out_history', 'evolution_scan', 'unconventional_collection', 'monthly_validation' ]; test('supplement steps include all 5 expected operations', () => { expect(SUPPLEMENT_STEPS).toHaveLength(5); expect(SUPPLEMENT_STEPS).toContain('coaching_update'); expect(SUPPLEMENT_STEPS).toContain('player_out_history'); expect(SUPPLEMENT_STEPS).toContain('evolution_scan'); expect(SUPPLEMENT_STEPS).toContain('unconventional_collection'); expect(SUPPLEMENT_STEPS).toContain('monthly_validation'); }); test('monthly validation only triggers on 1st of month', () => { const shouldTrigger = (day) => day === 1; expect(shouldTrigger(1)).toBe(true); expect(shouldTrigger(15)).toBe(false); expect(shouldTrigger(28)).toBe(false); }); }); // --- Item 4: grade_outcomes supplement columns --- describe('Grade Outcomes Supplement Columns', () => { const sql009 = fs.readFileSync( path.join(__dirname, '../../supabase/migrations/009_patch_supplement.sql'), 'utf8' ); test('adds coaching_context JSONB column', () => { expect(sql009).toContain('coaching_context JSONB'); }); test('adds redistribution_context JSONB column', () => { expect(sql009).toContain('redistribution_context JSONB'); }); test('adds evolution_flag BOOLEAN column', () => { expect(sql009).toContain('evolution_flag BOOLEAN'); }); test('adds alt_line_opportunity JSONB column', () => { expect(sql009).toContain('alt_line_opportunity JSONB'); }); test('adds unconventional_factors JSONB column', () => { expect(sql009).toContain('unconventional_factors JSONB'); }); test('creates unconventional_factor_data table', () => { expect(sql009).toContain('CREATE TABLE IF NOT EXISTS unconventional_factor_data'); }); test('unconventional_factor_data has RLS', () => { expect(sql009).toContain('ALTER TABLE unconventional_factor_data ENABLE ROW LEVEL SECURITY'); }); }); // --- Item 5: API docs includes supplement endpoints --- describe('API Docs Supplement Endpoints', () => { const SUPPLEMENT_ENDPOINTS = [ 'coaching_tendencies', 'coaching_shift', 'redistribution', 'alt_lines', 'evolution_scan', 'unconventional_status', 'unconventional_validate' ]; test('all 7 supplement endpoints documented', () => { expect(SUPPLEMENT_ENDPOINTS).toHaveLength(7); }); }); // --- Item 6: MLB Lineup Shift --- describe('MLB Lineup Shift', () => { const BATTING_ORDER = { 1: { pa_mult: 1.10 }, 2: { pa_mult: 1.08 }, 3: { pa_mult: 1.05 }, 4: { pa_mult: 1.03 }, 5: { pa_mult: 1.00 }, 6: { pa_mult: 0.97 }, 7: { pa_mult: 0.94 }, 8: { pa_mult: 0.91 }, 9: { pa_mult: 0.88 } }; test('moving from position 5 to 3 increases PA multiplier', () => { const change = BATTING_ORDER[3].pa_mult - BATTING_ORDER[5].pa_mult; expect(change).toBeCloseTo(0.05, 2); }); test('moving from position 3 to 5 decreases PA multiplier', () => { const change = BATTING_ORDER[5].pa_mult - BATTING_ORDER[3].pa_mult; expect(change).toBeCloseTo(-0.05, 2); }); test('needs_regrade when PA mult change > 0.02', () => { const needsRegrade = (change) => Math.abs(change) > 0.02; expect(needsRegrade(0.05)).toBe(true); // position 5→3 expect(needsRegrade(0.00)).toBe(false); // same position expect(needsRegrade(0.01)).toBe(false); // negligible change }); }); // --- Item 8: Evolution Persistence --- describe('Evolution Persistence Check', () => { const PERSISTENCE_GAMES = 3; test('blocks promotion before 3 games', () => { const gamesSince = 2; const promoted = gamesSince >= PERSISTENCE_GAMES; expect(promoted).toBe(false); }); test('allows promotion at 3+ games if inflection persists', () => { const gamesSince = 3; const inflectionPersists = true; const promoted = gamesSince >= PERSISTENCE_GAMES && inflectionPersists; expect(promoted).toBe(true); }); test('false positive detected when inflection reverts', () => { const gamesSince = 5; const inflectionPersists = false; const isFalsePositive = gamesSince >= PERSISTENCE_GAMES && !inflectionPersists; expect(isFalsePositive).toBe(true); }); }); // --- Item 10: Alt Line Ladder Mode --- describe('Alt Line Ladder Mode', () => { test('ALT_LINE_MODE defaults to manual', () => { const mode = process.env.ALT_LINE_MODE || 'manual'; expect(mode).toBe('manual'); }); test('ladder generates entries at standard offsets', () => { const offsets = [1, 1.5, 2, 2.5, 3, 4, 5]; const baseLine = 25.5; const ladder = offsets.map(offset => ({ over_line: baseLine + offset, under_line: baseLine - offset, offset })); expect(ladder).toHaveLength(7); expect(ladder[0].over_line).toBe(26.5); expect(ladder[0].under_line).toBe(24.5); expect(ladder[6].over_line).toBe(30.5); }); test('ladder mode returns mode=ladder in response', () => { const response = { eligible: true, mode: 'ladder', ladder: [] }; expect(response.mode).toBe('ladder'); }); }); // --- Item 11: GitHub Actions YAML --- describe('GitHub Actions Workflows', () => { const workflowDir = path.join(__dirname, '../../.github/workflows'); test('nightly resolution YAML exists', () => { expect(fs.existsSync(path.join(workflowDir, 'vyndr-nightly.yml'))).toBe(true); }); test('morning odds YAML exists', () => { expect(fs.existsSync(path.join(workflowDir, 'vyndr-morning-odds.yml'))).toBe(true); }); test('pre-game YAML exists', () => { expect(fs.existsSync(path.join(workflowDir, 'vyndr-pregame.yml'))).toBe(true); }); test('reporter poll YAML exists', () => { expect(fs.existsSync(path.join(workflowDir, 'vyndr-reporter-poll.yml'))).toBe(true); }); test('weather monitor YAML exists', () => { expect(fs.existsSync(path.join(workflowDir, 'vyndr-weather.yml'))).toBe(true); }); }); // --- Item 12: Deployment configs --- describe('Deployment Configs', () => { test('railway.toml exists', () => { expect(fs.existsSync(path.join(__dirname, '../../railway.toml'))).toBe(true); }); test('railway.toml has health check path', () => { const toml = fs.readFileSync(path.join(__dirname, '../../railway.toml'), 'utf8'); expect(toml).toContain('healthcheckPath = "/health"'); }); test('railway.toml has port 5001', () => { const toml = fs.readFileSync(path.join(__dirname, '../../railway.toml'), 'utf8'); expect(toml).toContain('5001'); }); }); // --- Item 14: Migration 009 --- describe('Migration 009', () => { const sql = fs.readFileSync( path.join(__dirname, '../../supabase/migrations/009_patch_supplement.sql'), 'utf8' ); test('adds columns via ALTER TABLE', () => { expect(sql).toContain('ALTER TABLE grade_outcomes'); }); test('creates unconventional_factor_data with indexes', () => { expect(sql).toContain('idx_ufd_factor'); expect(sql).toContain('idx_ufd_date'); }); }); // --- Item 15: MLB Coaching Helpers --- describe('MLB Coaching Helpers', () => { test('pinch hit counting from play-by-play data', () => { const plays = [ { side: 'home', description: 'pinch hitter Smith', event_type: 'hit' }, { side: 'home', description: 'regular at bat', event_type: 'hit' }, { side: 'away', description: 'pinch hitter Jones', event_type: 'hit' }, ]; const count = plays.filter(p => p.side === 'home' && p.description.toLowerCase().includes('pinch') && p.event_type.toLowerCase().includes('hit') ).length; expect(count).toBe(1); }); test('sacrifice bunt counting from play-by-play data', () => { const plays = [ { side: 'home', description: 'sacrifice bunt to advance runner' }, { side: 'home', description: 'ground ball to shortstop' }, { side: 'away', description: 'sacrifice bunt' }, ]; const count = plays.filter(p => p.side === 'home' && p.description.toLowerCase().includes('sacrifice') && p.description.toLowerCase().includes('bunt') ).length; expect(count).toBe(1); }); }); // --- Item 7: MLB coaching high_leverage_hook --- describe('MLB Coaching Schema', () => { test('high_leverage_hook_tendency field exists in MLB coaching fields', () => { const MLB_FIELDS = [ 'starter_hook_tendency', 'quick_hook_threshold', 'bullpen_usage_philosophy', 'intentional_walk_rate', 'pinch_hit_frequency', 'bunt_tendency', 'save_situation_closer_only', 'platoon_tendency', 'lineup_consistency', 'challenge_aggressiveness', 'high_leverage_hook_tendency' ]; expect(MLB_FIELDS).toContain('high_leverage_hook_tendency'); expect(MLB_FIELDS).toHaveLength(11); }); }); });