/** * Grading pipeline end-to-end integration. * * 1. Mock SharpAPI returning 3 player props (one A-tier shaped, one * neutral, one trap-heavy that should downgrade). * 2. Mock feature cache returning features. * 3. Mock trap detection — return high composite for the trap prop. * 4. Run the orchestrator. * 5. Assert: all 3 props get a grade_history insert. * 6. Assert: A-tier prop queued for Engine 2, D/C-tier not queued. * 7. Assert: trap composite > 0.5 downgrades the trap prop's grade. */ process.env.ENGINE2_ENABLED = 'true'; const mockAxiosGet = jest.fn(); jest.mock('axios', () => ({ get: (...args) => mockAxiosGet(...args), post: jest.fn() })); const mockGetPlayerProps = jest.fn(); jest.mock('../../src/services/adapters/sharpApiAdapter', () => ({ configured: () => true, getPlayerProps: (...args) => mockGetPlayerProps(...args), })); jest.mock('../../src/services/adapters/openRouterAdapter', () => ({ configured: () => false, analyze: async () => null, getUsage: () => ({ requestsToday: 0 }), })); // Feature vectors per player — vary to drive different grades. const mockFeatures = { current: new Map() }; jest.mock('../../src/services/intelligence/featureCache', () => ({ getFeatures: async ({ playerName }) => { const f = mockFeatures.current.get(playerName) || {}; return { features: f, meta: { features_available: Object.keys(f), features_missing: [] } }; }, })); const mockTrapScores = { current: new Map() }; jest.mock('../../src/services/intelligence/trapDetection', () => ({ getTrapScore: async ({ playerName }) => mockTrapScores.current.get(playerName) || { composite: 0, signals: {}, active_count: 0, recommendation: 'proceed', }, normalizeName: (n) => n, detectReturningTeammate: () => null, })); jest.mock('../../src/services/intelligence/consistencyScore', () => ({ getConsistency: async () => ({ consistency: 'reliable', cv: 0.18, score: 0.7, games: 20 }), })); jest.mock('../../src/services/intelligence/gameLogService', () => ({ getGameLogs: async () => [ { points: 30 }, { points: 28 }, { points: 32 }, { points: 27 }, { points: 31 }, ], getCareerPlayoffGames: async () => 50, getWithWithoutStats: async () => null, })); const mockInserts = []; const mockUpdates = []; jest.mock('../../src/utils/supabase', () => ({ getSupabaseServiceClient: () => ({ from(table) { const proxy = { insert(row) { mockInserts.push({ table, row }); return { select() { return { single: () => Promise.resolve({ data: { id: `gid-${mockInserts.length}` }, error: null }) }; }, }; }, update(patch) { mockUpdates.push({ table, patch }); return { eq() { return Promise.resolve({ error: null }); } }; }, }; return proxy; }, }), })); const orchestrator = require('../../src/services/intelligence/gradingOrchestrator'); const engine2 = require('../../src/services/intelligence/engine2'); beforeEach(() => { mockAxiosGet.mockReset(); mockGetPlayerProps.mockReset(); mockInserts.length = 0; mockUpdates.length = 0; mockFeatures.current.clear(); mockTrapScores.current.clear(); engine2.clearQueue(); }); describe('grading pipeline — end-to-end through orchestrator', () => { test('three props grade through, A-tier queued, trap-heavy downgraded', async () => { // ESPN scoreboard with one game. mockAxiosGet.mockResolvedValue({ status: 200, data: { events: [{ id: 'ev-mix', date: '2026-06-08T23:30Z', status: { type: { state: 'pre' } }, competitions: [{ competitors: [ { homeAway: 'home', id: '1', team: { abbreviation: 'NYK', displayName: 'Knicks' } }, { homeAway: 'away', id: '2', team: { abbreviation: 'BOS', displayName: 'Celtics' } }, ], }], }], }, }); // Three props: // A-tier: hot recent form, weak D, rested, home — should grade A or A- // neutral: average everything — should land C-ish // trap-heavy: hot recent form BUT trap composite 0.7 → downgrade mockGetPlayerProps.mockResolvedValue([ { player: 'A-Player', team: 'NYK', statType: 'points', line: 25.5, direction: 'over' }, { player: 'C-Player', team: 'NYK', statType: 'points', line: 25.5, direction: 'over' }, { player: 'Trap-Player', team: 'NYK', statType: 'points', line: 25.5, direction: 'over' }, ]); mockFeatures.current.set('A-Player', { l5_avg: 32, l20_avg: 28, opp_rank_stat: 0.85, home_away: 1.0, rest_days: 2, }); mockFeatures.current.set('C-Player', { l5_avg: 25, l20_avg: 25.5 }); mockFeatures.current.set('Trap-Player', { l5_avg: 32, l20_avg: 28, opp_rank_stat: 0.85, home_away: 1.0, rest_days: 2, }); // Only the trap-heavy prop has a high trap composite. mockTrapScores.current.set('Trap-Player', { composite: 0.7, signals: {}, active_count: 4, recommendation: 'avoid' }); const summary = await orchestrator.runPipeline('nba', { skipEngine2: true }); expect(summary.props_graded).toBe(3); expect(mockInserts.length).toBe(3); // All inserts went to grade_history. expect(mockInserts.every((i) => i.table === 'grade_history')).toBe(true); // factors column is stored as an array. for (const i of mockInserts) { expect(Array.isArray(i.row.factors) || i.row.factors === null).toBe(true); } // A-Player grade is in A range; Trap-Player is downgraded BELOW A. const aRow = mockInserts.find((i) => i.row.player_name === 'A-Player'); const cRow = mockInserts.find((i) => i.row.player_name === 'C-Player'); const trapRow = mockInserts.find((i) => i.row.player_name === 'Trap-Player'); const GRADE_ORDER = ['F','D','C-','C','C+','B-','B','B+','A-','A','A+']; const idx = (g) => GRADE_ORDER.indexOf(g); expect(idx(aRow.row.grade)).toBeGreaterThan(idx('B+')); expect(idx(trapRow.row.grade)).toBeLessThan(idx(aRow.row.grade)); expect(idx(cRow.row.grade)).toBeLessThan(idx(aRow.row.grade)); // Engine 2 queue: A-Player qualifies (A range); Trap-Player downgraded // out of A/B range so should not be queued; C-Player C-tier excluded. const queued = engine2.getQueueSize(); expect(queued).toBeGreaterThanOrEqual(1); }); test('p_over and factors get persisted on the row', async () => { mockAxiosGet.mockResolvedValue({ status: 200, data: { events: [{ id: 'ev-store', competitions: [{ competitors: [ { homeAway: 'home', id: '1', team: { abbreviation: 'NYK' } }, { homeAway: 'away', id: '2', team: { abbreviation: 'BOS' } }, ] }], }], }, }); mockGetPlayerProps.mockResolvedValue([ { player: 'P', team: 'NYK', statType: 'points', line: 25.5, direction: 'over' }, ]); mockFeatures.current.set('P', { l5_avg: 30, l20_avg: 28, opp_rank_stat: 0.85, home_away: 1.0, }); await orchestrator.runPipeline('nba', { skipEngine2: true }); const row = mockInserts[0].row; expect(row.factors).not.toBeNull(); expect(Array.isArray(row.factors)).toBe(true); expect(row.factors.length).toBeGreaterThan(0); expect(Number.isFinite(row.p_over)).toBe(true); }); });