Files
vyndr/tests/unit/parlayScanParallel.test.js
T

123 lines
4.0 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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-');
});
});