Sessions 5-7a: 955 tests, deployment ready
This commit is contained in:
@@ -0,0 +1,133 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const mockRefAssignments = { current: null };
|
||||
const mockRefProfiles = { current: [] };
|
||||
const mockCoachProfile = { current: null };
|
||||
|
||||
jest.mock('../../src/utils/supabase', () => ({
|
||||
getSupabaseServiceClient: () => ({
|
||||
from(table) {
|
||||
const proxy = {
|
||||
select() { return proxy; },
|
||||
eq() { return proxy; },
|
||||
in() { return Promise.resolve({ data: mockRefProfiles.current, error: null }); },
|
||||
maybeSingle() {
|
||||
if (table === 'game_ref_assignments') return Promise.resolve({ data: mockRefAssignments.current, error: null });
|
||||
if (table === 'coach_profiles') return Promise.resolve({ data: mockCoachProfile.current, error: null });
|
||||
return Promise.resolve({ data: null, error: null });
|
||||
},
|
||||
upsert() { return Promise.resolve({ error: null }); },
|
||||
};
|
||||
return proxy;
|
||||
},
|
||||
}),
|
||||
}));
|
||||
|
||||
const refSignals = require('../../src/services/intelligence/refSignals');
|
||||
const coachSignals = require('../../src/services/intelligence/coachSignals');
|
||||
const lineupSignals = require('../../src/services/intelligence/lineupSignals');
|
||||
|
||||
beforeEach(() => {
|
||||
mockRefAssignments.current = null;
|
||||
mockRefProfiles.current = [];
|
||||
mockCoachProfile.current = null;
|
||||
});
|
||||
|
||||
describe('refSignals', () => {
|
||||
test('getRefImpact returns null when no assignment', async () => {
|
||||
expect(await refSignals.getRefImpact('g-nope')).toBeNull();
|
||||
});
|
||||
|
||||
test('uses precomputed crew values if present on assignment row', async () => {
|
||||
mockRefAssignments.current = {
|
||||
ref1_name: 'Scott Foster',
|
||||
ref2_name: 'Tony Brothers',
|
||||
ref3_name: null,
|
||||
ref_crew_avg_fouls: 22.5,
|
||||
ref_crew_pace_impact: -0.3,
|
||||
};
|
||||
const r = await refSignals.getRefImpact('g-fast');
|
||||
expect(r.avg_fouls).toBe(22.5);
|
||||
expect(r.pace_impact).toBe(-0.3);
|
||||
expect(r.crew).toHaveLength(2);
|
||||
});
|
||||
|
||||
test('averages ref_profiles when assignment has no precomputed values', async () => {
|
||||
mockRefAssignments.current = {
|
||||
ref1_name: 'Foster', ref2_name: 'Brothers', ref3_name: 'Goble',
|
||||
ref_crew_avg_fouls: null, ref_crew_pace_impact: null,
|
||||
};
|
||||
mockRefProfiles.current = [
|
||||
{ ref_name: 'Foster', avg_fouls_per_game: 22, avg_free_throws_per_game: 24, pace_impact: -0.5, home_whistle_bias: 0.1 },
|
||||
{ ref_name: 'Brothers', avg_fouls_per_game: 20, avg_free_throws_per_game: 22, pace_impact: 0.2, home_whistle_bias: 0.0 },
|
||||
];
|
||||
const r = await refSignals.getRefImpact('g-avg');
|
||||
expect(r.avg_fouls).toBe(21);
|
||||
expect(r.pace_impact).toBeCloseTo(-0.15, 5);
|
||||
});
|
||||
});
|
||||
|
||||
describe('coachSignals', () => {
|
||||
test('returns null when coach not in DB nor seed', async () => {
|
||||
expect(await coachSignals.getCoachImpact('nba', 'GHOST')).toBeNull();
|
||||
});
|
||||
|
||||
test('falls back to seed when DB has no record (NYK is in seed)', async () => {
|
||||
const impact = await coachSignals.getCoachImpact('nba', 'NYK');
|
||||
expect(impact).not.toBeNull();
|
||||
expect(impact.coach_name).toBe('Tom Thibodeau');
|
||||
expect(impact.pace_delta).toBeCloseTo(96.5 - 98.1, 5);
|
||||
expect(impact.adjusted_pace_delta).toBe(impact.pace_delta * impact.tenure_adjustment);
|
||||
});
|
||||
|
||||
test('returns system_override when primary player is out', async () => {
|
||||
const impact = await coachSignals.getCoachImpact('nba', 'NYK', { primary_player_status: 'out' });
|
||||
expect(impact.system_override).toBe('motion');
|
||||
expect(impact.without_primary_pace_shift).toBe(1.5);
|
||||
});
|
||||
|
||||
test('tenureAdjustment maps tenure to 0..1 linearly capped', () => {
|
||||
expect(coachSignals.tenureAdjustment(0)).toBe(0);
|
||||
expect(coachSignals.tenureAdjustment(20)).toBeCloseTo(0.5, 5);
|
||||
expect(coachSignals.tenureAdjustment(40)).toBe(1);
|
||||
expect(coachSignals.tenureAdjustment(400)).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('lineupSignals', () => {
|
||||
test('classifyByUsage thresholds', () => {
|
||||
expect(lineupSignals.classifyByUsage(0.32)).toBe('primary_handler');
|
||||
expect(lineupSignals.classifyByUsage(0.22)).toBe('secondary');
|
||||
expect(lineupSignals.classifyByUsage(0.12)).toBe('role_player');
|
||||
expect(lineupSignals.classifyByUsage(undefined)).toBe('role_player');
|
||||
});
|
||||
|
||||
test('roleValue maps role labels to 1.0/0.5/0.0', () => {
|
||||
expect(lineupSignals.roleValue('primary_handler')).toBe(1.0);
|
||||
expect(lineupSignals.roleValue('secondary')).toBe(0.5);
|
||||
expect(lineupSignals.roleValue('role_player')).toBe(0.0);
|
||||
});
|
||||
|
||||
test('getProjectedStarters walks the box score', async () => {
|
||||
// Use the fixture from 6a — real ESPN box.
|
||||
const fixture = JSON.parse(fs.readFileSync(
|
||||
path.join(__dirname, '..', 'fixtures', 'nba-boxscore-sample.json'),
|
||||
'utf8',
|
||||
));
|
||||
const out = await lineupSignals.getProjectedStarters('nba', 'g', fixture);
|
||||
expect(out.home.length).toBeGreaterThan(0);
|
||||
expect(out.away.length).toBeGreaterThan(0);
|
||||
expect(out.home[0].role).toBe('primary_handler');
|
||||
});
|
||||
});
|
||||
|
||||
describe('coaches.json seed', () => {
|
||||
test('seed file is valid JSON with the expected shape', () => {
|
||||
const seed = coachSignals.loadSeed();
|
||||
expect(seed.coaches).toBeDefined();
|
||||
expect(Array.isArray(seed.coaches)).toBe(true);
|
||||
expect(seed.coaches[0]).toHaveProperty('coach_name');
|
||||
expect(seed.coaches[0]).toHaveProperty('team');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user