Sessions 5-7a: 955 tests, deployment ready
This commit is contained in:
@@ -0,0 +1,193 @@
|
||||
/**
|
||||
* Grading pipeline end-to-end integration.
|
||||
*
|
||||
* 1. Mock SharpAPI returning 3 player props (one A-tier shaped, one
|
||||
* neutral, one trap-heavy that should downgrade).
|
||||
* 2. Mock feature cache returning features.
|
||||
* 3. Mock trap detection — return high composite for the trap prop.
|
||||
* 4. Run the orchestrator.
|
||||
* 5. Assert: all 3 props get a grade_history insert.
|
||||
* 6. Assert: A-tier prop queued for Engine 2, D/C-tier not queued.
|
||||
* 7. Assert: trap composite > 0.5 downgrades the trap prop's grade.
|
||||
*/
|
||||
|
||||
process.env.ENGINE2_ENABLED = 'true';
|
||||
|
||||
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),
|
||||
}));
|
||||
|
||||
jest.mock('../../src/services/adapters/openRouterAdapter', () => ({
|
||||
configured: () => false, analyze: async () => null, getUsage: () => ({ requestsToday: 0 }),
|
||||
}));
|
||||
|
||||
// Feature vectors per player — vary to drive different grades.
|
||||
const mockFeatures = { current: new Map() };
|
||||
jest.mock('../../src/services/intelligence/featureCache', () => ({
|
||||
getFeatures: async ({ playerName }) => {
|
||||
const f = mockFeatures.current.get(playerName) || {};
|
||||
return { features: f, meta: { features_available: Object.keys(f), features_missing: [] } };
|
||||
},
|
||||
}));
|
||||
|
||||
const mockTrapScores = { current: new Map() };
|
||||
jest.mock('../../src/services/intelligence/trapDetection', () => ({
|
||||
getTrapScore: async ({ playerName }) => mockTrapScores.current.get(playerName) || {
|
||||
composite: 0, signals: {}, active_count: 0, recommendation: 'proceed',
|
||||
},
|
||||
normalizeName: (n) => n,
|
||||
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 }, { points: 32 }, { points: 27 }, { points: 31 },
|
||||
],
|
||||
getCareerPlayoffGames: async () => 50,
|
||||
getWithWithoutStats: async () => null,
|
||||
}));
|
||||
|
||||
const mockInserts = [];
|
||||
const mockUpdates = [];
|
||||
jest.mock('../../src/utils/supabase', () => ({
|
||||
getSupabaseServiceClient: () => ({
|
||||
from(table) {
|
||||
const proxy = {
|
||||
insert(row) {
|
||||
mockInserts.push({ table, row });
|
||||
return {
|
||||
select() {
|
||||
return { single: () => Promise.resolve({ data: { id: `gid-${mockInserts.length}` }, error: null }) };
|
||||
},
|
||||
};
|
||||
},
|
||||
update(patch) {
|
||||
mockUpdates.push({ table, patch });
|
||||
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;
|
||||
mockUpdates.length = 0;
|
||||
mockFeatures.current.clear();
|
||||
mockTrapScores.current.clear();
|
||||
engine2.clearQueue();
|
||||
});
|
||||
|
||||
describe('grading pipeline — end-to-end through orchestrator', () => {
|
||||
test('three props grade through, A-tier queued, trap-heavy downgraded', async () => {
|
||||
// ESPN scoreboard with one game.
|
||||
mockAxiosGet.mockResolvedValue({
|
||||
status: 200,
|
||||
data: {
|
||||
events: [{
|
||||
id: 'ev-mix',
|
||||
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' } },
|
||||
],
|
||||
}],
|
||||
}],
|
||||
},
|
||||
});
|
||||
|
||||
// Three props:
|
||||
// A-tier: hot recent form, weak D, rested, home — should grade A or A-
|
||||
// neutral: average everything — should land C-ish
|
||||
// trap-heavy: hot recent form BUT trap composite 0.7 → downgrade
|
||||
mockGetPlayerProps.mockResolvedValue([
|
||||
{ player: 'A-Player', team: 'NYK', statType: 'points', line: 25.5, direction: 'over' },
|
||||
{ player: 'C-Player', team: 'NYK', statType: 'points', line: 25.5, direction: 'over' },
|
||||
{ player: 'Trap-Player', team: 'NYK', statType: 'points', line: 25.5, direction: 'over' },
|
||||
]);
|
||||
|
||||
mockFeatures.current.set('A-Player', {
|
||||
l5_avg: 32, l20_avg: 28, opp_rank_stat: 0.85, home_away: 1.0, rest_days: 2,
|
||||
});
|
||||
mockFeatures.current.set('C-Player', { l5_avg: 25, l20_avg: 25.5 });
|
||||
mockFeatures.current.set('Trap-Player', {
|
||||
l5_avg: 32, l20_avg: 28, opp_rank_stat: 0.85, home_away: 1.0, rest_days: 2,
|
||||
});
|
||||
|
||||
// Only the trap-heavy prop has a high trap composite.
|
||||
mockTrapScores.current.set('Trap-Player', { composite: 0.7, signals: {}, active_count: 4, recommendation: 'avoid' });
|
||||
|
||||
const summary = await orchestrator.runPipeline('nba', { skipEngine2: true });
|
||||
|
||||
expect(summary.props_graded).toBe(3);
|
||||
expect(mockInserts.length).toBe(3);
|
||||
|
||||
// All inserts went to grade_history.
|
||||
expect(mockInserts.every((i) => i.table === 'grade_history')).toBe(true);
|
||||
|
||||
// factors column is stored as an array.
|
||||
for (const i of mockInserts) {
|
||||
expect(Array.isArray(i.row.factors) || i.row.factors === null).toBe(true);
|
||||
}
|
||||
|
||||
// A-Player grade is in A range; Trap-Player is downgraded BELOW A.
|
||||
const aRow = mockInserts.find((i) => i.row.player_name === 'A-Player');
|
||||
const cRow = mockInserts.find((i) => i.row.player_name === 'C-Player');
|
||||
const trapRow = mockInserts.find((i) => i.row.player_name === 'Trap-Player');
|
||||
const GRADE_ORDER = ['F','D','C-','C','C+','B-','B','B+','A-','A','A+'];
|
||||
const idx = (g) => GRADE_ORDER.indexOf(g);
|
||||
|
||||
expect(idx(aRow.row.grade)).toBeGreaterThan(idx('B+'));
|
||||
expect(idx(trapRow.row.grade)).toBeLessThan(idx(aRow.row.grade));
|
||||
expect(idx(cRow.row.grade)).toBeLessThan(idx(aRow.row.grade));
|
||||
|
||||
// Engine 2 queue: A-Player qualifies (A range); Trap-Player downgraded
|
||||
// out of A/B range so should not be queued; C-Player C-tier excluded.
|
||||
const queued = engine2.getQueueSize();
|
||||
expect(queued).toBeGreaterThanOrEqual(1);
|
||||
});
|
||||
|
||||
test('p_over and factors get persisted on the row', async () => {
|
||||
mockAxiosGet.mockResolvedValue({
|
||||
status: 200,
|
||||
data: {
|
||||
events: [{
|
||||
id: 'ev-store',
|
||||
competitions: [{ competitors: [
|
||||
{ homeAway: 'home', id: '1', team: { abbreviation: 'NYK' } },
|
||||
{ homeAway: 'away', id: '2', team: { abbreviation: 'BOS' } },
|
||||
] }],
|
||||
}],
|
||||
},
|
||||
});
|
||||
mockGetPlayerProps.mockResolvedValue([
|
||||
{ player: 'P', team: 'NYK', statType: 'points', line: 25.5, direction: 'over' },
|
||||
]);
|
||||
mockFeatures.current.set('P', {
|
||||
l5_avg: 30, l20_avg: 28, opp_rank_stat: 0.85, home_away: 1.0,
|
||||
});
|
||||
await orchestrator.runPipeline('nba', { skipEngine2: true });
|
||||
const row = mockInserts[0].row;
|
||||
expect(row.factors).not.toBeNull();
|
||||
expect(Array.isArray(row.factors)).toBe(true);
|
||||
expect(row.factors.length).toBeGreaterThan(0);
|
||||
expect(Number.isFinite(row.p_over)).toBe(true);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user