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
114 lines
3.5 KiB
TypeScript
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();
|
|
});
|
|
});
|