Files
vyndr/tests/unit/patchIntegration.test.js
T

328 lines
11 KiB
JavaScript

/**
* 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);
});
});
});