ROIBot / test /matchups.test.js
Codex
Add full pitcher chart suite
d2a67b5
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');
});