123 lines
4.0 KiB
JavaScript
123 lines
4.0 KiB
JavaScript
// PERF-2 (Session 7d): proves analyzeProp runs in parallel inside
|
||
// scanParlay. We mock analyzeProp to sleep — a sequential loop would
|
||
// take N × delay, parallel allSettled takes ~delay.
|
||
|
||
let mockAnalyzeDelayMs = 100;
|
||
let mockAnalyzeCallTimes = [];
|
||
let mockAnalyzeRejectIndices = new Set();
|
||
|
||
jest.mock('../../src/services/propAnalyzer', () => ({
|
||
analyzeProp: 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,
|
||
grade: 'A-',
|
||
confidence: 0.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-');
|
||
});
|
||
});
|