// PERF-2 (Session 7d): proves analyzeProp runs in parallel inside // scanParlay. ARCH-1 Step 4 (Session 7f): the call target rotated from // `propAnalyzer.analyzeProp` to `intelligence/analyzeViaEngine1`. Mock // target updated to follow; the assertion (parallel timing) is // unchanged. let mockAnalyzeDelayMs = 100; let mockAnalyzeCallTimes = []; let mockAnalyzeRejectIndices = new Set(); jest.mock('../../src/services/intelligence/analyzeViaEngine1', () => ({ analyzeViaEngine1: async (leg) => { const startedAt = Date.now(); mockAnalyzeCallTimes.push(startedAt); await new Promise((r) => setTimeout(r, mockAnalyzeDelayMs)); if (mockAnalyzeRejectIndices.has(leg._idx)) { throw new Error(`forced failure for leg ${leg._idx}`); } return { ...leg, // Mock keeps the legacy-shape 4-letter grade so the existing // value-level assertion ('A-') is preserved verbatim. Real // analyzeViaEngine1 also emits the 4-letter shape via the adapter. grade: 'A-', confidence: 78, edge_pct: 5.2, reasoning: { summary: 'ok', steps: {} }, kill_conditions_triggered: [], }; }, })); jest.mock('../../src/services/oddsService', () => ({ getOdds: async () => ({ spreads: [], props: [] }), })); jest.mock('../../src/services/correlationEngine', () => ({ detectCorrelations: () => [], })); jest.mock('../../src/services/parlayGrader', () => ({ gradeParlayFromLegs: () => ({ grade: 'A-', confidence: 0.7 }), })); jest.mock('../../src/services/upgradePitch', () => ({ generateUpgradePitch: async () => null, })); function makeSelectChain(arrayRows, singleRow) { return { single: () => Promise.resolve({ data: singleRow, error: null }), then: (resolve) => resolve({ data: arrayRows, error: null }), }; } const mockSupabase = { from() { return { insert(rows) { const isArr = Array.isArray(rows); const arrayRows = isArr ? rows.map((_, i) => ({ id: `p${i + 1}` })) : [{ id: 'p1' }]; const singleRow = { id: 'sess-1' }; return { select: () => makeSelectChain(arrayRows, singleRow), }; }, update() { const chain = { eq() { return chain; }, select() { return chain; }, single: () => Promise.resolve({ data: { scan_count: 1 }, error: null }), }; return chain; }, }; }, }; jest.mock('../../src/utils/supabase', () => ({ getSupabaseServiceClient: () => mockSupabase, })); const { scanParlay } = require('../../src/services/parlayScanService'); beforeEach(() => { mockAnalyzeCallTimes = []; mockAnalyzeRejectIndices = new Set(); mockAnalyzeDelayMs = 80; }); describe('parlayScanService parallel leg resolution (PERF-2)', () => { test('6 legs resolve in roughly one delay window, not six', async () => { const user = { id: 'u1', tier: 'desk', scan_count: 0 }; const legs = [0, 1, 2, 3, 4, 5].map((i) => ({ _idx: i, player: `P${i}`, stat_type: 'points', line: 25, direction: 'over', })); const start = Date.now(); const out = await scanParlay(user, legs); const elapsed = Date.now() - start; expect(out.legs).toHaveLength(6); // analyzeProp was invoked once per leg. expect(mockAnalyzeCallTimes.length).toBe(6); // Every call started within a small window of each other — proves // the loop didn't await one before issuing the next. const first = Math.min(...mockAnalyzeCallTimes); const last = Math.max(...mockAnalyzeCallTimes); expect(last - first).toBeLessThan(40); // Sequential 6 × 80ms ≈ 480ms; parallel should land near 80-200ms // depending on host. Leave generous headroom for slow CI. expect(elapsed).toBeLessThan(6 * mockAnalyzeDelayMs * 0.7); }); test('one failed leg does not crash the parlay; the rest succeed', async () => { const user = { id: 'u2', tier: 'desk', scan_count: 0 }; const legs = [0, 1, 2].map((i) => ({ _idx: i, player: `P${i}`, stat_type: 'points', line: 25, direction: 'over', })); mockAnalyzeRejectIndices = new Set([1]); const out = await scanParlay(user, legs); expect(out.legs).toHaveLength(3); // Index 1 is the failed leg — grade should be 'F' from the stub. expect(out.legs[1].grade).toBe('F'); expect(out.legs[0].grade).toBe('A-'); expect(out.legs[2].grade).toBe('A-'); }); });