import test from 'node:test'; import assert from 'node:assert/strict'; import { commands } from '../src/commands.js'; import { buildBestMatchupsEmbed, buildHrProfileEmbed, buildKProfileEmbed, buildMatchupHittersEmbed, buildPlayerContextEmbed, } from '../src/embeds.js'; import { CockroachMatchupSource, HostedArtifactSource, MatchupService } from '../src/matchups.js'; test('hosted artifact source resolves latest available daily slate and caches parquet reads', async () => { const responses = new Map([ ['https://example.test/daily/2026-04-06/slate.parquet', [ { game_pk: 10, away_team: 'CHC', home_team: 'MIL', away_probable_pitcher_id: 1001, home_probable_pitcher_id: 1002, away_probable_pitcher: 'Shota Imanaga', home_probable_pitcher: 'Freddy Peralta', away_probable_hand: 'L', home_probable_hand: 'R', }, ]], ['https://example.test/daily/2026-04-06/daily_hitter_metrics.parquet', [ { team: 'CHC', hitter_name: '100', batter: 100, stand: 'R', split_key: 'vs_rhp', recent_window: 'season', weighted_mode: 'weighted', swstr_pct: 0.08, barrel_bbe_pct: 0.16, barrel_bip_pct: 0.14, pulled_barrel_pct: 0.11, sweet_spot_pct: 0.37, likely_starter_score: 98.2, xwoba: 0.398, fb_pct: 0.29, hard_hit_pct: 48.2, avg_launch_angle: 17.2, }, ]], ['https://example.test/daily/2026-04-06/daily_pitcher_metrics.parquet', [ { pitcher_id: 1001, pitcher_name: 'Shota Imanaga', p_throws: 'L', split_key: 'overall', recent_window: 'season', weighted_mode: 'weighted', }, { pitcher_id: 1002, pitcher_name: 'Freddy Peralta', p_throws: 'R', split_key: 'overall', recent_window: 'season', weighted_mode: 'weighted', }, ]], ['https://example.test/daily/2026-04-06/hitter_pitcher_exclusions.parquet', []], ['https://example.test/daily/2026-04-06/rosters.parquet', [ { team: 'CHC', player_id: 100, player_name: 'Seiya Suzuki' }, ]], ['https://example.test/daily/2026-04-06/daily_batter_zone_profiles.parquet', []], ['https://example.test/daily/2026-04-06/daily_pitcher_zone_profiles.parquet', []], ]); const calls = []; const source = new HostedArtifactSource( { baseUrl: 'https://example.test', cacheTtlMs: 60_000, fallbackDays: 2, }, { readParquetImpl: async (url) => { calls.push(url); if (!responses.has(url)) { throw new Error(`Missing ${url}`); } return responses.get(url); }, fetchTextImpl: async () => '', logger: { debug() {}, warn() {} }, } ); const first = await source.getTopHitters({ date: '2026-04-07' }); const second = await source.getTopHitters({ date: '2026-04-07' }); assert.equal(first.resolvedDate, '2026-04-06'); assert.equal(first.rows[0].hitter_name, 'Seiya Suzuki'); assert.equal(first.rows[0].opponent_team, 'MIL'); assert.equal(second.rows[0].opposing_pitcher_name, 'Freddy Peralta'); assert.equal(calls.filter((url) => url.includes('2026-04-06/slate.parquet')).length, 1); assert.equal(calls.filter((url) => url.includes('daily_hitter_metrics.parquet')).length, 1); }); test('hosted artifact source matches common team aliases against team abbreviations', async () => { const responses = new Map([ ['https://example.test/daily/2026-04-07/slate.parquet', [ { game_pk: 20, away_team: 'ATH', home_team: 'NYY', away_probable_pitcher_id: 2001, home_probable_pitcher_id: 2002, away_probable_pitcher: 'JP Sears', home_probable_pitcher: 'Carlos Rodon', away_probable_hand: 'L', home_probable_hand: 'L', }, ]], ['https://example.test/daily/2026-04-07/daily_hitter_metrics.parquet', [ { team: 'NYY', hitter_name: '592450', batter: 99, stand: 'R', split_key: 'vs_lhp', recent_window: 'season', weighted_mode: 'weighted', xwoba: 0.455, fb_pct: 29.4, pulled_barrel_pct: 18.7, sweet_spot_pct: 36.8, barrel_bip_pct: 22.1, hard_hit_pct: 61.0, avg_launch_angle: 14.2, }, ]], ['https://example.test/daily/2026-04-07/daily_pitcher_metrics.parquet', [ { pitcher_id: 2001, pitcher_name: 'JP Sears', p_throws: 'L', split_key: 'overall', recent_window: 'season', weighted_mode: 'weighted' }, { pitcher_id: 2002, pitcher_name: 'Carlos Rodon', p_throws: 'L', split_key: 'overall', recent_window: 'season', weighted_mode: 'weighted' }, ]], ['https://example.test/daily/2026-04-07/hitter_pitcher_exclusions.parquet', []], ['https://example.test/daily/2026-04-07/rosters.parquet', [ { team: 'NYY', player_id: 99, player_name: 'Aaron Judge', }, ]], ['https://example.test/daily/2026-04-07/daily_batter_zone_profiles.parquet', []], ['https://example.test/daily/2026-04-07/daily_pitcher_zone_profiles.parquet', []], ]); const source = new HostedArtifactSource( { baseUrl: 'https://example.test', cacheTtlMs: 60_000, fallbackDays: 0, }, { readParquetImpl: async (url) => { if (!responses.has(url)) { throw new Error(`Missing ${url}`); } return responses.get(url); }, fetchTextImpl: async () => '', logger: { debug() {}, warn() {} }, } ); const result = await source.getBestMatchups({ date: '2026-04-07', team: 'Yankees' }); assert.equal(result.rows.length, 1); assert.equal(result.rows[0].team, 'NYY'); assert.equal(result.rows[0].hitter_name, 'Aaron Judge'); assert.equal(result.rows[0].sweet_spot_pct, 36.8); }); test('hosted best matchups uses pitcher-hand split rows from hosted slate inputs', async () => { const responses = new Map([ ['https://example.test/daily/2026-04-07/slate.parquet', [ { game_pk: 20, away_team: 'BOS', home_team: 'MIL', away_probable_pitcher_id: 501, home_probable_pitcher_id: 601, away_probable_pitcher: 'Garrett Crochet', home_probable_pitcher: 'Freddy Peralta', away_probable_hand: 'L', home_probable_hand: 'R', }, ]], ['https://example.test/daily/2026-04-07/daily_hitter_metrics.parquet', [ { team: 'MIL', hitter_name: '123', batter: 123, stand: 'R', split_key: 'overall', recent_window: 'season', weighted_mode: 'weighted', xwoba: 0.200, swstr_pct: 0.20, barrel_bbe_pct: 0.01, barrel_bip_pct: 0.01, pulled_barrel_pct: 0.01, sweet_spot_pct: 0.20, fb_pct: 0.20, hard_hit_pct: 0.20, avg_launch_angle: 5, }, { team: 'MIL', hitter_name: '123', batter: 123, stand: 'R', split_key: 'vs_lhp', recent_window: 'season', weighted_mode: 'weighted', xwoba: 0.380, swstr_pct: 0.08, barrel_bbe_pct: 0.16, barrel_bip_pct: 0.14, pulled_barrel_pct: 0.11, sweet_spot_pct: 0.39, fb_pct: 0.31, hard_hit_pct: 0.49, avg_launch_angle: 20, }, ]], ['https://example.test/daily/2026-04-07/daily_pitcher_metrics.parquet', [ { pitcher_id: 501, pitcher_name: 'Garrett Crochet', p_throws: 'L', split_key: 'overall', recent_window: 'season', weighted_mode: 'weighted', }, { pitcher_id: 601, pitcher_name: 'Freddy Peralta', p_throws: 'R', split_key: 'overall', recent_window: 'season', weighted_mode: 'weighted', }, ]], ['https://example.test/daily/2026-04-07/hitter_pitcher_exclusions.parquet', []], ['https://example.test/daily/2026-04-07/rosters.parquet', [ { team: 'MIL', player_id: 123, player_name: 'Gary Sanchez' }, ]], ['https://example.test/daily/2026-04-07/daily_batter_zone_profiles.parquet', []], ['https://example.test/daily/2026-04-07/daily_pitcher_zone_profiles.parquet', []], ]); const source = new HostedArtifactSource( { baseUrl: 'https://example.test', cacheTtlMs: 60_000, fallbackDays: 0, }, { readParquetImpl: async (url) => { if (!responses.has(url)) { throw new Error(`Missing ${url}`); } return responses.get(url); }, fetchTextImpl: async () => '', logger: { debug() {}, warn() {} }, } ); const result = await source.getBestMatchups({ date: '2026-04-07', team: 'Brewers' }); assert.equal(result.rows.length, 1); assert.equal(result.rows[0].hitter_name, 'Gary Sanchez'); assert.equal(result.rows[0].opposing_pitcher_name, 'Garrett Crochet'); assert.equal(result.rows[0].matchup_score > 50, true); }); test('matchup service enriches hosted top hitter rows from Cockroach snapshots', async () => { const teamArgs = []; const service = new MatchupService( { databaseUrl: 'postgresql://test', hosted: { baseUrl: 'https://example.test' } }, { hosted: { isConfigured: () => true, async getTopHitters() { return { source: 'hosted', resolvedDate: '2026-04-07', rows: [ { hitter_name: 'Aaron Judge', team: 'NYY', xwoba: 0.455, barrel_bip_pct: 22.1, pulled_barrel_pct: 18.7, fb_pct: 29.4, sweet_spot_pct: 36.8, avg_launch_angle: 14.2 }, { hitter_name: 'Ben Rice', team: 'NYY', xwoba: 0.397, barrel_bip_pct: 17.4, pulled_barrel_pct: 11.2, fb_pct: 31.6, sweet_spot_pct: 42.7, avg_launch_angle: 16.7 }, { hitter_name: 'Paul Goldschmidt', team: 'NYY', xwoba: 0.354, barrel_bip_pct: 9.3, pulled_barrel_pct: 7.6, fb_pct: 28.9, sweet_spot_pct: 37.3, avg_launch_angle: 12.1 }, { hitter_name: 'Austin Wells', team: 'NYY', xwoba: 0.328, barrel_bip_pct: 8.2, pulled_barrel_pct: 4.3, fb_pct: 24.1, sweet_spot_pct: 33.2, avg_launch_angle: 11.5 }, ], }; }, async getHealth() { return { configured: true, latestDate: '2026-04-07' }; }, }, fallback: { async getHitterSnapshotRows(options = {}) { teamArgs.push(options.team ?? null); return [ { hitter_name: 'Aaron Judge', team: 'NYY', matchup_score: 59.7, ceiling_score: 78.2, zone_fit_score: 48.1, likely_starter_score: 99.0 }, { hitter_name: 'Ben Rice', team: 'NYY', matchup_score: 70.3, ceiling_score: 81.0, zone_fit_score: 63.4, likely_starter_score: 98.0 }, { hitter_name: 'Paul Goldschmidt', team: 'NYY', matchup_score: 47.4, ceiling_score: 60.2, zone_fit_score: 42.5, likely_starter_score: 97.0 }, { hitter_name: 'Austin Wells', team: 'NYY', matchup_score: 35.1, ceiling_score: 49.3, zone_fit_score: 31.1, likely_starter_score: 96.0 }, ]; }, async getHealth() { return { configured: true, latestDate: '2026-04-07' }; }, async close() {}, }, logger: { warn() {} }, } ); const result = await service.getTopHitters({ date: '2026-04-07', team: 'Yankees', limit: 3 }); assert.equal(result.rows.length, 3); assert.equal(teamArgs[0], 'Yankees'); assert.equal(result.rows[0].hitter_name, 'Ben Rice'); assert.equal(result.rows[0].matchup_score, 70.3); assert.equal(result.rows[2].hitter_name, 'Paul Goldschmidt'); }); test('hr zone data falls back to the slate probable pitcher when hitter rows leave the pitcher name blank', async () => { const responses = new Map([ ['https://example.test/daily/2026-04-07/slate.parquet', [ { game_pk: 20, away_team: 'LAA', home_team: 'SEA', away_probable_pitcher_id: 501, home_probable_pitcher_id: 601, away_probable_pitcher: 'Jose Soriano', home_probable_pitcher: 'Bryan Woo', away_probable_hand: 'R', home_probable_hand: 'R', }, ]], ['https://example.test/daily/2026-04-07/daily_hitter_metrics.parquet', [ { game_pk: 20, team: 'LAA', hitter_name: '142', batter: 142, stand: 'R', split_key: 'overall', recent_window: 'season', weighted_mode: 'weighted', opposing_pitcher_id: '', opposing_pitcher_name: '', opposing_pitcher_hand: 'R', zone_fit_score: 0.057, xwoba: 0.355, swstr_pct: 0.11, barrel_bbe_pct: 0.12, barrel_bip_pct: 0.10, pulled_barrel_pct: 0.08, sweet_spot_pct: 0.31, fb_pct: 0.27, hard_hit_pct: 0.44, avg_launch_angle: 16.1, }, ]], ['https://example.test/daily/2026-04-07/daily_pitcher_metrics.parquet', [ { pitcher_id: 601, pitcher_name: 'Bryan Woo', p_throws: 'R', split_key: 'overall', recent_window: 'season', weighted_mode: 'weighted' }, ]], ['https://example.test/daily/2026-04-07/rosters.parquet', [ { team: 'LAA', player_id: 142, player_name: 'Jorge Soler' }, ]], ['https://example.test/daily/2026-04-07/daily_batter_zone_profiles.parquet', [ { batter_id: 142, pitcher_hand_key: 'vs_rhp', zone: 5, hit_rate: 0.22, hr_rate: 0.09, sample_size: 40 }, { batter_id: 142, pitcher_hand_key: 'vs_rhp', zone: 8, hit_rate: 0.18, hr_rate: 0.05, sample_size: 20 }, ]], ['https://example.test/daily/2026-04-07/daily_pitcher_zone_profiles.parquet', [ { pitcher_id: 601, batter_side_key: 'vs_rhb', zone: 5, usage_rate: 0.25, sample_size: 60 }, { pitcher_id: 601, batter_side_key: 'vs_rhb', zone: 8, usage_rate: 0.11, sample_size: 30 }, ]], ]); const hosted = new HostedArtifactSource( { baseUrl: 'https://example.test', cacheTtlMs: 60_000, fallbackDays: 0, }, { readParquetImpl: async (url) => { if (!responses.has(url)) { throw new Error(`Missing ${url}`); } return responses.get(url); }, fetchTextImpl: async () => '', logger: { debug() {}, warn() {} }, } ); const service = new MatchupService( { databaseUrl: 'postgresql://test', hosted: { baseUrl: 'https://example.test' } }, { hosted, fallback: { async close() {} }, logger: { debug() {}, warn() {} }, } ); const result = await service.getHrZoneData({ date: '2026-04-07', player: 'jorge soler' }); assert.equal(result.playerName, 'Jorge Soler'); assert.equal(result.opposingPitcherName, 'Bryan Woo'); assert.equal(result.opposingPitcherHand, 'R'); }); test('k profile falls back to broader hosted pitcher context when the daily chart slice misses the pitcher', async () => { const responses = new Map([ ['https://example.test/daily/2026-04-07/slate.parquet', [ { game_pk: 40, away_team: 'PIT', home_team: 'CHC', away_probable_pitcher_id: 301, home_probable_pitcher_id: 401, away_probable_pitcher: 'Paul Skenes', home_probable_pitcher: 'Jameson Taillon', away_probable_hand: 'R', home_probable_hand: 'R', }, ]], ['https://example.test/daily/2026-04-07/daily_hitter_metrics.parquet', []], ['https://example.test/daily/2026-04-07/daily_pitcher_metrics.parquet', [ { pitcher_id: 401, pitcher_name: 'Jameson Taillon', team: 'CHC', opponent_team: 'PIT', p_throws: 'R', split_key: 'overall', recent_window: 'season', weighted_mode: 'weighted' }, ]], ['https://example.test/daily/2026-04-07/rosters.parquet', [ { team: 'PIT', player_id: 301, player_name: 'Paul Skenes' }, { team: 'CHC', player_id: 401, player_name: 'Jameson Taillon' }, ]], ['https://example.test/reusable/hitter_metrics.parquet', []], ['https://example.test/reusable/pitcher_metrics.parquet', [ { pitcher_id: 301, player_id: 301, pitcher_name: '301', team: 'PIT', opponent_team: 'CHC', p_throws: 'R', split_key: 'overall', recent_window: 'season', weighted_mode: 'weighted', pitcher_score: 92.2, strikeout_score: 89.5, pitcher_matchup_adjustment: 6.3, strikeout_matchup_adjustment: 7.1, csw_pct: 0.312, swstr_pct: 0.176, putaway_pct: 0.281, opponent_whiff_tendency: 27.4, }, ]], ['https://example.test/reusable/hitter_rolling.parquet', []], ['https://example.test/reusable/pitcher_rolling.parquet', []], ['https://example.test/reusable/batter_zone_profiles.parquet', []], ['https://example.test/reusable/pitcher_zone_profiles.parquet', []], ['https://example.test/reusable/pitcher_arsenal.parquet', []], ['https://example.test/reusable/pitcher_usage_by_count.parquet', []], ]); const hosted = new HostedArtifactSource( { baseUrl: 'https://example.test', cacheTtlMs: 60_000, fallbackDays: 0, }, { readParquetImpl: async (url) => { if (!responses.has(url)) { throw new Error(`Missing ${url}`); } return responses.get(url); }, fetchTextImpl: async () => '', logger: { debug() {}, warn() {} }, } ); const service = new MatchupService( { databaseUrl: 'postgresql://test', hosted: { baseUrl: 'https://example.test' } }, { hosted, fallback: { async close() {} }, logger: { debug() {}, warn() {} }, } ); const result = await service.getKProfileData({ date: '2026-04-07', pitcher: 'paul skenes' }); assert.equal(result.pitcherName, 'Paul Skenes'); assert.equal(result.team, 'PIT'); assert.equal(result.opponentTeam, 'CHC'); assert.equal(result.pitcher_score, 92.2); assert.equal(result.strikeout_score, 89.5); }); test('hosted best matchups keeps only the top three hitters per game before ranking the slate board', async () => { const responses = new Map([ ['https://example.test/daily/2026-04-07/slate.parquet', [ { game_pk: 1, away_team: 'NYY', home_team: 'ATH', away_probable_pitcher_id: 101, home_probable_pitcher_id: 201, away_probable_pitcher: 'Away One', home_probable_pitcher: 'Home One', away_probable_hand: 'R', home_probable_hand: 'L' }, { game_pk: 2, away_team: 'BOS', home_team: 'BAL', away_probable_pitcher_id: 301, home_probable_pitcher_id: 401, away_probable_pitcher: 'Away Two', home_probable_pitcher: 'Home Two', away_probable_hand: 'R', home_probable_hand: 'R' }, ]], ['https://example.test/daily/2026-04-07/daily_hitter_metrics.parquet', [ { team: 'NYY', hitter_name: '11', batter: 11, stand: 'R', split_key: 'vs_lhp', recent_window: 'season', weighted_mode: 'weighted', xwoba: 0.410, swstr_pct: 0.08, barrel_bbe_pct: 0.20, barrel_bip_pct: 0.18, pulled_barrel_pct: 0.13, sweet_spot_pct: 0.40, fb_pct: 0.30, hard_hit_pct: 0.50, avg_launch_angle: 20 }, { team: 'ATH', hitter_name: '12', batter: 12, stand: 'R', split_key: 'vs_rhp', recent_window: 'season', weighted_mode: 'weighted', xwoba: 0.380, swstr_pct: 0.09, barrel_bbe_pct: 0.16, barrel_bip_pct: 0.14, pulled_barrel_pct: 0.11, sweet_spot_pct: 0.38, fb_pct: 0.28, hard_hit_pct: 0.47, avg_launch_angle: 19 }, { team: 'NYY', hitter_name: '13', batter: 13, stand: 'L', split_key: 'vs_lhp', recent_window: 'season', weighted_mode: 'weighted', xwoba: 0.360, swstr_pct: 0.10, barrel_bbe_pct: 0.14, barrel_bip_pct: 0.12, pulled_barrel_pct: 0.08, sweet_spot_pct: 0.35, fb_pct: 0.25, hard_hit_pct: 0.43, avg_launch_angle: 17 }, { team: 'ATH', hitter_name: '14', batter: 14, stand: 'L', split_key: 'vs_rhp', recent_window: 'season', weighted_mode: 'weighted', xwoba: 0.340, swstr_pct: 0.11, barrel_bbe_pct: 0.12, barrel_bip_pct: 0.10, pulled_barrel_pct: 0.07, sweet_spot_pct: 0.32, fb_pct: 0.23, hard_hit_pct: 0.40, avg_launch_angle: 15 }, { team: 'BOS', hitter_name: '21', batter: 21, stand: 'R', split_key: 'vs_rhp', recent_window: 'season', weighted_mode: 'weighted', xwoba: 0.405, swstr_pct: 0.08, barrel_bbe_pct: 0.19, barrel_bip_pct: 0.17, pulled_barrel_pct: 0.12, sweet_spot_pct: 0.39, fb_pct: 0.29, hard_hit_pct: 0.49, avg_launch_angle: 20 }, { team: 'BAL', hitter_name: '22', batter: 22, stand: 'R', split_key: 'vs_rhp', recent_window: 'season', weighted_mode: 'weighted', xwoba: 0.370, swstr_pct: 0.09, barrel_bbe_pct: 0.15, barrel_bip_pct: 0.13, pulled_barrel_pct: 0.09, sweet_spot_pct: 0.36, fb_pct: 0.27, hard_hit_pct: 0.45, avg_launch_angle: 18 }, { team: 'BOS', hitter_name: '23', batter: 23, stand: 'L', split_key: 'vs_rhp', recent_window: 'season', weighted_mode: 'weighted', xwoba: 0.350, swstr_pct: 0.10, barrel_bbe_pct: 0.13, barrel_bip_pct: 0.11, pulled_barrel_pct: 0.07, sweet_spot_pct: 0.33, fb_pct: 0.24, hard_hit_pct: 0.41, avg_launch_angle: 16 }, { team: 'BAL', hitter_name: '24', batter: 24, stand: 'L', split_key: 'vs_rhp', recent_window: 'season', weighted_mode: 'weighted', xwoba: 0.330, swstr_pct: 0.11, barrel_bbe_pct: 0.11, barrel_bip_pct: 0.09, pulled_barrel_pct: 0.06, sweet_spot_pct: 0.31, fb_pct: 0.22, hard_hit_pct: 0.39, avg_launch_angle: 14 }, ]], ['https://example.test/daily/2026-04-07/daily_pitcher_metrics.parquet', [ { pitcher_id: 101, pitcher_name: 'Away One', p_throws: 'R', split_key: 'overall', recent_window: 'season', weighted_mode: 'weighted' }, { pitcher_id: 201, pitcher_name: 'Home One', p_throws: 'L', split_key: 'overall', recent_window: 'season', weighted_mode: 'weighted' }, { pitcher_id: 301, pitcher_name: 'Away Two', p_throws: 'R', split_key: 'overall', recent_window: 'season', weighted_mode: 'weighted' }, { pitcher_id: 401, pitcher_name: 'Home Two', p_throws: 'R', split_key: 'overall', recent_window: 'season', weighted_mode: 'weighted' }, ]], ['https://example.test/daily/2026-04-07/hitter_pitcher_exclusions.parquet', []], ['https://example.test/daily/2026-04-07/rosters.parquet', [ { team: 'NYY', player_id: 11, player_name: 'Game One A' }, { team: 'ATH', player_id: 12, player_name: 'Game One B' }, { team: 'NYY', player_id: 13, player_name: 'Game One C' }, { team: 'ATH', player_id: 14, player_name: 'Game One D' }, { team: 'BOS', player_id: 21, player_name: 'Game Two A' }, { team: 'BAL', player_id: 22, player_name: 'Game Two B' }, { team: 'BOS', player_id: 23, player_name: 'Game Two C' }, { team: 'BAL', player_id: 24, player_name: 'Game Two D' }, ]], ['https://example.test/daily/2026-04-07/daily_batter_zone_profiles.parquet', []], ['https://example.test/daily/2026-04-07/daily_pitcher_zone_profiles.parquet', []], ]); const source = new HostedArtifactSource( { baseUrl: 'https://example.test', cacheTtlMs: 60_000, fallbackDays: 0, }, { readParquetImpl: async (url) => { if (!responses.has(url)) { throw new Error(`Missing ${url}`); } return responses.get(url); }, fetchTextImpl: async () => '', logger: { debug() {}, warn() {} }, } ); const result = await source.getBestMatchups({ date: '2026-04-07' }); assert.equal(result.rows.length, 6); assert.deepEqual( result.rows.map((row) => row.hitter_name), ['Game One A', 'Game Two A', 'Game One B', 'Game Two B', 'Game One C', 'Game Two C'] ); }); test('matchup service falls back to Cockroach when hosted source fails', async () => { const service = new MatchupService( { databaseUrl: 'postgresql://test', hosted: { baseUrl: 'https://example.test' } }, { hosted: { isConfigured: () => true, async getTopPitchers() { throw new Error('hosted down'); }, async getHealth() { return { configured: true, latestDate: null }; }, }, fallback: { async getTopPitchers() { return { source: 'cockroach', resolvedDate: '2026-04-06', rows: [{ pitcher_name: 'Tarik Skubal', pitcher_score: 91.2, strikeout_score: 88.1 }], }; }, async getHealth() { return { configured: true, latestDate: '2026-04-06' }; }, async close() {}, }, logger: { warn() {} }, } ); const result = await service.getTopPitchers({ date: '2026-04-07' }); assert.equal(result.source, 'cockroach'); assert.equal(result.rows[0].pitcher_name, 'Tarik Skubal'); assert.match(result.warning, /hosted down/i); }); test('commands include new matchup commands', () => { const names = commands.map((command) => command.name); assert.ok(names.includes('matchuphitters')); assert.ok(names.includes('matchuppitchers')); assert.ok(names.includes('playercontext')); assert.ok(names.includes('bestmatchups')); assert.ok(names.includes('teambestmatchups')); assert.ok(names.includes('matchuphealth')); assert.ok(names.includes('hrboardchart')); assert.ok(names.includes('hrtrend')); assert.ok(names.includes('hrprofile')); assert.ok(names.includes('hrvaluechart')); assert.ok(names.includes('hrzone')); assert.ok(names.includes('ktrend')); assert.ok(names.includes('kladder')); assert.ok(names.includes('kprofile')); assert.ok(names.includes('kmatchup')); assert.ok(names.includes('kcount')); assert.ok(names.includes('pitchertrend')); assert.ok(names.includes('pitcherarsenal')); assert.ok(names.includes('pitcherlocation')); assert.ok(names.includes('pitcherapproach')); assert.ok(names.includes('pitchercompare')); }); test('pitcher suite chart methods build cockroach-backed payloads from query access', async () => { const service = new MatchupService( { databaseUrl: 'postgresql://test', hosted: { baseUrl: 'https://example.test' } }, { hosted: { isConfigured: () => false, async getHealth() { return { configured: false, latestDate: null }; }, }, fallback: { async query(text) { if (text.includes('WITH candidates')) { return { rows: [{ pitcher_name: 'Paul Skenes', pitcher_id: '301', team: 'PIT', opponent_team: 'CHC', pitcher_hand: 'R', latest_date: '2026-04-07', }], }; } if (text.includes('FROM public.pitcher_model_snapshots') && text.includes('pitcher_score')) { return { rows: [{ slate_date: '2026-04-07', team: 'PIT', opponent_team: 'CHC', pitcher_name: 'Paul Skenes', p_throws: 'R', pitcher_score: 92.1, strikeout_score: 88.7, pitcher_matchup_adjustment: 4.2, strikeout_matchup_adjustment: 6.1, opponent_lineup_quality: 87.2, opponent_contact_threat: 82.4, opponent_whiff_tendency: 27.8, xwoba: 0.262, csw_pct: 0.311, swstr_pct: 0.178, putaway_pct: 0.274, ball_pct: 0.319, siera: 2.94, gb_pct: 0.44, gb_fb_ratio: 1.23, barrel_bip_pct: 0.051, hard_hit_pct: 0.311, }], }; } if (text.includes('FROM public.live_pitch_mix_2026') && text.includes('GROUP BY point_key')) { return { rows: [ { point_key: '2026-04-01', primary_value: 98.2, overlay_a: 96.4, overlay_b: 15.1 }, { point_key: '2026-04-04', primary_value: 98.8, overlay_a: 96.9, overlay_b: 15.5 }, ], }; } if (text.includes('FROM public.pitcher_arsenal_profiles')) { return { rows: [ { pitch_type: 'FF', usage_rate: 0.42, avg_velocity: 99.1, avg_spin_rate: 2412, avg_extension: 6.7, avg_pfx_x: -0.4, avg_pfx_z: 1.8, avg_spin_axis: 212 }, { pitch_type: 'SL', usage_rate: 0.31, avg_velocity: 87.4, avg_spin_rate: 2699, avg_extension: 6.3, avg_pfx_x: 0.9, avg_pfx_z: -0.2, avg_spin_axis: 101 }, ], }; } if (text.includes('SELECT plate_x, plate_z, zone')) { return { rows: [ { plate_x: -0.3, plate_z: 3.5, zone: 1, pitch_name: 'FF', pitch_type: 'FF', stand: 'L', balls: 0, strikes: 0, description: 'called_strike', events: null, estimated_woba_using_speedangle: null }, { plate_x: 0.0, plate_z: 2.9, zone: 5, pitch_name: 'SL', pitch_type: 'SL', stand: 'R', balls: 1, strikes: 2, description: 'swinging_strike', events: null, estimated_woba_using_speedangle: 0.18 }, { plate_x: 0.2, plate_z: 2.6, zone: 6, pitch_name: 'FF', pitch_type: 'FF', stand: 'R', balls: 1, strikes: 2, description: 'foul_tip', events: null, estimated_woba_using_speedangle: 0.11 }, ], }; } if (text.includes('SELECT pitch_name, balls, strikes, description, stand')) { return { rows: [ { pitch_name: 'FF', balls: 0, strikes: 0, description: 'called_strike', stand: 'L' }, { pitch_name: 'FF', balls: 1, strikes: 2, description: 'swinging_strike', stand: 'R' }, { pitch_name: 'SL', balls: 1, strikes: 2, description: 'swinging_strike', stand: 'R' }, { pitch_name: 'SL', balls: 2, strikes: 1, description: 'ball', stand: 'R' }, ], }; } if (text.includes('SELECT AVG(release_speed) AS avg_release_speed') && text.includes('public.shared_pitcher_baseline_event_rows')) { return { rows: [{ avg_release_speed: 97.9, avg_release_spin_rate: 2368, avg_release_extension: 6.5, avg_pfx_x: -0.3, avg_pfx_z: 1.6, }], }; } if (text.includes('SELECT AVG(release_speed) AS avg_release_speed') && text.includes('public.live_pitch_mix_2026')) { return { rows: [{ avg_release_speed: 98.6, avg_release_spin_rate: 2412, avg_release_extension: 6.7, avg_pfx_x: -0.4, avg_pfx_z: 1.8, }], }; } if (text.includes('SELECT slate_date::date AS point_date, strikeout_score, hard_hit_pct')) { return { rows: [ { point_date: '2026-04-01', strikeout_score: 84.2, hard_hit_pct: 28.1, xwoba: 0.252, pitcher_score: 88.4 }, { point_date: '2026-04-04', strikeout_score: 88.7, hard_hit_pct: 24.9, xwoba: 0.241, pitcher_score: 92.1 }, ], }; } return { rows: [] }; }, async getPlayerContext() { throw new Error('not used'); }, async close() {}, }, logger: { debug() {}, warn() {} }, } ); const [trend, arsenal, location, approach, compare] = await Promise.all([ service.getPitcherTrendChartData({ pitcher: 'paul skenes', view: 'velo', window: 'last_5' }), service.getPitcherArsenalChartData({ pitcher: 'paul skenes', view: 'shape' }), service.getPitcherLocationChartData({ pitcher: 'paul skenes', view: 'miss' }), service.getPitcherApproachChartData({ pitcher: 'paul skenes', view: 'count_usage' }), service.getPitcherCompareChartData({ pitcher: 'paul skenes', view: 'risk_reward', window: 'last_5' }), ]); assert.equal(trend.pitcherName, 'Paul Skenes'); assert.equal(trend.points.length, 2); assert.equal(arsenal.rows[0].label, 'FF'); assert.equal(location.cells.length, 9); assert.ok(approach.datasets.length > 0); assert.equal(compare.chartType, 'scatter'); }); test('matchup embeds render core matchup data clearly', () => { const hittersEmbed = buildMatchupHittersEmbed({ source: 'hosted', resolvedDate: '2026-04-06', rows: [{ hitter_name: 'Aaron Judge', team: 'NYY', matchup_score: 85.1, ceiling_score: 90.2, zone_fit_score: 78.4, likely_starter_score: 99.0, xwoba: 0.455, hard_hit_pct: 57.2, opponent_team: 'BOS', opposing_pitcher_name: 'Tanner Houck', opposing_pitcher_hand: 'R', }], }); const playerEmbed = buildPlayerContextEmbed({ source: 'hosted', resolvedDate: '2026-04-06', playerType: 'pitcher', name: 'Paul Skenes', team: 'PIT', metrics: [ { label: 'Pitch Score', value: 92.2 }, { label: 'Strikeout', value: 89.5 }, ], rolling: [{ label: 'Rolling 5', value: 0.245 }], zones: [{ label: 'Up-In', metricKey: 'xwoba_allowed', value: 0.198, sample: 22 }], arsenal: [{ pitchType: 'Four-Seam', usagePct: 41.2, velocity: 99.1, whiffRate: 31.4 }], countUsage: [{ countBucket: 'Putaway', batterSide: 'R', pitchType: 'Slider', usagePct: 38.2 }], }); assert.equal(hittersEmbed.data.title, 'Matchup Hitters'); assert.match(hittersEmbed.data.fields[0].name, /Aaron Judge/); assert.equal(playerEmbed.data.title, 'Paul Skenes - Pitcher'); assert.ok(playerEmbed.data.fields.some((field) => field.name === 'Arsenal')); }); test('best matchups embed renders expanded hosted matchup metrics', () => { const embed = buildBestMatchupsEmbed({ source: 'hosted', resolvedDate: '2026-04-07', rows: [{ hitter_name: 'Ben Rice', team: 'NYY', matchup_score: 70.3, ceiling_score: 81.0, zone_fit_score: 63.4, barrel_bip_pct: 17.4, pulled_barrel_pct: 11.2, fb_pct: 31.6, sweet_spot_pct: 42.7, avg_launch_angle: 16.7, xwoba: 0.397, }], }, { team: 'Yankees' }); assert.equal(embed.data.title, 'Best Matchups'); assert.match(embed.data.fields[0].value, /Zone Fit 63\.400/); assert.match(embed.data.fields[0].value, /Barrel 17\.4%/); assert.match(embed.data.fields[0].value, /PulledBarrel 11\.2%/); assert.match(embed.data.fields[0].value, /Launch Angle 16\.7/); }); test('best matchups embed supports a custom title for team boards', () => { const embed = buildBestMatchupsEmbed({ source: 'hosted', resolvedDate: '2026-04-07', rows: [{ hitter_name: 'Kyle Schwarber', team: 'PHI', matchup_score: 64.6, ceiling_score: 98.6, zone_fit_score: 0.1, barrel_bip_pct: 20.3, pulled_barrel_pct: 10.2, fb_pct: 28.3, sweet_spot_pct: 36.0, avg_launch_angle: 17.7, xwoba: 0.371, }], }, { team: 'Phillies' }, { title: 'Team Best Matchups' }); assert.equal(embed.data.title, 'Team Best Matchups'); }); test('baseball chart embeds render chart-first summaries', () => { const hrEmbed = buildHrProfileEmbed({ source: 'hosted', resolvedDate: '2026-04-07', playerName: 'Yordan Alvarez', team: 'HOU', opposingPitcherName: 'Bryan Woo', opposingPitcherHand: 'R', zone_fit_score: 0.1234, matchup_score: 79.8, ceiling_score: 89.8, }, 'hr-profile-radar.png'); const kEmbed = buildKProfileEmbed({ source: 'hosted', resolvedDate: '2026-04-07', pitcherName: 'Tarik Skubal', team: 'DET', opponentTeam: 'MIN', pitcher_score: 86.2, strikeout_score: 84.4, strikeout_matchup_adjustment: 7.1, }, 'k-profile-radar.png'); assert.equal(hrEmbed.data.image.url, 'attachment://hr-profile-radar.png'); assert.match(hrEmbed.data.description, /Zone Fit 0\.123/i); assert.equal(kEmbed.data.image.url, 'attachment://k-profile-radar.png'); assert.match(kEmbed.data.description, /Pitch Score 86\.2/i); }); test('hosted hitter parity cache reuses the same computed slate across team queries', async () => { let dailyHitterReads = 0; const responses = new Map([ ['https://example.test/daily/2026-04-07/slate.parquet', [ { game_pk: 20, away_team: 'HOU', home_team: 'SEA', away_probable_pitcher_id: 501, home_probable_pitcher_id: 601, away_probable_pitcher: 'Pitcher A', home_probable_pitcher: 'Pitcher B', away_probable_hand: 'R', home_probable_hand: 'L', }, ]], ['https://example.test/daily/2026-04-07/daily_hitter_metrics.parquet', [ { game_pk: 20, team: 'HOU', opponent_team: 'SEA', batter: 1, hitter_name: 'Yordan Alvarez', stand: 'L', split_key: 'vs_lhp', recent_window: 'season', weighted_mode: 'weighted', matchup_score: 70, ceiling_score: 80, zone_fit_score: 0.1234, likely_starter_score: 99, xwoba: 0.4, swstr_pct: 0.1, barrel_bbe_pct: 0.1, fb_pct: 0.2, pulled_barrel_pct: 0.1, sweet_spot_pct: 0.3, barrel_bip_pct: 0.15, hard_hit_pct: 0.45, avg_launch_angle: 15 }, { game_pk: 20, team: 'HOU', opponent_team: 'SEA', batter: 2, hitter_name: 'Jose Altuve', stand: 'R', split_key: 'vs_lhp', recent_window: 'season', weighted_mode: 'weighted', matchup_score: 68, ceiling_score: 78, zone_fit_score: 0.1111, likely_starter_score: 98, xwoba: 0.38, swstr_pct: 0.1, barrel_bbe_pct: 0.08, fb_pct: 0.2, pulled_barrel_pct: 0.08, sweet_spot_pct: 0.3, barrel_bip_pct: 0.1, hard_hit_pct: 0.4, avg_launch_angle: 12 }, { game_pk: 20, team: 'HOU', opponent_team: 'SEA', batter: 3, hitter_name: 'Kyle Tucker', stand: 'L', split_key: 'vs_lhp', recent_window: 'season', weighted_mode: 'weighted', matchup_score: 66, ceiling_score: 76, zone_fit_score: 0.2222, likely_starter_score: 97, xwoba: 0.37, swstr_pct: 0.1, barrel_bbe_pct: 0.08, fb_pct: 0.2, pulled_barrel_pct: 0.08, sweet_spot_pct: 0.3, barrel_bip_pct: 0.1, hard_hit_pct: 0.4, avg_launch_angle: 13 }, ]], ['https://example.test/daily/2026-04-07/daily_pitcher_metrics.parquet', [ { pitcher_id: 501, p_throws: 'R', split_key: 'vs_lhp', recent_window: 'season', weighted_mode: 'weighted' }, { pitcher_id: 601, p_throws: 'L', split_key: 'vs_lhp', recent_window: 'season', weighted_mode: 'weighted' }, ]], ['https://example.test/daily/2026-04-07/hitter_pitcher_exclusions.parquet', []], ['https://example.test/daily/2026-04-07/rosters.parquet', [ { team: 'HOU', player_id: 1, player_name: 'Yordan Alvarez' }, { team: 'HOU', player_id: 2, player_name: 'Jose Altuve' }, { team: 'HOU', player_id: 3, player_name: 'Kyle Tucker' }, ]], ['https://example.test/daily/2026-04-07/daily_batter_zone_profiles.parquet', []], ['https://example.test/daily/2026-04-07/daily_pitcher_zone_profiles.parquet', []], ]); const source = new HostedArtifactSource( { baseUrl: 'https://example.test', cacheTtlMs: 60_000, fallbackDays: 0, }, { readParquetImpl: async (url) => { if (url.endsWith('/daily_hitter_metrics.parquet')) { dailyHitterReads += 1; } if (!responses.has(url)) { throw new Error(`Missing ${url}`); } return responses.get(url); }, fetchTextImpl: async () => '', logger: { debug() {}, warn() {} }, } ); await source.getTeamBestMatchups({ date: '2026-04-07', team: 'Astros' }); await source.getBestMatchups({ date: '2026-04-07', team: 'Astros' }); assert.equal(dailyHitterReads, 1); }); test('hosted team best matchups returns the top three hitters for the selected team', async () => { const responses = new Map([ ['https://example.test/daily/2026-04-07/slate.parquet', [ { game_pk: 20, away_team: 'BOS', home_team: 'PHI', away_probable_pitcher_id: 501, home_probable_pitcher_id: 601, away_probable_pitcher: 'Garrett Crochet', home_probable_pitcher: 'Zack Wheeler', away_probable_hand: 'L', home_probable_hand: 'R', }, ]], ['https://example.test/daily/2026-04-07/daily_hitter_metrics.parquet', [ { team: 'PHI', hitter_name: '1', batter: 1, stand: 'L', split_key: 'vs_lhp', recent_window: 'season', weighted_mode: 'weighted', xwoba: 0.371, swstr_pct: 0.09, barrel_bbe_pct: 0.18, barrel_bip_pct: 0.203, pulled_barrel_pct: 0.102, sweet_spot_pct: 0.36, fb_pct: 0.283, hard_hit_pct: 0.51, avg_launch_angle: 17.7, likely_starter_score: 95 }, { team: 'PHI', hitter_name: '2', batter: 2, stand: 'R', split_key: 'vs_lhp', recent_window: 'season', weighted_mode: 'weighted', xwoba: 0.355, swstr_pct: 0.10, barrel_bbe_pct: 0.14, barrel_bip_pct: 0.150, pulled_barrel_pct: 0.081, sweet_spot_pct: 0.34, fb_pct: 0.260, hard_hit_pct: 0.46, avg_launch_angle: 16.2, likely_starter_score: 94 }, { team: 'PHI', hitter_name: '3', batter: 3, stand: 'L', split_key: 'vs_lhp', recent_window: 'season', weighted_mode: 'weighted', xwoba: 0.344, swstr_pct: 0.11, barrel_bbe_pct: 0.12, barrel_bip_pct: 0.131, pulled_barrel_pct: 0.062, sweet_spot_pct: 0.33, fb_pct: 0.240, hard_hit_pct: 0.43, avg_launch_angle: 15.1, likely_starter_score: 93 }, { team: 'PHI', hitter_name: '4', batter: 4, stand: 'R', split_key: 'vs_lhp', recent_window: 'season', weighted_mode: 'weighted', xwoba: 0.320, swstr_pct: 0.12, barrel_bbe_pct: 0.10, barrel_bip_pct: 0.101, pulled_barrel_pct: 0.041, sweet_spot_pct: 0.31, fb_pct: 0.220, hard_hit_pct: 0.39, avg_launch_angle: 13.8, likely_starter_score: 92 }, { team: 'BOS', hitter_name: '10', batter: 10, stand: 'L', split_key: 'vs_rhp', recent_window: 'season', weighted_mode: 'weighted', xwoba: 0.390, swstr_pct: 0.08, barrel_bbe_pct: 0.19, barrel_bip_pct: 0.18, pulled_barrel_pct: 0.11, sweet_spot_pct: 0.37, fb_pct: 0.29, hard_hit_pct: 0.50, avg_launch_angle: 18.0, likely_starter_score: 95 }, ]], ['https://example.test/daily/2026-04-07/daily_pitcher_metrics.parquet', [ { pitcher_id: 501, pitcher_name: 'Garrett Crochet', p_throws: 'L', split_key: 'overall', recent_window: 'season', weighted_mode: 'weighted' }, { pitcher_id: 601, pitcher_name: 'Zack Wheeler', p_throws: 'R', split_key: 'overall', recent_window: 'season', weighted_mode: 'weighted' }, ]], ['https://example.test/daily/2026-04-07/hitter_pitcher_exclusions.parquet', []], ['https://example.test/daily/2026-04-07/rosters.parquet', [ { team: 'PHI', player_id: 1, player_name: 'Kyle Schwarber' }, { team: 'PHI', player_id: 2, player_name: 'Trea Turner' }, { team: 'PHI', player_id: 3, player_name: 'Bryce Harper' }, { team: 'PHI', player_id: 4, player_name: 'Nick Castellanos' }, { team: 'BOS', player_id: 10, player_name: 'Jarren Duran' }, ]], ['https://example.test/daily/2026-04-07/daily_batter_zone_profiles.parquet', []], ['https://example.test/daily/2026-04-07/daily_pitcher_zone_profiles.parquet', []], ]); const source = new HostedArtifactSource( { baseUrl: 'https://example.test', cacheTtlMs: 60_000, fallbackDays: 0, }, { readParquetImpl: async (url) => { if (!responses.has(url)) { throw new Error(`Missing ${url}`); } return responses.get(url); }, fetchTextImpl: async () => '', logger: { debug() {}, warn() {} }, } ); const result = await source.getTeamBestMatchups({ date: '2026-04-07', team: 'Phillies' }); assert.equal(result.rows.length, 3); assert.deepEqual(result.rows.map((row) => row.hitter_name), ['Kyle Schwarber', 'Trea Turner', 'Bryce Harper']); }); test('cockroach snapshot lookup normalizes team aliases for filters', async () => { let receivedValues = null; const source = new CockroachMatchupSource('postgresql://test', { pool: { async query(_text, values) { receivedValues = values; return { rows: [] }; }, async end() {}, }, logger: { warn() {} }, }); await source.getHitterSnapshotRows({ date: '2026-04-07', team: 'Red Sox' }); assert.equal(receivedValues[1], 'BOS'); });