process.env.ENGINE2_ENABLED = 'false'; // queue but don't drain in unit tests 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), getGameOdds: async () => null, getConsensusLine: async () => null, })); jest.mock('../../src/services/adapters/openRouterAdapter', () => ({ configured: () => false, analyze: async () => null, getUsage: () => ({ requestsToday: 0, requestsRemaining: 1000 }), })); // Feature/trap/consistency stubs — orchestrator owns the wiring; we // verify that wiring with deterministic returns rather than re-testing // every sub-service. jest.mock('../../src/services/intelligence/featureCache', () => ({ getFeatures: async () => ({ features: { l5_avg: 30, l20_avg: 26, home_away: 1.0, opp_rank_stat: 0.85, rest_days: 2 }, meta: { features_available: [], features_missing: [] }, }), })); jest.mock('../../src/services/intelligence/trapDetection', () => ({ getTrapScore: async () => ({ composite: 0.1, signals: {}, active_count: 0, recommendation: 'proceed', }), normalizeName: (n) => String(n || '').toLowerCase(), 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 }], getCareerPlayoffGames: async () => null, getWithWithoutStats: async () => null, })); const mockInserts = []; jest.mock('../../src/utils/supabase', () => ({ getSupabaseServiceClient: () => ({ from() { const proxy = { insert(row) { mockInserts.push(row); const inserted = { id: `gid-${mockInserts.length}` }; return { select() { return { single: () => Promise.resolve({ data: inserted, error: null }) }; }, }; }, update() { 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; engine2.clearQueue(); }); describe('runPipeline', () => { test('empty slate → summary with zeros', async () => { mockAxiosGet.mockResolvedValue({ status: 200, data: { events: [] } }); const summary = await orchestrator.runPipeline('nba'); expect(summary).toMatchObject({ sport: 'nba', games_processed: 0, props_graded: 0, }); }); test('one game with three props → 3 grades inserted', async () => { mockAxiosGet.mockResolvedValue({ status: 200, data: { events: [{ id: 'ev-1', 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' } }, ], }], }], }, }); mockGetPlayerProps.mockResolvedValue([ { player: 'Jalen Brunson', team: 'NYK', statType: 'points', line: 26.5, direction: 'over' }, { player: 'Jayson Tatum', team: 'BOS', statType: 'points', line: 28.5, direction: 'over' }, { player: 'OG Anunoby', team: 'NYK', statType: 'rebounds', line: 6.5, direction: 'under' }, ]); const summary = await orchestrator.runPipeline('nba', { skipEngine2: true }); expect(summary.games_processed).toBe(1); expect(summary.props_graded).toBe(3); expect(mockInserts).toHaveLength(3); }); test('SharpAPI failure on one game increments errors, other games continue', async () => { mockAxiosGet.mockResolvedValue({ status: 200, data: { events: [ { id: 'ev-a', competitions: [{ competitors: [{ homeAway: 'home', team: { abbreviation: 'A' } }, { homeAway: 'away', team: { abbreviation: 'B' } }] }] }, { id: 'ev-b', competitions: [{ competitors: [{ homeAway: 'home', team: { abbreviation: 'C' } }, { homeAway: 'away', team: { abbreviation: 'D' } }] }] }, ], }, }); mockGetPlayerProps .mockRejectedValueOnce(new Error('upstream down')) .mockResolvedValueOnce([{ player: 'X', team: 'C', statType: 'points', line: 10, direction: 'over' }]); const summary = await orchestrator.runPipeline('nba', { skipEngine2: true }); expect(summary.errors).toBe(1); expect(summary.props_graded).toBe(1); }); test('A-tier grades queued for engine2; C-tier not queued', async () => { process.env.ENGINE2_ENABLED = 'true'; jest.resetModules(); // Re-require with fresh ENGINE2_ENABLED. jest.doMock('axios', () => ({ get: (...args) => mockAxiosGet(...args), post: jest.fn() })); jest.doMock('../../src/services/adapters/sharpApiAdapter', () => ({ configured: () => true, getPlayerProps: (...args) => mockGetPlayerProps(...args), })); jest.doMock('../../src/services/adapters/openRouterAdapter', () => ({ configured: () => false, analyze: async () => null, getUsage: () => ({ requestsToday: 0, requestsRemaining: 1000 }), })); jest.doMock('../../src/services/intelligence/featureCache', () => ({ getFeatures: async () => ({ features: { l5_avg: 32, l20_avg: 27, opp_rank_stat: 0.85, home_away: 1.0, rest_days: 2 }, meta: {} }), })); jest.doMock('../../src/services/intelligence/trapDetection', () => ({ getTrapScore: async () => ({ composite: 0.05, signals: {} }), normalizeName: (n) => n, })); jest.doMock('../../src/services/intelligence/consistencyScore', () => ({ getConsistency: async () => ({ consistency: 'elite', score: 1.0 }), })); jest.doMock('../../src/services/intelligence/gameLogService', () => ({ getGameLogs: async () => null, getCareerPlayoffGames: async () => null, })); jest.doMock('../../src/utils/supabase', () => ({ getSupabaseServiceClient: () => ({ from() { const proxy = { insert(row) { mockInserts.push(row); return { select() { return { single: () => Promise.resolve({ data: { id: `gid-${mockInserts.length}` }, error: null }) }; } }; }, update() { return { eq() { return Promise.resolve({ error: null }); } }; }, }; return proxy; }, }), })); const orch = require('../../src/services/intelligence/gradingOrchestrator'); const e2 = require('../../src/services/intelligence/engine2'); e2.clearQueue(); mockInserts.length = 0; mockAxiosGet.mockResolvedValue({ status: 200, data: { events: [{ id: 'ev-q', competitions: [{ competitors: [ { homeAway: 'home', team: { abbreviation: 'NYK' } }, { homeAway: 'away', team: { abbreviation: 'BOS' } }, ] }], }], }, }); mockGetPlayerProps.mockResolvedValue([ { player: 'A-tier player', team: 'NYK', statType: 'points', line: 25.5, direction: 'over' }, ]); const summary = await orch.runPipeline('nba', { skipEngine2: true }); expect(summary.props_graded).toBe(1); // The grade should have been A/A-/A+ given those features, so engine2 // queue should have grown. expect(summary.engine2_queued).toBe(1); process.env.ENGINE2_ENABLED = 'false'; }); }); describe('getEngineStatus', () => { test('returns adapter configuration and queue size', () => { const status = orchestrator.getEngineStatus(); expect(status.adapters_configured).toHaveProperty('sharp_api'); expect(status.adapters_configured).toHaveProperty('open_router'); expect(typeof status.engine2_queue_size).toBe('number'); }); });