134 lines
5.1 KiB
JavaScript
134 lines
5.1 KiB
JavaScript
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');
|
|
});
|
|
});
|