'use strict'; /** * featureCoverage — Step 0 input check (2026-08-01). * * The property worth locking is the one whose absence produced a false * all-zero reading on the first run: a coverage probe must count a feature as * populated ONLY when it is a real finite number, and it must read features * from the grader's own return shape. A probe that makes working code look * broken is more dangerous than no probe. */ const { coverage, __internals } = require('../../src/services/featureCoverage'); const { populated } = __internals; const isModelBook = (b) => b === 'draftkings'; const prop = (player, stat) => ({ player, stat_type: stat, line: 1.5, book: 'draftkings', }); describe('populated — strict, never 0-coercing', () => { it('treats null / undefined / empty string as ABSENT, not zero', () => { for (const v of [null, undefined, '', NaN, 'abc']) expect(populated(v)).toBe(false); }); it('treats a real zero as PRESENT (0 rest days is a fact, not a gap)', () => { expect(populated(0)).toBe(true); expect(populated(4.31)).toBe(true); }); }); describe('coverage — reads the grader’s shape and splits by stat', () => { const getOdds = async () => ({ props: [ prop('Batter A', 'hits'), prop('Batter B', 'rbi'), prop('Pitcher C', 'strikeouts'), { ...prop('Ignored', 'hits'), book: 'prizepicks' }, // DFS never reaches the model ], }); // Mirrors computeFeaturesForProp: features live UNDER `features`. const computeFeatures = async (p) => ({ features: p.stat_type === 'strikeouts' ? { l5_avg: 6.1, l20_avg: 5.8 } // pitcher: no at-bats : { l5_avg: 1.2, l20_avg: 1.1, ab_per_game: 4.3, rest_days: 0 }, }); it('reports per-feature coverage from the nested features object', async () => { const r = await coverage({ sport: 'mlb', sample: 10, concurrency: 2, getOdds, computeFeatures, isModelBook }); expect(r.sampled).toBe(3); // the DFS row is excluded upstream expect(r.coverage.l5_avg.pct).toBe(100); expect(r.coverage.ab_per_game.populated).toBe(2); }); it('SPLITS BY STAT — a pooled number would hide a pitcher-shaped hole', async () => { const r = await coverage({ sport: 'mlb', sample: 10, concurrency: 2, getOdds, computeFeatures, isModelBook }); expect(r.by_stat.hits.ab_per_game).toBe(100); expect(r.by_stat.rbi.ab_per_game).toBe(100); expect(r.by_stat.strikeouts.ab_per_game).toBe(0); // the hole the pooled 66% would bury }); it('flags a per-player CONSTANT — it cannot separate a player’s own props', async () => { const twoProps = async () => ({ props: [prop('Same Guy', 'hits'), prop('Same Guy', 'rbi')] }); const constant = async () => ({ features: { ab_per_game: 4.3 } }); const r = await coverage({ sport: 'mlb', sample: 10, concurrency: 2, getOdds: twoProps, computeFeatures: constant, isModelBook }); expect(r.ab_per_game_shape.players_with_value).toBe(1); expect(r.ab_per_game_shape.players_where_it_varies_across_their_props).toBe(0); expect(r.ab_per_game_shape.note).toMatch(/CONSTANT per player/); }); it('a thrown feature build counts as ABSENT, never as populated', async () => { const boom = async () => { throw new Error('nope'); }; const r = await coverage({ sport: 'mlb', sample: 10, concurrency: 2, getOdds, computeFeatures: boom, isModelBook }); expect(r.coverage.l5_avg.populated).toBe(0); }); it('is read-only by contract', async () => { const r = await coverage({ sport: 'mlb', sample: 10, concurrency: 2, getOdds, computeFeatures, isModelBook }); expect(r.read_only).toBe(true); }); });