Files
vyndr/tests/unit/probablePitchers.test.js
T
builtbykev c8fc9f577e Session 46: Grade card intel + name normalization + pitchers (2122 tests)
Three focused P1 fixes on the Session-45 snapshot model.

- Grade card intel ROOT CAUSE: gameLogService is NBA/WNBA-only (offline Python),
  so MLB props never got l5_avg/l20_avg and buildIntelFields returned {}. Wired
  MLB game logs into featureCache.gameLogFeatures via mlbStatsAdapter.getPlayerStats
  (pure mlbGameLogFeatures + MLB stat_type->field map). buildIntelFields gained
  playerStats/projection fallbacks for partial intel.
- Player name normalization: src/utils/playerName.js (+ web/src/lib copy):
  normalizeName -> {display,key}. Strips periods, de-dots suffix, accent-folds
  the key. Applied in snapshotService grouping, slateAdapter grade index +
  player-strip merge (variants collapse, longest name shown), and
  playerIntelService. "A.J. Ewing"/"AJ Ewing" + "Jazz Chisholm"/"Jr." now merge.
- MLB starting pitchers: new GET /api/schedule/:sport/pitchers (probablePitchers
  service wrapping mlbStatsAdapter.getScheduleWithPitchers + best-effort ERA).
  Slate fetches it, builds a team->pitcher map (full name + mascot match),
  attaches pitchers to MLB GameCardData. + Next proxy.

Backend 2100 -> 2122 tests (+22), 176 suites. Web build clean (exit 0).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 23:56:26 -04:00

69 lines
3.0 KiB
JavaScript

// Session 46 — Phase 3: MLB starting pitchers.
const fs = require('fs');
const path = require('path');
const WEB = path.join(__dirname, '..', '..', 'web', 'src');
const svc = require('../../src/services/probablePitchers');
const slate = require('../../web/src/lib/slateAdapter');
const scheduleGames = [
{ home: { team: 'Philadelphia Phillies', probablePitcher: { id: 1, name: 'Zack Wheeler' } }, away: { team: 'Atlanta Braves', probablePitcher: { id: 2, name: 'Spencer Strider' } } },
{ home: { team: 'New York Yankees', probablePitcher: null }, away: { team: 'Boston Red Sox', probablePitcher: { id: 3, name: 'Brayan Bello' } } },
];
describe('shapePitcherGames', () => {
it('extracts pitcher names + team, drops games with no probables', () => {
const shaped = svc.shapePitcherGames(scheduleGames);
expect(shaped).toHaveLength(2);
expect(shaped[0].away.pitcher).toBe('Spencer Strider');
expect(shaped[0].home.pitcher).toBe('Zack Wheeler');
expect(svc.shapePitcherGames([{ home: {}, away: {} }])).toHaveLength(0);
});
});
describe('getProbablePitchers (injected adapter)', () => {
it('returns shaped games with best-effort ERA', async () => {
const games = await svc.getProbablePitchers('2026-06-18', {
mlbAdapter: { getScheduleWithPitchers: async () => scheduleGames },
eraLookup: async (id) => (id === 1 ? 2.89 : id === 2 ? 3.21 : null),
});
const phi = games[0];
expect(phi.home.era).toBe(2.89);
expect(phi.away.era).toBe(3.21);
});
it('degrades to [] when the adapter throws', async () => {
const games = await svc.getProbablePitchers('x', { mlbAdapter: { getScheduleWithPitchers: async () => { throw new Error('down'); } } });
expect(games).toEqual([]);
});
});
describe('slateAdapter pitcher mapping', () => {
const shaped = svc.shapePitcherGames(scheduleGames).map((g) => ({
home: { ...g.home, era: 2.89 }, away: { ...g.away, era: 3.21 },
}));
it('builds a team→pitcher map and resolves a game by team name', () => {
const map = slate.buildPitcherMap(shaped);
const p = slate.pitchersForGameTeams('Atlanta Braves', 'Philadelphia Phillies', map);
expect(p.away.name).toBe('Spencer Strider');
expect(p.away.era).toBe('3.21');
expect(p.home.name).toBe('Zack Wheeler');
});
it('matches by mascot when the full name differs slightly', () => {
const map = slate.buildPitcherMap(shaped);
const p = slate.pitchersForGameTeams('Braves', 'Phillies', map);
expect(p.away.name).toBe('Spencer Strider');
});
it('returns undefined when no pitchers match', () => {
expect(slate.pitchersForGameTeams('Dodgers', 'Giants', slate.buildPitcherMap(shaped))).toBeUndefined();
});
});
describe('Slate wires MLB pitchers', () => {
const src = fs.readFileSync(path.join(WEB, 'components', 'Slate.tsx'), 'utf8');
it('fetches /api/schedule/mlb/pitchers and attaches pitchers for MLB', () => {
expect(src).toContain('/api/schedule/mlb/pitchers');
expect(src).toContain('buildPitcherMap');
expect(src).toContain('pitchersForGameTeams');
});
});