MLB odds cache is keyed on the baseball date, not the UTC date
A cache entry answers "what is the market for THIS SLATE". For MLB the slate is
a BASEBALL DATE in America/New_York -- the same date gameBinder,
retentionService, ledgerService and read_natural_key all use. The key was built
from the UTC calendar date, so between 00:00Z and Eastern midnight the key
advanced while the slate did not:
2026-08-29T01:03Z = 2026-08-28 21:03 ET
slate date 2026-08-28, key looked up odds:mlb:2026-08-29
A value written earlier that evening under odds:mlb:2026-08-28 was then
unreachable -- not expired, ADDRESSED WRONG.
CAUSAL HONESTY: this is NOT retroactively the cause of the failed 01:03Z canary.
That run also used an 84-minute-old observation whose cache had passed its 1h
TTL. Two independent reasons; the date defect is real but not proven
counterfactual.
MLB ONLY. Every other sport keeps the UTC basis -- their date semantics are
unproven here and rekeying a cache they already write and read consistently
would invalidate live entries for no demonstrated defect.
Symmetry is structural, not conventional: the three readers that built the key
inline now ask `oddsService.getCacheKey(sport)`, so writer and readers cannot
diverge. The ET date comes from `scheduleService.gameDateET` -- the repository's
own Intl/America\/New_York primitive, now exported -- so DST is the zone
database's business and never offset arithmetic. An unresolvable clock REFUSES
rather than falling back to the other basis.
TTL truth is kept separate: a correctly addressed but expired entry still
misses, and CACHE_TTL is unchanged at 3600.
Provider priority, quota policy, retries, EARLY_RETURN_ODDS_ERROR semantics,
lineage lookup, game-date repair, canonical participant and intraday belief
integrity are all untouched.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CQJeAG8vcDoL5zkiaJyVb8
This commit is contained in:
@@ -38,9 +38,9 @@ async function readGroupedProps(sport) {
|
|||||||
const store = await cacheGet(`bookprices:${sport}`);
|
const store = await cacheGet(`bookprices:${sport}`);
|
||||||
if (store && Array.isArray(store.props) && store.props.length) return store.props;
|
if (store && Array.isArray(store.props) && store.props.length) return store.props;
|
||||||
|
|
||||||
const utcDate = new Date().toISOString().split('T')[0];
|
// The writer owns the key: MLB is keyed on its ET baseball date.
|
||||||
const cache =
|
const cache =
|
||||||
(await cacheGet(`odds:${sport}:${utcDate}`)) ??
|
(await cacheGet(require('../services/oddsService').getCacheKey(sport))) ??
|
||||||
(await cacheGet(`odds:${sport}`));
|
(await cacheGet(`odds:${sport}`));
|
||||||
const flat = cache && Array.isArray(cache.props) ? cache.props : [];
|
const flat = cache && Array.isArray(cache.props) ? cache.props : [];
|
||||||
const by = new Map();
|
const by = new Map();
|
||||||
|
|||||||
@@ -370,8 +370,9 @@ function defaultCollectors() {
|
|||||||
},
|
},
|
||||||
getMovers: (sport) => lineSnapshotService.getBiggestMovers(sport, { limit: 10 }),
|
getMovers: (sport) => lineSnapshotService.getBiggestMovers(sport, { limit: 10 }),
|
||||||
getBestLines: async (sport) => {
|
getBestLines: async (sport) => {
|
||||||
const utc = new Date().toISOString().split('T')[0];
|
// The writer owns the key: MLB is keyed on its ET baseball date.
|
||||||
const cache = (await cacheGet(`odds:${sport}:${utc}`)) ?? (await cacheGet(`odds:${sport}`));
|
const key = require('./oddsService').getCacheKey(sport);
|
||||||
|
const cache = (await cacheGet(key)) ?? (await cacheGet(`odds:${sport}`));
|
||||||
const props = Array.isArray(cache?.props) ? cache.props : [];
|
const props = Array.isArray(cache?.props) ? cache.props : [];
|
||||||
return bookComparisonService.bestLines(props, { limit: 10 });
|
return bookComparisonService.bestLines(props, { limit: 10 });
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -199,9 +199,52 @@ function getMarketsForSport(sport) {
|
|||||||
|
|
||||||
const BOOKMAKERS = 'draftkings,fanduel,betmgm,caesars,fanatics,bet365,hardrockbet,pointsbet,betrivers';
|
const BOOKMAKERS = 'draftkings,fanduel,betmgm,caesars,fanatics,bet365,hardrockbet,pointsbet,betrivers';
|
||||||
|
|
||||||
function getCacheKey(sport) {
|
/**
|
||||||
const now = new Date();
|
* ── CACHE DATE BASIS, PER SPORT ──────────────────────────────────────────
|
||||||
const date = now.toISOString().split('T')[0]; // UTC date
|
* A cache entry answers "what is the market for THIS SLATE". For MLB the slate
|
||||||
|
* is a BASEBALL DATE in America/New_York — that is the date `gameBinder`,
|
||||||
|
* `retentionService`, `ledgerService` and the natural key all use. The cache
|
||||||
|
* key was built from the UTC calendar date instead, so between 00:00Z and
|
||||||
|
* Eastern midnight (04:00Z under EDT, 05:00Z under EST) the key advanced while
|
||||||
|
* the slate did not:
|
||||||
|
*
|
||||||
|
* 2026-08-29T01:03Z = 2026-08-28 21:03 ET
|
||||||
|
* slate date 2026-08-28 key looked up odds:mlb:2026-08-29
|
||||||
|
*
|
||||||
|
* A value written earlier that evening under `odds:mlb:2026-08-28` was then
|
||||||
|
* unreachable — not expired, ADDRESSED WRONG. That is a cache IDENTITY defect
|
||||||
|
* and it is repaired here for MLB only.
|
||||||
|
*
|
||||||
|
* DELIBERATELY NOT A MULTI-SPORT CONVERSION. Every other sport keeps the UTC
|
||||||
|
* basis: their date semantics are UNPROVEN here, and changing a key they are
|
||||||
|
* already writing and reading consistently would invalidate live caches for no
|
||||||
|
* demonstrated defect. Classification is recorded in the tranche audit.
|
||||||
|
*
|
||||||
|
* The ET date comes from `scheduleService.gameDateET` — the repository's own
|
||||||
|
* `Intl.DateTimeFormat('en-CA', { timeZone: 'America/New_York' })` primitive,
|
||||||
|
* so DST is handled by the zone database and never by offset arithmetic.
|
||||||
|
* Required lazily: `oddsService` is loaded early and this keeps the edge
|
||||||
|
* runtime-only.
|
||||||
|
*
|
||||||
|
* @param {string} sport
|
||||||
|
* @param {Date} [now] injectable clock — deterministic midnight/DST fixtures
|
||||||
|
*/
|
||||||
|
const ET_DATE_SPORTS = Object.freeze(['mlb']);
|
||||||
|
|
||||||
|
function getCacheKey(sport, now = new Date()) {
|
||||||
|
const sp = String(sport || '').toLowerCase();
|
||||||
|
if (ET_DATE_SPORTS.includes(sp)) {
|
||||||
|
const { gameDateET } = require('./scheduleService');
|
||||||
|
const stamp = now instanceof Date
|
||||||
|
? (Number.isNaN(now.getTime()) ? null : now.toISOString())
|
||||||
|
: now;
|
||||||
|
const et = stamp ? gameDateET(stamp) : null;
|
||||||
|
if (et) return `odds:${sport}:${et}`;
|
||||||
|
// An unparseable clock must not silently fall back to a DIFFERENT date
|
||||||
|
// basis for the same sport — that is the very asymmetry being repaired.
|
||||||
|
throw new TypeError(`getCacheKey: could not resolve an ET date for ${sport}`);
|
||||||
|
}
|
||||||
|
const date = (now instanceof Date ? now : new Date(now)).toISOString().split('T')[0]; // UTC date
|
||||||
return `odds:${sport}:${date}`;
|
return `odds:${sport}:${date}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -565,6 +608,7 @@ module.exports = {
|
|||||||
fetchEventsFromApi,
|
fetchEventsFromApi,
|
||||||
fetchEventOddsFromApi,
|
fetchEventOddsFromApi,
|
||||||
getCacheKey,
|
getCacheKey,
|
||||||
|
ET_DATE_SPORTS,
|
||||||
SPORT_KEYS,
|
SPORT_KEYS,
|
||||||
SOCCER_SPORT_KEYS,
|
SOCCER_SPORT_KEYS,
|
||||||
// Wave 2A — futures/outrights keys (separate budget; futuresService only).
|
// Wave 2A — futures/outrights keys (separate budget; futuresService only).
|
||||||
|
|||||||
@@ -166,13 +166,16 @@ async function enrichFlags(sport, date, games) {
|
|||||||
if (!Array.isArray(games) || games.length === 0) return games;
|
if (!Array.isArray(games) || games.length === 0) return games;
|
||||||
const ymd = String(date).replace(/-/g, '');
|
const ymd = String(date).replace(/-/g, '');
|
||||||
|
|
||||||
// odds-api props cache — oddsService writes `odds:{sport}:{utcDate}`
|
// Props cache — `oddsService.getCacheKey(sport)` owns the key. For MLB that
|
||||||
// as `{ updated_at, props, spreads }`. The slate `date` is ET, so try
|
// is now the ET BASEBALL DATE; every other sport is still the UTC date. The
|
||||||
// the ET key first then the UTC key (they differ only past midnight).
|
// requested slate `date` is tried FIRST because it is more specific than
|
||||||
const utcDate = new Date().toISOString().split('T')[0];
|
// "today" (it can be a future or past slate), then the writer's own key.
|
||||||
|
// Lazy require: the key belongs to the writer, and asking it removes any
|
||||||
|
// chance of a reader inventing a different date basis.
|
||||||
|
const writerKey = require('./oddsService').getCacheKey(sport);
|
||||||
const oddsCache =
|
const oddsCache =
|
||||||
(await cacheGet(`odds:${sport}:${date}`)) ??
|
(await cacheGet(`odds:${sport}:${date}`)) ??
|
||||||
(await cacheGet(`odds:${sport}:${utcDate}`)) ??
|
(await cacheGet(writerKey)) ??
|
||||||
(await cacheGet(`odds:${sport}`));
|
(await cacheGet(`odds:${sport}`));
|
||||||
const hasOdds = hasPropsData(oddsCache);
|
const hasOdds = hasPropsData(oddsCache);
|
||||||
|
|
||||||
@@ -261,6 +264,10 @@ module.exports = {
|
|||||||
getSchedule,
|
getSchedule,
|
||||||
enrichFlags,
|
enrichFlags,
|
||||||
todayET,
|
todayET,
|
||||||
|
// The ET calendar date of an INSTANT. `todayET()` is this applied to now;
|
||||||
|
// exporting it lets the odds cache key be derived for an injected clock
|
||||||
|
// without a sixteenth copy of the America/New_York formatter.
|
||||||
|
gameDateET,
|
||||||
getGameSummary,
|
getGameSummary,
|
||||||
__internals: { normalizeEvent, fetchScheduleFromEspn, hasPropsData, hasLinesData, ESPN_SPORT_PATHS },
|
__internals: { normalizeEvent, fetchScheduleFromEspn, hasPropsData, hasLinesData, ESPN_SPORT_PATHS },
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,182 @@
|
|||||||
|
// MLB ODDS CACHE DATE CONTRACT.
|
||||||
|
//
|
||||||
|
// A cache entry answers "what is the market for THIS SLATE". For MLB the slate
|
||||||
|
// is a BASEBALL DATE in America/New_York -- the same date `gameBinder`,
|
||||||
|
// `retentionService`, `ledgerService` and `read_natural_key` all use. The key
|
||||||
|
// was built from the UTC calendar date, so between 00:00Z and Eastern midnight
|
||||||
|
// the key advanced while the slate did not, and a value written earlier that
|
||||||
|
// evening became unreachable: not expired, ADDRESSED WRONG.
|
||||||
|
//
|
||||||
|
// TTL truth is deliberately kept SEPARATE. A correctly keyed but expired entry
|
||||||
|
// must still miss.
|
||||||
|
|
||||||
|
const odds = require('../../src/services/oddsService');
|
||||||
|
const schedule = require('../../src/services/scheduleService');
|
||||||
|
|
||||||
|
const at = (iso) => new Date(iso);
|
||||||
|
|
||||||
|
describe('MLB is keyed on the ET baseball date', () => {
|
||||||
|
test('the UTC-midnight window: the key must not advance before Eastern midnight', () => {
|
||||||
|
// EDT: Eastern midnight is 04:00Z.
|
||||||
|
expect(odds.getCacheKey('mlb', at('2026-08-28T23:59:59Z'))).toBe('odds:mlb:2026-08-28');
|
||||||
|
expect(odds.getCacheKey('mlb', at('2026-08-29T00:00:00Z'))).toBe('odds:mlb:2026-08-28');
|
||||||
|
// The exact instant of the failed canary.
|
||||||
|
expect(odds.getCacheKey('mlb', at('2026-08-29T01:03:00Z'))).toBe('odds:mlb:2026-08-28');
|
||||||
|
expect(odds.getCacheKey('mlb', at('2026-08-29T03:59:59Z'))).toBe('odds:mlb:2026-08-28');
|
||||||
|
// Eastern midnight: NOW it advances.
|
||||||
|
expect(odds.getCacheKey('mlb', at('2026-08-29T04:00:00Z'))).toBe('odds:mlb:2026-08-29');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('EST: Eastern midnight is 05:00Z, and nothing hardcodes an offset', () => {
|
||||||
|
// January -- standard time. A fixed -04:00 would roll an hour early here.
|
||||||
|
expect(odds.getCacheKey('mlb', at('2026-01-15T23:59:59Z'))).toBe('odds:mlb:2026-01-15');
|
||||||
|
expect(odds.getCacheKey('mlb', at('2026-01-16T00:00:00Z'))).toBe('odds:mlb:2026-01-15');
|
||||||
|
expect(odds.getCacheKey('mlb', at('2026-01-16T04:00:00Z'))).toBe('odds:mlb:2026-01-15');
|
||||||
|
expect(odds.getCacheKey('mlb', at('2026-01-16T04:59:59Z'))).toBe('odds:mlb:2026-01-15');
|
||||||
|
expect(odds.getCacheKey('mlb', at('2026-01-16T05:00:00Z'))).toBe('odds:mlb:2026-01-16');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('the DST transitions themselves, read from the zone database', () => {
|
||||||
|
// Spring forward 2026-03-08 (EST -> EDT). The night BEFORE still rolls at 05:00Z.
|
||||||
|
expect(odds.getCacheKey('mlb', at('2026-03-08T04:59:59Z'))).toBe('odds:mlb:2026-03-07');
|
||||||
|
expect(odds.getCacheKey('mlb', at('2026-03-08T05:00:00Z'))).toBe('odds:mlb:2026-03-08');
|
||||||
|
// The night AFTER is EDT, so it rolls at 04:00Z.
|
||||||
|
expect(odds.getCacheKey('mlb', at('2026-03-09T03:59:59Z'))).toBe('odds:mlb:2026-03-08');
|
||||||
|
expect(odds.getCacheKey('mlb', at('2026-03-09T04:00:00Z'))).toBe('odds:mlb:2026-03-09');
|
||||||
|
// Fall back 2026-11-01 (EDT -> EST).
|
||||||
|
expect(odds.getCacheKey('mlb', at('2026-11-01T03:59:59Z'))).toBe('odds:mlb:2026-10-31');
|
||||||
|
expect(odds.getCacheKey('mlb', at('2026-11-01T04:00:00Z'))).toBe('odds:mlb:2026-11-01');
|
||||||
|
expect(odds.getCacheKey('mlb', at('2026-11-02T04:59:59Z'))).toBe('odds:mlb:2026-11-01');
|
||||||
|
expect(odds.getCacheKey('mlb', at('2026-11-02T05:00:00Z'))).toBe('odds:mlb:2026-11-02');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('the ET date comes from the repository primitive, not offset arithmetic', () => {
|
||||||
|
// Same instant, same answer, through the schedule module's own helper.
|
||||||
|
for (const iso of ['2026-08-29T01:03:00Z', '2026-01-16T04:30:00Z', '2026-03-08T06:00:00Z']) {
|
||||||
|
expect(odds.getCacheKey('mlb', at(iso))).toBe(`odds:mlb:${schedule.gameDateET(iso)}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('an unresolvable clock REFUSES rather than silently using the other basis', () => {
|
||||||
|
expect(() => odds.getCacheKey('mlb', new Date('not-a-date'))).toThrow(/could not resolve an ET date/);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('no other sport changed', () => {
|
||||||
|
test('every non-MLB sport keeps the UTC basis, including across Eastern midnight', () => {
|
||||||
|
for (const sp of ['nba', 'wnba', 'nfl', 'nhl', 'soccer', 'ncaab']) {
|
||||||
|
expect(odds.getCacheKey(sp, at('2026-08-29T01:03:00Z'))).toBe(`odds:${sp}:2026-08-29`);
|
||||||
|
expect(odds.getCacheKey(sp, at('2026-08-28T23:59:59Z'))).toBe(`odds:${sp}:2026-08-28`);
|
||||||
|
}
|
||||||
|
expect(odds.ET_DATE_SPORTS).toEqual(['mlb']);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('writer / reader symmetry', () => {
|
||||||
|
// The defect class is an ET writer with a UTC reader (or the reverse). The
|
||||||
|
// only durable guarantee is that BOTH ask the same function, so the readers
|
||||||
|
// are asserted to call it rather than to construct a key of their own.
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const strip = (f) => fs.readFileSync(path.join(__dirname, '../../', f), 'utf8')
|
||||||
|
.replace(/\/\*[\s\S]*?\*\//g, '').replace(/^\s*\/\/.*$/gm, '');
|
||||||
|
|
||||||
|
test('no reader builds an odds cache key from its own date', () => {
|
||||||
|
for (const f of ['src/services/contentTemplateService.js', 'src/routes/bookComparison.js',
|
||||||
|
'src/services/scheduleService.js']) {
|
||||||
|
const src = strip(f);
|
||||||
|
expect(src).toMatch(/getCacheKey\(sport\)/);
|
||||||
|
// The hand-rolled `odds:${sport}:${someLocalDate}` shape must be gone.
|
||||||
|
expect(src).not.toMatch(/`odds:\$\{sport\}:\$\{utc/);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('the writer and every reader resolve the SAME key for one instant', () => {
|
||||||
|
const now = at('2026-08-29T01:03:00Z');
|
||||||
|
const writer = odds.getCacheKey('mlb', now);
|
||||||
|
expect(writer).toBe('odds:mlb:2026-08-28');
|
||||||
|
// Readers call getCacheKey with no clock, so prove the no-clock default is
|
||||||
|
// the same function of "now" rather than a second implementation.
|
||||||
|
expect(odds.getCacheKey('mlb')).toBe(`odds:mlb:${schedule.todayET()}`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('TTL truth is independent of key truth', () => {
|
||||||
|
// A correctly addressed entry that has expired must still miss. Repairing an
|
||||||
|
// address must never be allowed to look like extending freshness.
|
||||||
|
const makeRedis = (store) => ({
|
||||||
|
get: async (k) => (Object.prototype.hasOwnProperty.call(store, k) ? store[k] : null),
|
||||||
|
});
|
||||||
|
|
||||||
|
test('correct key + live entry -> HIT; expired -> MISS; wrong date -> MISS', async () => {
|
||||||
|
const key = odds.getCacheKey('mlb', at('2026-08-29T01:03:00Z'));
|
||||||
|
const live = makeRedis({ [key]: JSON.stringify({ updated_at: 'x', props: [{ p: 1 }] }) });
|
||||||
|
expect(await live.get(key)).toBeTruthy();
|
||||||
|
|
||||||
|
// Expiry is Redis's, not ours: an absent key is a miss however correct it is.
|
||||||
|
const expired = makeRedis({});
|
||||||
|
expect(await expired.get(key)).toBeNull();
|
||||||
|
|
||||||
|
// Yesterday's entry does not answer today's lookup.
|
||||||
|
const yesterday = makeRedis({ 'odds:mlb:2026-08-27': JSON.stringify({ props: [{ p: 1 }] }) });
|
||||||
|
expect(await yesterday.get(key)).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('CACHE_TTL is untouched by this repair', () => {
|
||||||
|
expect(odds.CACHE_TTL).toBe(3600);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('cross-baseball-date isolation', () => {
|
||||||
|
test('date N cache cannot satisfy date N+1 once Eastern midnight passes', () => {
|
||||||
|
const beforeMidnight = odds.getCacheKey('mlb', at('2026-08-29T03:59:59Z'));
|
||||||
|
const afterMidnight = odds.getCacheKey('mlb', at('2026-08-29T04:00:00Z'));
|
||||||
|
expect(beforeMidnight).toBe('odds:mlb:2026-08-28');
|
||||||
|
expect(afterMidnight).toBe('odds:mlb:2026-08-29');
|
||||||
|
expect(beforeMidnight).not.toBe(afterMidnight);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('the ROOT-CAUSE fixture: the value written that evening is now reachable', () => {
|
||||||
|
// 2026-08-28 21:30 ET, inside TTL, written for baseball date 2026-08-28.
|
||||||
|
const writeKey = odds.getCacheKey('mlb', at('2026-08-29T01:30:00Z'));
|
||||||
|
// The 01:03Z-style reader, 27 minutes later, same baseball date.
|
||||||
|
const readKey = odds.getCacheKey('mlb', at('2026-08-29T01:57:00Z'));
|
||||||
|
expect(writeKey).toBe('odds:mlb:2026-08-28');
|
||||||
|
expect(readKey).toBe(writeKey); // POST-REPAIR: same address.
|
||||||
|
// PRE-REPAIR both sides used the UTC date, which had already rolled.
|
||||||
|
const preRepair = at('2026-08-29T01:57:00Z').toISOString().split('T')[0];
|
||||||
|
expect(`odds:mlb:${preRepair}`).toBe('odds:mlb:2026-08-29');
|
||||||
|
expect(`odds:mlb:${preRepair}`).not.toBe(writeKey);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('preservation: this is a cache IDENTITY repair only', () => {
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const src = (f) => fs.readFileSync(path.join(__dirname, '../../', f), 'utf8');
|
||||||
|
|
||||||
|
test('provider priority, quota and retry policy are untouched', () => {
|
||||||
|
const s = src('src/services/oddsService.js');
|
||||||
|
expect(s).toContain("require('./adapters/proplineAdapter')");
|
||||||
|
expect(s).toContain("require('./quotaTracker')");
|
||||||
|
expect(odds.CACHE_TTL).toBe(3600);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('the frozen repairs are still present', () => {
|
||||||
|
expect(src('src/services/gameBinder.js')).toContain('if (!p.game_date) p.game_date = etFast;');
|
||||||
|
expect(src('src/services/event/eventIdentity.js')).toMatch(/ids\.size !== 1/);
|
||||||
|
expect(src('src/services/intradayRefreshService.js')).toContain('BELIEF_FIELDS');
|
||||||
|
// Lineage lookup design is frozen in this tranche.
|
||||||
|
const ret = src('src/services/retentionService.js');
|
||||||
|
expect(ret).toContain('familyScopesFrom');
|
||||||
|
expect(ret).toContain('isValidLineageAction');
|
||||||
|
expect(ret).toContain("label: 'attachLineage.fetchExisting'");
|
||||||
|
});
|
||||||
|
|
||||||
|
test('intraday still resolves its odds through the same acquisition seam', () => {
|
||||||
|
const s = src('src/services/intradayRefreshService.js');
|
||||||
|
expect(s).toContain("require('./oddsService').getOdds");
|
||||||
|
// No belief mutation was reintroduced.
|
||||||
|
expect(s.replace(/\/\*[\s\S]*?\*\//g, '').replace(/^\s*\/\/.*$/gm, '')).not.toMatch(/applyRevision/);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user