Sessions 5-7a: 955 tests, deployment ready
This commit is contained in:
@@ -0,0 +1,327 @@
|
||||
/**
|
||||
* 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user