brackt/app/services/simulations/__tests__/manifest.test.ts
Claude dc4efe87cf
Unify futures odds with the simulation system
Make the futures-vs-Elo relationship an explicit, configurable rule and fix
futures odds failing to override stored Elo.

Root causes addressed:
- resolveSourceElos/resolveRatings used a hardcoded precedence (direct Elo ->
  projectedWins -> projectedTablePoints -> odds). The prior fix nulled sourceElo
  on futures entry but not projections, which still outranked odds.
- The override relied on a destructive null-on-save hack that also wiped
  manually entered Elo.
- Blended simulators buried their Elo/odds weight in module constants.
- Futures odds were not surfaced on the /admin/simulators inventory.

Changes:
- Add a configurable source policy: sourceEloPriority (ordered) and oddsWeight,
  parsed/clamped in getSimulatorInputPolicy. resolveSourceElos/resolveRatings now
  resolve each participant by the configured priority instead of a fixed order.
  Futures-centric simulators (ncaam, ncaaw, world_cup, ncaa_football,
  college_hockey) default to a futures-override priority.
- Drop the destructive nulling in batchSaveFuturesOddsForSimulator and
  batchSaveSourceOdds; override is now governed by policy, preserving stored Elo.
- Thread an optional SimulationContext (oddsWeight) through the Simulator
  interface so blended sims (UCL, World Cup, NCAA FB, MLB) read the blend weight
  from the season policy; defaults preserve prior calibration when no context is
  passed.
- Add a "Futures vs. Elo" strategy control and Odds Blend Weight input to the
  Simulator Setup input-policy card, persisted via save-input-policy.
- Surface futures on /admin/simulators: a source badge and a Futures Odds quick
  link; extend listSportsSeasonSimulatorSummaries with odds source info.
- Tests: configurable priority override (Elo/projections/rating), oddsWeight
  parsing/clamping, prefersFuturesOdds, manifest defaults, an NCAA Football
  context-blend behavioral test, and an updated non-destructive save test.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QhNeB7gN6VKene7sdbBVQT
2026-06-25 22:11:32 +00:00

68 lines
3 KiB
TypeScript

import { describe, expect, it } from "vitest";
import * as schema from "~/database/schema";
import { SIMULATOR_MANIFEST } from "../manifest";
import { SIMULATOR_TYPES } from "../registry";
import { getSimulatorInputPolicy, prefersFuturesOdds } from "../input-policy";
import { assertRegistrySchemaDriftFree } from "~/models/simulator";
describe("simulator manifest", () => {
it("has a manifest profile for every registered simulator type", () => {
expect(Object.keys(SIMULATOR_MANIFEST).toSorted()).toEqual([...SIMULATOR_TYPES].toSorted());
});
it("keeps the registry and database enum in sync", () => {
expect([...schema.simulatorTypeEnum.enumValues].toSorted()).toEqual([...SIMULATOR_TYPES].toSorted());
});
it("assertRegistrySchemaDriftFree does not throw when registry and schema are aligned", async () => {
await expect(assertRegistrySchemaDriftFree()).resolves.toBeUndefined();
});
it("documents admin setup requirements for all profiles", () => {
for (const simulatorType of SIMULATOR_TYPES) {
const profile = SIMULATOR_MANIFEST[simulatorType];
expect(profile.displayName).toBeTruthy();
expect(profile.description).toBeTruthy();
expect(Array.isArray(profile.requiredInputs)).toBe(true);
expect(Array.isArray(profile.optionalInputs)).toBe(true);
expect(profile.setupSections.length).toBeGreaterThan(0);
expect(profile.defaultConfig).toBeTypeOf("object");
}
});
it("only derives inputs from declared optional inputs", () => {
for (const simulatorType of SIMULATOR_TYPES) {
const profile = SIMULATOR_MANIFEST[simulatorType];
for (const alternatives of Object.values(profile.derivableInputs ?? {})) {
for (const alternative of alternatives ?? []) {
expect(profile.optionalInputs).toContain(alternative);
}
}
}
});
it("defaults futures-centric simulators to a futures-override source priority", () => {
const futuresCentric = ["ncaam_bracket", "ncaaw_bracket", "world_cup", "ncaa_football_bracket", "college_hockey_bracket"] as const;
for (const simulatorType of futuresCentric) {
const policy = getSimulatorInputPolicy(SIMULATOR_MANIFEST[simulatorType].defaultConfig);
expect(prefersFuturesOdds(policy.sourceEloPriority)).toBe(true);
}
});
it("exposes the blended Elo/odds weight through the input policy", () => {
expect(getSimulatorInputPolicy(SIMULATOR_MANIFEST.ncaa_football_bracket.defaultConfig).oddsWeight).toBe(0.4);
expect(getSimulatorInputPolicy(SIMULATOR_MANIFEST.world_cup.defaultConfig).oddsWeight).toBe(0.3);
// A non-futures team sport keeps the Elo-first default.
expect(prefersFuturesOdds(getSimulatorInputPolicy(SIMULATOR_MANIFEST.nba_bracket.defaultConfig).sourceEloPriority)).toBe(false);
});
it("keeps EPL projection and match parity as separate config knobs", () => {
expect(SIMULATOR_MANIFEST.epl_standings.defaultConfig).toMatchObject({
parityFactor: 400,
matchParityFactor: 400,
averageOpponentElo: 1500,
baseDrawRate: 0.26,
drawDecay: 0.002,
});
});
});