216 lines
8.1 KiB
JavaScript
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');
|
|
});
|
|
});
|