brackt/app/routes/__tests__/admin.sports-seasons.$id.simulator.helpers.test.ts
Claude 280a46eb5f
Make MLB projected wins actually drive the simulation
Entering projected wins for an in-progress MLB season did not behave as
expected: the entered numbers came back changed, and the simulation appeared
to ignore them in favour of whatever Elo was already stored. Four separate
defects were involved.

Projections are now stored and shown verbatim. The Elo Ratings page never
kept the number typed into it — the field was a display derived from Elo, so
a pasted 95 rendered as 95.1 the moment it was applied (wins to Elo rounds to
an integer Elo) and drifted again after each run, because a run re-resolves
that Elo through the input policy. The loader now reads back the stored
projection and the paste flow keeps the pasted value as-is; the derived
round-trip survives only as a prefill for seasons that have never had a
projection saved.

A stale Elo no longer silently outranks a projection. baseEloPriority takes
the first available base source, and the simulator page's bulk CSV wrote
projectedWins without stamping metadata.sourceEloMethod, so the
non-destructive upsert left the old Elo in place as a trusted direct value
and it won the race — the projection was stored and then ignored on every
run. The CSV path now stamps the flag like the Elo Ratings page does, the
metadata upsert merges rather than replaces so a flag-only write keeps
unrelated keys, and Base Elo Source is editable per season for the case where
a genuine hand-entered Elo should still lose to projections.

Projected wins now act as a projected final total. The value was baked into a
flat season-long rate (projectedWins / 162) applied to every remaining game,
so a team at 60-50 projected for 95 finished around 90.5 and the projection
was never reached mid-season. seedingWinRateFor spreads the difference over
the games still to play, which is a no-op pre-season where the two rates
coincide; projectedWinsWeight blends it back toward the Elo-implied rate.

Playoff-parity compression is restored for Elo-rated teams. eloToRDif scaled
by RDIF_DIVISOR, making it the exact algebraic inverse of winRateFromRDif, so
any team with an Elo skipped the compression every hardcoded-rdif team gets:
a 95-win projection became RDif +686 and played playoff games at .586 instead
of the documented ~.517. It now scales by SEEDING_RDIF_SCALE, landing at ~+140
alongside the Dodgers' hardcoded +137.

Also fixes the preview table's "missing a required input" marker, which
flagged every projection-configured participant because a generated Elo or
rating is deliberately hidden from getParticipantSimulatorInputs. It now
consults the resolved values, so it agrees with readiness.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CQSEmmojmqmGdJttgzqCWK
2026-08-29 05:31:15 +00:00

99 lines
3.7 KiB
TypeScript

import { describe, expect, it } from "vitest";
import {
parseBaseEloPriorityChoice,
projectionMethodMetadata,
resolvedInputMethodLabel,
} from "../admin.sports-seasons.$id.simulator.helpers";
import { DEFAULT_BASE_ELO_PRIORITY } from "~/services/simulations/input-policy";
describe("projectionMethodMetadata", () => {
it("flags a row that supplies projected wins and no Elo", () => {
expect(projectionMethodMetadata(undefined, 95, undefined)).toEqual({
sourceEloMethod: "projectedWins",
});
});
it("flags a row that supplies projected table points and no Elo", () => {
expect(projectionMethodMetadata(undefined, undefined, 76.5)).toEqual({
sourceEloMethod: "projectedTablePoints",
});
});
it("leaves metadata alone when the row supplies an explicit Elo", () => {
// An explicit Elo is a direct entry and must stay trusted, even alongside a
// projection — the upsert then clears any stale generated flag.
expect(projectionMethodMetadata(1600, 95, undefined)).toBeUndefined();
});
it("leaves metadata alone for a row with neither", () => {
expect(projectionMethodMetadata(undefined, undefined, undefined)).toBeUndefined();
});
it("prefers wins over table points when a row somehow carries both", () => {
expect(projectionMethodMetadata(undefined, 95, 76.5)).toEqual({
sourceEloMethod: "projectedWins",
});
});
});
describe("parseBaseEloPriorityChoice", () => {
it("puts projections ahead of raw Elo", () => {
expect(parseBaseEloPriorityChoice("projectionsFirst", DEFAULT_BASE_ELO_PRIORITY)).toEqual([
"projectedWins",
"projectedTablePoints",
"sourceElo",
]);
});
it("puts raw Elo first for eloFirst", () => {
expect(parseBaseEloPriorityChoice("eloFirst", DEFAULT_BASE_ELO_PRIORITY)).toEqual(
DEFAULT_BASE_ELO_PRIORITY
);
});
it("keeps the stored ordering when the select was not on the form", () => {
// Simulators with no projection alternative never render the control; saving
// other config must not rewrite their ordering.
const custom: typeof DEFAULT_BASE_ELO_PRIORITY = ["projectedWins", "sourceElo"];
expect(parseBaseEloPriorityChoice(null, custom)).toEqual(custom);
});
it("preserves the relative order of the projection keys", () => {
expect(
parseBaseEloPriorityChoice("projectionsFirst", [
"projectedTablePoints",
"sourceElo",
"projectedWins",
])
).toEqual(["projectedTablePoints", "projectedWins", "sourceElo"]);
});
it("round-trips: flipping back restores Elo-first", () => {
const flipped = parseBaseEloPriorityChoice("projectionsFirst", DEFAULT_BASE_ELO_PRIORITY);
expect(parseBaseEloPriorityChoice("eloFirst", flipped)).toEqual(DEFAULT_BASE_ELO_PRIORITY);
});
});
describe("resolvedInputMethodLabel", () => {
it("badges nothing for a directly entered Elo or rating", () => {
expect(resolvedInputMethodLabel("direct")).toBeNull();
});
it("badges both projection methods the same way", () => {
expect(resolvedInputMethodLabel("projectedWins")).toBe("from projections");
expect(resolvedInputMethodLabel("projectedTablePoints")).toBe("from projections");
});
it("distinguishes futures and blended Elo", () => {
expect(resolvedInputMethodLabel("sourceOdds")).toBe("from futures");
expect(resolvedInputMethodLabel("blend")).toBe("blended");
});
it("badges every missing-input strategy as a fallback", () => {
expect(resolvedInputMethodLabel("fallbackElo")).toBe("fallback");
expect(resolvedInputMethodLabel("fallbackRating")).toBe("fallback");
expect(resolvedInputMethodLabel("averageKnown")).toBe("fallback");
expect(resolvedInputMethodLabel("worstKnownMinus")).toBe("fallback");
});
});