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