brackt/app/models/__tests__/simulator-inputs.test.ts
Claude 88248e349c
All checks were successful
🚀 Deploy / 🚀 Deploy (pull_request) Has been skipped
🚀 Deploy / 🧪 Test (pull_request) Successful in 2m58s
🚀 Deploy / ʦ🔍 Typecheck & Lint (pull_request) Successful in 1m23s
🚀 Deploy / 🐳 Build (pull_request) Has been skipped
Fix futures odds being ignored when stale Elo exists
When an admin entered futures (preseason) odds for a season that already
had Elo ratings stored, the simulator kept using the old Elo and silently
ignored the new odds. This affected any Elo-based simulator (e.g. NHL).

Root cause: resolveSourceElos() ranks a direct sourceElo above the
sourceOdds -> convertFuturesToElo branch, but batchSaveFuturesOddsForSimulator()
only cleared the bracket-seeding `rating`/`ratingMethod` — never the stale
`sourceElo`/`sourceEloMethod`. A manually entered Elo (method "direct") is not
treated as generated, so it survived and short-circuited the resolver.

Fix:
- batchSaveFuturesOddsForSimulator now also nulls sourceElo and strips
  sourceEloMethod (both the pre-update and upsert-conflict paths), so the
  existing futures -> Elo conversion drives the run.
- resolveSourceElos' sourceOdds branch now guards for >= 2 participants
  (mirroring resolveRatings), so a lone-odds season falls through to the
  configured missing-Elo strategy instead of getting a flat ~1500.
- batchSaveSourceOdds clears the legacy EV sourceElo and marks source as
  futures_odds so the elo-ratings page won't resurrect a stale rating.

Adds unit coverage for odds-derived Elo, the single-participant guard, the
post-clear regression, generated-vs-direct sourceElo suppression, and the
new clearing behavior in batchSaveFuturesOddsForSimulator.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YNfUEd9RzD3zm84oLHBHUH
2026-06-25 17:43:31 +00:00

114 lines
3.5 KiB
TypeScript

import { describe, expect, it, vi, beforeEach } from "vitest";
import { getParticipantSimulatorInputs } from "../simulator";
const mockDb = vi.hoisted(() => ({
query: {
seasonParticipants: { findMany: vi.fn() },
seasonParticipantSimulatorInputs: { findMany: vi.fn() },
seasonParticipantExpectedValues: { findMany: vi.fn() },
},
}));
vi.mock("~/database/context", () => ({
database: () => mockDb,
}));
describe("simulator input model", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("does not treat previously generated ratings as direct rating inputs", async () => {
mockDb.query.seasonParticipants.findMany.mockResolvedValue([
{ id: "direct-rating" },
{ id: "generated-rating" },
{ id: "legacy-direct-rating" },
]);
mockDb.query.seasonParticipantSimulatorInputs.findMany.mockResolvedValue([
{
participantId: "direct-rating",
sourceOdds: 750,
sourceElo: null,
worldRanking: null,
rating: "31.2500",
projectedWins: null,
projectedTablePoints: null,
seed: null,
region: null,
metadata: { ratingMethod: "direct" },
},
{
participantId: "generated-rating",
sourceOdds: 750,
sourceElo: null,
worldRanking: null,
rating: "35.0000",
projectedWins: null,
projectedTablePoints: null,
seed: null,
region: null,
metadata: { ratingMethod: "sourceOdds" },
},
{
participantId: "legacy-direct-rating",
sourceOdds: null,
sourceElo: null,
worldRanking: null,
rating: "27.5000",
projectedWins: null,
projectedTablePoints: null,
seed: null,
region: null,
metadata: null,
},
]);
mockDb.query.seasonParticipantExpectedValues.findMany.mockResolvedValue([]);
const inputs = await getParticipantSimulatorInputs("season-1");
const byParticipant = new Map(inputs.map((input) => [input.participantId, input]));
expect(byParticipant.get("direct-rating")?.rating).toBe(31.25);
expect(byParticipant.get("generated-rating")?.rating).toBeNull();
expect(byParticipant.get("legacy-direct-rating")?.rating).toBe(27.5);
});
it("keeps direct sourceElo but suppresses generated sourceElo", async () => {
mockDb.query.seasonParticipants.findMany.mockResolvedValue([
{ id: "direct-elo" },
{ id: "generated-elo" },
]);
mockDb.query.seasonParticipantSimulatorInputs.findMany.mockResolvedValue([
{
participantId: "direct-elo",
sourceOdds: null,
sourceElo: 1600,
worldRanking: null,
rating: null,
projectedWins: null,
projectedTablePoints: null,
seed: null,
region: null,
metadata: { sourceEloMethod: "direct" },
},
{
participantId: "generated-elo",
sourceOdds: 750,
sourceElo: 1480,
worldRanking: null,
rating: null,
projectedWins: null,
projectedTablePoints: null,
seed: null,
region: null,
metadata: { sourceEloMethod: "sourceOdds" },
},
]);
mockDb.query.seasonParticipantExpectedValues.findMany.mockResolvedValue([]);
const inputs = await getParticipantSimulatorInputs("season-1");
const byParticipant = new Map(inputs.map((input) => [input.participantId, input]));
expect(byParticipant.get("direct-elo")?.sourceElo).toBe(1600);
expect(byParticipant.get("generated-elo")?.sourceElo).toBeNull();
});
});