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