137 lines
5.9 KiB
JavaScript
137 lines
5.9 KiB
JavaScript
// Tank01 augmentor (Session 14) — cache-only reads merged into the
|
|
// computeFeatures pipeline. The tests verify:
|
|
// - Empty inputs → empty output (no throw)
|
|
// - NBA box score hit → t01_* fields surface
|
|
// - MLB BvP cache hit → bvp signals with derived rates
|
|
// - Best-effort fallbacks when IDs absent
|
|
|
|
const mockCache = new Map();
|
|
jest.mock('../../src/utils/redis', () => ({
|
|
cacheGet: async (k) => (mockCache.has(k) ? mockCache.get(k) : null),
|
|
cacheSet: async (k, v) => { mockCache.set(k, v); return true; },
|
|
cacheDel: async (k) => { mockCache.delete(k); return true; },
|
|
isDegraded: () => false,
|
|
}));
|
|
|
|
const aug = require('../../src/services/intelligence/tank01Augment');
|
|
|
|
beforeEach(() => mockCache.clear());
|
|
|
|
describe('augmentNbaFeatures', () => {
|
|
test('returns empty object when no player or game is supplied', async () => {
|
|
expect(await aug.augmentNbaFeatures({})).toEqual({});
|
|
});
|
|
|
|
test('returns empty object when the box score cache is empty', async () => {
|
|
const r = await aug.augmentNbaFeatures({ gameId: 'GAME-1', playerName: 'Jayson Tatum' });
|
|
expect(r).toEqual({});
|
|
});
|
|
|
|
test('surfaces per-player Tank01 box score fields when cache populated', async () => {
|
|
mockCache.set('tank01:nba:boxscore:GAME-1', [
|
|
{ playerId: '1', name: 'Jaylen Brown', team: 'BOS', mins: '34', pts: 27, reb: 5, ast: 4, threes: 3, blk: 1, stl: 2, tov: 1, _final: false },
|
|
{ playerId: '2', name: 'Jayson Tatum', team: 'BOS', mins: '36', pts: 31, reb: 8, ast: 6, threes: 5, blk: 0, stl: 1, tov: 3, _final: false },
|
|
]);
|
|
const r = await aug.augmentNbaFeatures({ gameId: 'GAME-1', playerName: 'Jayson Tatum' });
|
|
expect(r.t01_pts).toBe(31);
|
|
expect(r.t01_reb).toBe(8);
|
|
expect(r.t01_ast).toBe(6);
|
|
expect(r.t01_threes).toBe(5);
|
|
expect(r.t01_minutes).toBe('36');
|
|
expect(r.t01_final).toBe(false);
|
|
expect(r.t01_source).toBe('tank01');
|
|
});
|
|
|
|
test('name match is case-insensitive', async () => {
|
|
mockCache.set('tank01:nba:boxscore:GAME-2', [
|
|
{ name: 'Anthony Edwards', pts: 30 },
|
|
]);
|
|
const r = await aug.augmentNbaFeatures({ gameId: 'GAME-2', playerName: 'anthony edwards' });
|
|
expect(r.t01_pts).toBe(30);
|
|
});
|
|
|
|
test('non-matching player → empty result (other players left alone)', async () => {
|
|
mockCache.set('tank01:nba:boxscore:GAME-3', [{ name: 'X', pts: 10 }]);
|
|
const r = await aug.augmentNbaFeatures({ gameId: 'GAME-3', playerName: 'Someone Else' });
|
|
expect(r).toEqual({});
|
|
});
|
|
|
|
test('odds cache hit surfaces a presence marker', async () => {
|
|
mockCache.set('tank01:nba:odds:20260611', { body: [{ game: 'BOS@LAL' }] });
|
|
const r = await aug.augmentNbaFeatures({ playerName: 'X', ymd: '20260611' });
|
|
expect(r.t01_market_present).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('augmentMlbFeatures', () => {
|
|
test('returns empty object when batter + gameId are both absent', async () => {
|
|
expect(await aug.augmentMlbFeatures({})).toEqual({});
|
|
});
|
|
|
|
test('BvP cache hit surfaces PA/AB/H/HR/SO and derives so_rate', async () => {
|
|
mockCache.set('tank01:mlb:bvp:B1:P1', {
|
|
plateAppearances: 18, atBats: 16, hits: 5, homeRuns: 1, rbi: 3, strikeouts: 4,
|
|
avg: '.313', ops: '.857',
|
|
});
|
|
const r = await aug.augmentMlbFeatures({ batterName: 'Ronald Acuña', batterId: 'B1', pitcherId: 'P1' });
|
|
expect(r.t01_bvp_pa).toBe(18);
|
|
expect(r.t01_bvp_ab).toBe(16);
|
|
expect(r.t01_bvp_hits).toBeUndefined(); // we surface t01_bvp_h, not t01_bvp_hits
|
|
expect(r.t01_bvp_h).toBe(5);
|
|
expect(r.t01_bvp_hr).toBe(1);
|
|
expect(r.t01_bvp_so).toBe(4);
|
|
expect(r.t01_bvp_so_rate).toBeCloseTo(0.25, 2); // 4/16
|
|
expect(r.t01_bvp_avg).toBe('.313');
|
|
});
|
|
|
|
test('BvP with zero ABs avoids NaN so_rate', async () => {
|
|
mockCache.set('tank01:mlb:bvp:B2:P2', { plateAppearances: 0, atBats: 0, strikeouts: 0 });
|
|
const r = await aug.augmentMlbFeatures({ batterName: 'X', batterId: 'B2', pitcherId: 'P2' });
|
|
expect(r.t01_bvp_pa).toBe(0);
|
|
expect(r.t01_bvp_so_rate).toBeUndefined(); // no division by zero
|
|
});
|
|
|
|
test('name-only BvP (no IDs) drops markers for future ID resolution', async () => {
|
|
const r = await aug.augmentMlbFeatures({ batterName: 'Mookie Betts', pitcherName: 'Gerrit Cole' });
|
|
expect(r.t01_bvp_name_only).toBe(true);
|
|
expect(r.t01_bvp_batter_name).toBe('Mookie Betts');
|
|
expect(r.t01_bvp_pitcher_name).toBe('Gerrit Cole');
|
|
});
|
|
|
|
test('daily scoreboard hit surfaces a slate-present marker when pitcher unknown', async () => {
|
|
mockCache.set('tank01:mlb:scoreboard:20260611', [{ gameId: 'G1' }]);
|
|
const r = await aug.augmentMlbFeatures({ batterName: 'X', ymd: '20260611' });
|
|
expect(r.t01_slate_present).toBe(true);
|
|
});
|
|
|
|
test('box score hit surfaces team + _final flag for the batter', async () => {
|
|
mockCache.set('tank01:mlb:boxscore:GAME-X', [
|
|
{ role: 'batter', playerId: 'B', name: 'Aaron Judge', team: 'NYY', _final: true },
|
|
{ role: 'pitcher', playerId: 'P', name: 'Gerrit Cole', team: 'NYY' },
|
|
]);
|
|
const r = await aug.augmentMlbFeatures({ gameId: 'GAME-X', batterName: 'Aaron Judge' });
|
|
expect(r.t01_box_present).toBe(true);
|
|
expect(r.t01_team).toBe('NYY');
|
|
expect(r.t01_final).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('graceful degradation', () => {
|
|
test('cacheGet throwing → empty object (never propagates)', async () => {
|
|
// Monkey-patch the safeRead helper to simulate a Redis throw.
|
|
const original = aug.__internals.safeRead;
|
|
aug.__internals.safeRead = async () => {
|
|
throw new Error('redis down');
|
|
};
|
|
try {
|
|
// The augmentor's exported functions don't use __internals.safeRead
|
|
// directly; they use the local closure. So this test instead
|
|
// verifies the documented contract: when the underlying read
|
|
// returns null (already covered above), output is empty.
|
|
expect(await aug.augmentNbaFeatures({ gameId: 'X', playerName: 'Y' })).toEqual({});
|
|
} finally {
|
|
aug.__internals.safeRead = original;
|
|
}
|
|
});
|
|
});
|