Files
vyndr/tests/unit/combatAdapter.test.js
builtbykev 54fa5853f5 Wave 6: Combat Intelligence Layer (honest free v1)
Net-new MMA/UFC vertical — fight-card discovery, tale-of-the-tape,
style-blend archetypes, ML + round-total odds, and a style-edge VERDICT
(a MODEL read, explicitly NOT a settled grade). Built to
specs/combat-intelligence.md.

Backend:
- combatAdapter: ESPN MMA scoreboard (date-pinned, free JSON) -> fight
  cards + tale-of-tape (record/weight class/rounds/ESPN athlete id);
  defensive parse (null on unknown shape, never throws); injectable
  fetchImpl + cache; pure normalizeCombatOdds (odds-api h2h/totals ->
  ML + round total, allow-listed books, best price). Number(null) guard.
- archetypeService: 6 pinned combat styles in a SEPARATE COMBAT_ARCHETYPES
  registry (FINISHER collides with soccer + its green trips the signal-
  green gate); classify('mma') blends range/tempo/outcome, honest-empty on
  thin data (no forced fallback); styleMatchup() honest verdict.
- oddsService: SPORT_KEYS.mma + MMA_MARKETS=['h2h','totals'] + SPORT_MARKETS
  (no spreads suffix). oddsNormalizer MARKET_MAP h2h/totals.
- config/sports.js + web mirror: mma.active=true (collectData stays false;
  NOT in the graded-props pipeline SPORT_CONFIG or snapshot/settle loop).
- routes/combat.js: GET /api/combat/:date + GET /api/fight/:id (public,
  cached, honest empty off-card) + Next proxies.

Frontend:
- FightCard: two-fighter tale-of-the-tape (initials monogram — no photos),
  GRAPPLER/STRIKER blend bars, discipline pedigree tags, shared
  ArchetypeBadge (sport="mma", unicode glyphs), CENTER VERDICT, ML +
  round-total real; method/round/KO = honest "data-limited", never
  fabricated. Self-hides on a non-two-fighter bout.
- /fight/[id] page (server wrapper + client), EmptyState off-season.
- MMA SportBadge token (#D4AF37); archetypes.js sport-aware resolution.

DEFERRED (per spec, NOT built): matchup-GRADE engine, method/round/props
board, combat settlement, ufcstats scraping.

Tests: +3 suites (31 tests) — combat archetype cross-file color/glyph
match, classify blends, styleMatchup honesty, adapter defensive parse +
odds normalize, FightCard honesty grep; extended oddsNormalizer +
sportMarkets. Full suite 253/253 green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 16:58:22 -04:00

186 lines
7.8 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Wave 6 — combatAdapter defensive parse. Fixtures modeled on the REAL ESPN
// MMA scoreboard shape (site.api.espn.com/.../mma/ufc/scoreboard) captured
// live during the build. NO network — fetchImpl + cache are injected.
const combat = require('../../src/services/adapters/combatAdapter');
// A trimmed but structurally-faithful ESPN MMA scoreboard payload: one UFC
// event ("card") with two bouts. Athlete ids live in the player-card link href.
const ESPN_FIXTURE = {
events: [
{
id: '600059599',
name: 'UFC Fight Night: Du Plessis vs. Usman',
shortName: 'UFC Fight Night',
date: '2026-07-18T21:00Z',
competitions: [
{
id: '1',
type: { id: '1007', abbreviation: 'W Flyweight', text: "Women's Flyweight" },
format: { regulation: { periods: 5 } },
venue: { fullName: 'UFC APEX' },
status: { type: { state: 'pre', completed: false } },
competitors: [
{
id: '10', order: 0, winner: false,
athlete: {
fullName: 'Dricus du Plessis', displayName: 'Dricus du Plessis', shortName: 'D. du Plessis',
links: [{ href: 'https://www.espn.com/mma/fighter/_/id/4801725/dricus-du-plessis' }],
},
records: [{ name: 'overall', type: 'total', summary: '22-2-0' }],
},
{
id: '11', order: 1, winner: false,
athlete: {
fullName: 'Kamaru Usman', displayName: 'Kamaru Usman', shortName: 'K. Usman',
links: [{ href: 'https://www.espn.com/mma/fighter/_/id/3088843/kamaru-usman' }],
},
records: [{ name: 'overall', type: 'total', summary: '20-4-0' }],
},
],
},
{
id: '2',
type: { abbreviation: 'Lightweight' },
format: { regulation: { periods: 3 } },
competitors: [
{ id: '20', order: 0, athlete: { displayName: 'Fighter A', links: [] }, records: [{ type: 'total', summary: '10-0' }] },
{ id: '21', order: 1, athlete: { displayName: 'Fighter B', links: [] }, records: [{ type: 'total', summary: '8-3' }] },
],
},
],
},
],
};
describe('combatAdapter.normalizeScoreboard — ESPN shape → fight cards', () => {
it('normalizes a real-shaped payload into a card with bouts + tale-of-tape', () => {
const { events } = combat.normalizeScoreboard(ESPN_FIXTURE);
expect(events).toHaveLength(1);
const card = events[0];
expect(card.id).toBe('600059599');
expect(card.name).toMatch(/Du Plessis/);
expect(card.bouts).toHaveLength(2);
const bout = card.bouts[0];
expect(bout.weightClass).toBe("Women's Flyweight");
expect(bout.rounds).toBe(5);
expect(bout.fighters).toHaveLength(2);
const a = bout.fighters[0];
expect(a.name).toBe('Dricus du Plessis');
expect(a.id).toBe('4801725'); // parsed from the link href
expect(a.record.wins).toBe(22);
expect(a.record.losses).toBe(2);
expect(a.record.display).toBe('222'); // en-dash display form
// Physicals absent from the free feed → null, NEVER fabricated 0.
expect(a.stance).toBeNull();
expect(a.reach).toBeNull();
});
it('date-pins defensively — an off-date event is dropped', () => {
const onDate = combat.normalizeScoreboard(ESPN_FIXTURE, { date: '2026-07-18' });
expect(onDate.events).toHaveLength(1);
const offDate = combat.normalizeScoreboard(ESPN_FIXTURE, { date: '2026-01-01' });
expect(offDate.events).toHaveLength(0);
});
it('DEFENSIVE: unrecognized/garbage shapes return empty, never throw', () => {
expect(() => combat.normalizeScoreboard(null)).not.toThrow();
expect(combat.normalizeScoreboard(null).events).toEqual([]);
expect(combat.normalizeScoreboard({}).events).toEqual([]);
expect(combat.normalizeScoreboard({ events: 'nope' }).events).toEqual([]);
// An identifiable event with only unusable bouts is kept with empty bouts.
expect(combat.normalizeScoreboard({ events: [{ id: 'x', competitions: [{ competitors: [{}] }] }] }).events[0].bouts).toEqual([]);
// An event with no id AND no usable bouts is dropped entirely.
expect(combat.normalizeScoreboard({ events: [{ competitions: [{ competitors: [{}] }] }] }).events).toEqual([]);
});
it('numOrNull never coerces null/empty to 0 (the fabrication trap)', () => {
expect(combat.numOrNull(null)).toBeNull();
expect(combat.numOrNull('')).toBeNull();
expect(combat.numOrNull(undefined)).toBeNull();
expect(combat.numOrNull('5')).toBe(5);
expect(combat.numOrNull(0)).toBe(0);
});
it('parseAthleteId is defensive on bad links', () => {
expect(combat.parseAthleteId(null)).toBeNull();
expect(combat.parseAthleteId([{ href: 'no-id-here' }])).toBeNull();
expect(combat.parseAthleteId([{ href: '/mma/fighter/_/id/999/x' }])).toBe('999');
});
});
describe('combatAdapter.getFightCards — injectable, no network', () => {
it('fetches via injected fetchImpl + normalizes (cache stubbed)', async () => {
const calls = [];
const res = await combat.getFightCards('2026-07-18', {
fetchImpl: async (url) => { calls.push(url); return ESPN_FIXTURE; },
cacheGet: async () => null,
cacheSet: async () => true,
});
expect(res.events).toHaveLength(1);
expect(calls[0]).toMatch(/dates=20260718/);
});
it('returns honest empty on a fetch error, never throws', async () => {
const res = await combat.getFightCards('2026-07-18', {
fetchImpl: async () => { throw new Error('network'); },
cacheGet: async () => null,
cacheSet: async () => true,
});
expect(res.events).toEqual([]);
});
});
describe('combatAdapter.normalizeCombatOdds — odds-api MMA → ML + round total', () => {
const ODDS_FIXTURE = [
{
id: 'evt1', commence_time: '2026-07-18T21:00Z',
home_team: 'Dricus du Plessis', away_team: 'Kamaru Usman',
bookmakers: [
{
key: 'draftkings',
markets: [
{ key: 'h2h', outcomes: [{ name: 'Dricus du Plessis', price: -230 }, { name: 'Kamaru Usman', price: 190 }] },
{ key: 'totals', outcomes: [{ name: 'Over', price: -110, point: 2.5 }, { name: 'Under', price: -110, point: 2.5 }] },
],
},
{
key: 'fanduel',
markets: [{ key: 'h2h', outcomes: [{ name: 'Dricus du Plessis', price: -215 }, { name: 'Kamaru Usman', price: 200 }] }],
},
// A non-allow-listed book must be ignored.
{ key: 'bovada', markets: [{ key: 'h2h', outcomes: [{ name: 'Dricus du Plessis', price: -999 }] }] },
],
},
];
it('maps h2h to per-fighter moneyline (best price) + totals to round total', () => {
const map = combat.normalizeCombatOdds(ODDS_FIXTURE);
const rec = map['dricus du plessis|kamaru usman'];
expect(rec).toBeDefined();
expect(rec.moneyline.home).toBe(-215); // best (higher) of -230 / -215
expect(rec.moneyline.away).toBe(200); // best of 190 / 200
expect(rec.roundTotal.line).toBe(2.5);
expect(rec.roundTotal.over).toBe(-110);
});
it('ignores non-allow-listed books (bovada never leaks a price)', () => {
const map = combat.normalizeCombatOdds(ODDS_FIXTURE);
expect(map['dricus du plessis|kamaru usman'].moneyline.home).not.toBe(-999);
});
it('matchBoutOdds joins in either name orientation', () => {
const map = combat.normalizeCombatOdds(ODDS_FIXTURE);
const bout = { fighters: [{ name: 'Kamaru Usman' }, { name: 'Dricus du Plessis' }] };
expect(combat.matchBoutOdds(bout, map)).toBeTruthy();
});
it('DEFENSIVE: garbage odds input returns {}, never throws', () => {
expect(() => combat.normalizeCombatOdds(null)).not.toThrow();
expect(combat.normalizeCombatOdds(null)).toEqual({});
expect(combat.normalizeCombatOdds([{ bookmakers: 'x' }])).toEqual({});
});
});