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

216 lines
8.1 KiB
JavaScript

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');
});
});