Files
vyndr/tests/unit/shipDataSources.test.js
T

396 lines
14 KiB
JavaScript

/**
* 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);
});
});