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

128 lines
4.3 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. 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-');
});
});