Sessions 5-7a: 955 tests, deployment ready
This commit is contained in:
@@ -0,0 +1,395 @@
|
||||
/**
|
||||
* VYNDR Ship Build — Data Source Component Tests
|
||||
* Pure logic tests with inline data shapes and parsing logic.
|
||||
*/
|
||||
|
||||
// ─── Inline Data Shapes ───────────────────────────────────────────────
|
||||
|
||||
const STARTING_TRUST = {
|
||||
beat_writer: 'reliable',
|
||||
national: 'authoritative',
|
||||
aggregator: 'unverified',
|
||||
insider: 'reliable',
|
||||
};
|
||||
|
||||
const REPORTER_DATABASE = {
|
||||
nba: {
|
||||
shams_charania: { name: 'Shams Charania', source_type: 'insider', trust: 'reliable' },
|
||||
woj: { name: 'Adrian Wojnarowski', source_type: 'national', trust: 'authoritative' },
|
||||
local_beat: { name: 'Local Beat Writer', source_type: 'beat_writer', trust: 'reliable' },
|
||||
},
|
||||
};
|
||||
|
||||
const TRUST_LEVELS = ['unverified', 'reliable', 'verified', 'authoritative'];
|
||||
|
||||
function escalateTrust(reporter) {
|
||||
const { tracked, accuracy } = reporter;
|
||||
if (tracked >= 30 && accuracy >= 0.95) return { level: 'authoritative', badge: 'confirmed' };
|
||||
if (tracked >= 20 && accuracy >= 0.90) return { level: 'verified', badge: 'trusted' };
|
||||
return { level: reporter.trust, badge: null };
|
||||
}
|
||||
|
||||
// ─── Tweet Parsing ────────────────────────────────────────────────────
|
||||
|
||||
const STATUS_PATTERNS = [
|
||||
{ pattern: /will start/i, status: 'confirmed_playing', confidence: 0.85 },
|
||||
{ pattern: /scratched/i, status: 'scratched', confidence: 0.90 },
|
||||
{ pattern: /game[- ]time decision/i, status: 'questionable', confidence: 0.70 },
|
||||
{ pattern: /\bOUT\b/, status: 'out', confidence: 0.90 },
|
||||
];
|
||||
|
||||
const PAST_TENSE_FILTERS = [/was out/i, /yesterday/i, /last night/i];
|
||||
|
||||
function parseTweet(text) {
|
||||
if (!text) return null;
|
||||
for (const filter of PAST_TENSE_FILTERS) {
|
||||
if (filter.test(text)) return null;
|
||||
}
|
||||
for (const { pattern, status, confidence } of STATUS_PATTERNS) {
|
||||
if (pattern.test(text)) return { status, confidence };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// ─── Odds API Parsing ─────────────────────────────────────────────────
|
||||
|
||||
function parseOddsOutcome(market, bookmakerKey) {
|
||||
if (!market || !market.outcomes || market.outcomes.length === 0) {
|
||||
return null;
|
||||
}
|
||||
const outcome = market.outcomes[0];
|
||||
return {
|
||||
player_name: outcome.name || null,
|
||||
line: outcome.point != null ? outcome.point : null,
|
||||
bookmaker: bookmakerKey,
|
||||
};
|
||||
}
|
||||
|
||||
// ─── Line Movement Detection ──────────────────────────────────────────
|
||||
|
||||
function detectLineMovement(opening, current) {
|
||||
const diff = Math.abs(current - opening);
|
||||
if (diff < 0.5) return null;
|
||||
return {
|
||||
movement: diff,
|
||||
direction: current > opening ? 'up' : 'down',
|
||||
flagged: true,
|
||||
};
|
||||
}
|
||||
|
||||
// ─── Weather + Dome Detection ─────────────────────────────────────────
|
||||
|
||||
const DOME_PARKS = ['tropicana_field', 'chase_field', 'minute_maid', 'rogers_centre', 'loanDepot_park', 'globe_life'];
|
||||
|
||||
function getWeatherForPark(park, conditions) {
|
||||
if (DOME_PARKS.includes(park)) {
|
||||
return { temperature: 72, wind: 0, humidity: 50, ball_carry_factor: 1.0 };
|
||||
}
|
||||
return conditions;
|
||||
}
|
||||
|
||||
function ballCarryFactor(temperature, humidity) {
|
||||
let factor = 1.0;
|
||||
if (temperature > 72) factor += (temperature - 72) * 0.003;
|
||||
if (temperature < 72) factor -= (72 - temperature) * 0.003;
|
||||
if (humidity > 50) factor -= (humidity - 50) * 0.002;
|
||||
if (humidity < 50) factor += (50 - humidity) * 0.002;
|
||||
return parseFloat(factor.toFixed(4));
|
||||
}
|
||||
|
||||
// ─── Catcher Framing ──────────────────────────────────────────────────
|
||||
|
||||
function clampFramingValue(raw) {
|
||||
return Math.max(-0.5, Math.min(0.5, raw));
|
||||
}
|
||||
|
||||
// ─── Umpire / Referee Minimums ────────────────────────────────────────
|
||||
|
||||
function getUmpireAdjustment(umpire) {
|
||||
if (umpire.games < 30) return 0.0;
|
||||
return umpire.k_rate_delta;
|
||||
}
|
||||
|
||||
function getRefereeAdjustment(referee) {
|
||||
if (referee.games < 30) return 0.0;
|
||||
return referee.foul_rate_delta;
|
||||
}
|
||||
|
||||
// ─── MLB Lineup Parsing ──────────────────────────────────────────────
|
||||
|
||||
function parseLineupEntry(raw) {
|
||||
return {
|
||||
player: raw.player,
|
||||
batting_order: raw.batting_order,
|
||||
position: raw.position,
|
||||
status: raw.source === 'official_api' ? 'confirmed' : 'projected',
|
||||
};
|
||||
}
|
||||
|
||||
// ─── ABS Challenge System ─────────────────────────────────────────────
|
||||
|
||||
function disciplineScore(chase_rate, bb_rate) {
|
||||
// chase_rate: 0-1 (lower = better), bb_rate: 0-1 (higher = better)
|
||||
const raw = (1 - chase_rate) * 0.5 + bb_rate * 0.5;
|
||||
return Math.max(0, Math.min(1, parseFloat(raw.toFixed(4))));
|
||||
}
|
||||
|
||||
function absKAdjustment(discipline) {
|
||||
if (discipline > 0.7) return -0.05;
|
||||
return 0;
|
||||
}
|
||||
|
||||
function framingVsDisciplined(framingValue, discipline) {
|
||||
// Framing is 50% less effective against disciplined batters
|
||||
if (discipline > 0.7) return framingValue * 0.5;
|
||||
return framingValue;
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
// TESTS
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
|
||||
describe('Reporter seeding + trust', () => {
|
||||
test('beat_writer starts at reliable', () => {
|
||||
expect(STARTING_TRUST.beat_writer).toBe('reliable');
|
||||
});
|
||||
|
||||
test('national starts at authoritative', () => {
|
||||
expect(STARTING_TRUST.national).toBe('authoritative');
|
||||
});
|
||||
|
||||
test('aggregator starts at unverified', () => {
|
||||
expect(STARTING_TRUST.aggregator).toBe('unverified');
|
||||
});
|
||||
|
||||
test('insider starts at reliable', () => {
|
||||
expect(STARTING_TRUST.insider).toBe('reliable');
|
||||
});
|
||||
|
||||
test('reporter_database has nba key', () => {
|
||||
expect(REPORTER_DATABASE).toHaveProperty('nba');
|
||||
expect(Object.keys(REPORTER_DATABASE.nba).length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test('reporter has source_type field', () => {
|
||||
const reporter = REPORTER_DATABASE.nba.shams_charania;
|
||||
expect(reporter).toHaveProperty('source_type');
|
||||
expect(typeof reporter.source_type).toBe('string');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Reporter trust escalation', () => {
|
||||
test('promote from reliable to verified at 20+ tracked and 90%+ accuracy', () => {
|
||||
const result = escalateTrust({ trust: 'reliable', tracked: 25, accuracy: 0.92 });
|
||||
expect(result.level).toBe('verified');
|
||||
});
|
||||
|
||||
test('stay at reliable if accuracy below 90%', () => {
|
||||
const result = escalateTrust({ trust: 'reliable', tracked: 25, accuracy: 0.85 });
|
||||
expect(result.level).toBe('reliable');
|
||||
});
|
||||
|
||||
test('authoritative requires 30+ tracked and 95%+ accuracy', () => {
|
||||
const result = escalateTrust({ trust: 'verified', tracked: 35, accuracy: 0.96 });
|
||||
expect(result.level).toBe('authoritative');
|
||||
});
|
||||
|
||||
test('badge for authoritative is confirmed', () => {
|
||||
const result = escalateTrust({ trust: 'verified', tracked: 35, accuracy: 0.96 });
|
||||
expect(result.badge).toBe('confirmed');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Tweet parsing', () => {
|
||||
test('"will start" detected as confirmed_playing', () => {
|
||||
const result = parseTweet('LeBron James will start tonight');
|
||||
expect(result.status).toBe('confirmed_playing');
|
||||
});
|
||||
|
||||
test('"scratched" detected as scratched', () => {
|
||||
const result = parseTweet('Giannis has been scratched from the lineup');
|
||||
expect(result.status).toBe('scratched');
|
||||
});
|
||||
|
||||
test('"game-time decision" detected as questionable', () => {
|
||||
const result = parseTweet('Jaylen Brown is a game-time decision');
|
||||
expect(result.status).toBe('questionable');
|
||||
});
|
||||
|
||||
test('past tense "was out" filtered', () => {
|
||||
const result = parseTweet('Curry was out for the game last week');
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
test('"yesterday" filtered', () => {
|
||||
const result = parseTweet('Player was scratched yesterday');
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
test('"last night" filtered', () => {
|
||||
const result = parseTweet('He sat out last night');
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
test('returns null for irrelevant text', () => {
|
||||
const result = parseTweet('Great weather in Boston today');
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
test('player OUT returns confidence 0.90', () => {
|
||||
const result = parseTweet('Player is OUT tonight');
|
||||
expect(result.confidence).toBe(0.90);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Odds API response parsing', () => {
|
||||
test('extracts player_name from outcome', () => {
|
||||
const market = { outcomes: [{ name: 'LeBron James', point: 25.5 }] };
|
||||
const result = parseOddsOutcome(market, 'draftkings');
|
||||
expect(result.player_name).toBe('LeBron James');
|
||||
});
|
||||
|
||||
test('extracts line from point', () => {
|
||||
const market = { outcomes: [{ name: 'LeBron James', point: 25.5 }] };
|
||||
const result = parseOddsOutcome(market, 'draftkings');
|
||||
expect(result.line).toBe(25.5);
|
||||
});
|
||||
|
||||
test('extracts bookmaker key', () => {
|
||||
const market = { outcomes: [{ name: 'LeBron James', point: 25.5 }] };
|
||||
const result = parseOddsOutcome(market, 'fanduel');
|
||||
expect(result.bookmaker).toBe('fanduel');
|
||||
});
|
||||
|
||||
test('handles missing outcomes gracefully', () => {
|
||||
expect(parseOddsOutcome({}, 'draftkings')).toBeNull();
|
||||
expect(parseOddsOutcome({ outcomes: [] }, 'draftkings')).toBeNull();
|
||||
expect(parseOddsOutcome(null, 'draftkings')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Line movement detection', () => {
|
||||
test('flags movement >= 0.5', () => {
|
||||
const result = detectLineMovement(24.5, 25.5);
|
||||
expect(result.flagged).toBe(true);
|
||||
expect(result.movement).toBe(1.0);
|
||||
});
|
||||
|
||||
test('ignores movement < 0.5', () => {
|
||||
const result = detectLineMovement(24.5, 24.8);
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
test('direction is up when current > opening', () => {
|
||||
const result = detectLineMovement(24.5, 25.5);
|
||||
expect(result.direction).toBe('up');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Weather dome detection', () => {
|
||||
test('dome parks return temperature=72', () => {
|
||||
const weather = getWeatherForPark('tropicana_field', {});
|
||||
expect(weather.temperature).toBe(72);
|
||||
});
|
||||
|
||||
test('dome parks return wind=0', () => {
|
||||
const weather = getWeatherForPark('chase_field', {});
|
||||
expect(weather.wind).toBe(0);
|
||||
});
|
||||
|
||||
test('ball_carry_factor for dome is 1.0', () => {
|
||||
const weather = getWeatherForPark('minute_maid', {});
|
||||
expect(weather.ball_carry_factor).toBe(1.0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Weather ball carry', () => {
|
||||
test('hot weather increases carry (>72F)', () => {
|
||||
const factor = ballCarryFactor(90, 50);
|
||||
expect(factor).toBeGreaterThan(1.0);
|
||||
});
|
||||
|
||||
test('humid weather decreases carry (>50%)', () => {
|
||||
const factor = ballCarryFactor(72, 80);
|
||||
expect(factor).toBeLessThan(1.0);
|
||||
});
|
||||
|
||||
test('neutral at 72F/50%', () => {
|
||||
const factor = ballCarryFactor(72, 50);
|
||||
expect(factor).toBe(1.0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Catcher framing', () => {
|
||||
test('framing value clamped at upper bound 0.5', () => {
|
||||
expect(clampFramingValue(0.8)).toBe(0.5);
|
||||
});
|
||||
|
||||
test('framing value clamped at lower bound -0.5', () => {
|
||||
expect(clampFramingValue(-0.9)).toBe(-0.5);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Umpire / referee minimums', () => {
|
||||
test('umpire returns 0.0 below 30 games', () => {
|
||||
expect(getUmpireAdjustment({ games: 15, k_rate_delta: 0.12 })).toBe(0.0);
|
||||
});
|
||||
|
||||
test('referee returns 0.0 below 30 games', () => {
|
||||
expect(getRefereeAdjustment({ games: 20, foul_rate_delta: 0.08 })).toBe(0.0);
|
||||
});
|
||||
|
||||
test('umpire returns adjustment at 30+ games', () => {
|
||||
const adj = getUmpireAdjustment({ games: 45, k_rate_delta: 0.12 });
|
||||
expect(adj).toBe(0.12);
|
||||
});
|
||||
|
||||
test('referee returns adjustment at 30+ games', () => {
|
||||
const adj = getRefereeAdjustment({ games: 30, foul_rate_delta: 0.08 });
|
||||
expect(adj).toBe(0.08);
|
||||
});
|
||||
});
|
||||
|
||||
describe('MLB lineup parsing', () => {
|
||||
test('lineup has batting_order field', () => {
|
||||
const entry = parseLineupEntry({ player: 'Mookie Betts', batting_order: 1, position: 'RF', source: 'official_api' });
|
||||
expect(entry).toHaveProperty('batting_order');
|
||||
expect(entry.batting_order).toBe(1);
|
||||
});
|
||||
|
||||
test('lineup has position field', () => {
|
||||
const entry = parseLineupEntry({ player: 'Freddie Freeman', batting_order: 3, position: '1B', source: 'official_api' });
|
||||
expect(entry).toHaveProperty('position');
|
||||
expect(entry.position).toBe('1B');
|
||||
});
|
||||
|
||||
test('status is confirmed from official API', () => {
|
||||
const entry = parseLineupEntry({ player: 'Shohei Ohtani', batting_order: 2, position: 'DH', source: 'official_api' });
|
||||
expect(entry.status).toBe('confirmed');
|
||||
});
|
||||
});
|
||||
|
||||
describe('ABS challenge system', () => {
|
||||
test('discipline_score from chase_rate + bb_rate is 0-1', () => {
|
||||
const score = disciplineScore(0.3, 0.12);
|
||||
expect(score).toBeGreaterThanOrEqual(0);
|
||||
expect(score).toBeLessThanOrEqual(1);
|
||||
});
|
||||
|
||||
test('elite discipline (>0.7) gets -5% K adjustment', () => {
|
||||
const adj = absKAdjustment(0.8);
|
||||
expect(adj).toBe(-0.05);
|
||||
});
|
||||
|
||||
test('low discipline gets no benefit', () => {
|
||||
const adj = absKAdjustment(0.4);
|
||||
expect(adj).toBe(0);
|
||||
});
|
||||
|
||||
test('framing 50% effective vs disciplined batter', () => {
|
||||
const full = framingVsDisciplined(0.3, 0.5);
|
||||
const reduced = framingVsDisciplined(0.3, 0.8);
|
||||
expect(full).toBe(0.3);
|
||||
expect(reduced).toBe(0.15);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user