A review of this branch found four problems, all downstream of one decision: calling runSportsSeasonSimulation from inside the result path. That function does three jobs — recompute probabilities, recalculate standings, write the daily EV snapshot — and the result path wants only the first. 1. The Discord standings post was silently suppressed on every scored match, for all 13 bracket-aware sports. recalculateAffectedLeagues detects change by snapshotting teamStandings, recalculating, then diffing; changedTeamIds gates the notification. But processMatchResult runs updateProbabilitiesAfterResult first, which now reached the runner's own recalculateStandings. The new totals were therefore already written when the "before" snapshot was taken, the diff came back empty, and the post never fired. previousRank went the same way: recalculateStandings rolls it forward on every call, so the extra one erased rank movement. runSportsSeasonSimulation now takes skipStandingsRecalc / skipSnapshots and the probability updater passes both. The snapshot is skipped because it is a per-day series keyed by snapshotDate — writing it per match result just overwrites the day's row with intra-day values. 2. finalizeQualifyingPoints marks the season completed immediately before calling the updater, and the runner rejects a completed season outright. With the ICM fallback gone that failed every time, stranding anyone still in the unfinished set on permanently stale probabilities — reachable for cs2_major_qualifying_points, the one bracket-aware qualifying-points sport. A completed season is not this branch's case rather than a failure: every placement is final and the floor it protects can no longer be contradicted. shouldRerunSimulator now excludes it and it falls through to ICM as before. The genuine failure modes still leave probabilities alone rather than falling back to the path being replaced. 3. match-sync calls processMatchResult in a per-match loop with no skipSideEffects, so each synced match ran a full Monte Carlo plus EV rewrite, snapshot and standings recalc. processMatchResult gains skipProbabilities — mirroring the option processPlayoffEvent already takes, and narrower than skipSideEffects — which match-sync passes in the loop before refreshing once at the end. Per-match standings and Discord posts are unchanged. 4. A partially-seeded afl_10 bracket now throws from inside the result path. The throw is correct and stays; the concern was that it was silent, which the error surfacing in 2 covers. Two claims from the review did not hold up and were left alone: the batch bracket route already passes skipSideEffects per match and refreshes once after the loop, and autoCompleteRoundIfDone already passes skipProbabilities, so there is no double run per round completion. Tests: the runner honors both skip flags and still writes EVs; the updater asks for probabilities only; a completed season takes the ICM path without erroring; processMatchResult skips the refresh but still announces. The two behavioral ones were confirmed to fail against the previous behavior. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VPxDnSEKVoKx9HFcQ6gmcS
533 lines
19 KiB
TypeScript
533 lines
19 KiB
TypeScript
import { describe, it, expect, beforeEach, vi } from "vitest";
|
|
import {
|
|
updateProbabilitiesAfterResult,
|
|
} from "../probability-updater";
|
|
import * as participantResultModel from "~/models/participant-result";
|
|
import * as participantEVModel from "~/models/participant-expected-value";
|
|
|
|
// Mock the dependencies
|
|
vi.mock("~/models/participant-result");
|
|
vi.mock("~/models/participant-expected-value");
|
|
vi.mock("~/models/simulator");
|
|
vi.mock("~/models/sports-season");
|
|
vi.mock("~/services/simulations/runner");
|
|
vi.mock("~/database/context", () => ({
|
|
database: () => ({
|
|
query: {
|
|
participantExpectedValues: {
|
|
findMany: vi.fn(),
|
|
},
|
|
},
|
|
}),
|
|
}));
|
|
|
|
// vi.mock above is hoisted over the imports, so this is already the mocked function.
|
|
const upsertEV = vi.mocked(participantEVModel.upsertParticipantEV);
|
|
|
|
describe("probability-updater", () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
});
|
|
|
|
describe("updateProbabilitiesAfterResult", () => {
|
|
it("should set 100% probability for finished participants at their placement", async () => {
|
|
// Mock data: One participant finished 1st
|
|
vi.mocked(participantResultModel.findParticipantResultsBySportsSeasonId).mockResolvedValue([
|
|
{
|
|
id: "result-1",
|
|
participantId: "participant-1",
|
|
sportsSeasonId: "season-1",
|
|
finalPosition: 1,
|
|
isPartialScore: false,
|
|
qualifyingPoints: null,
|
|
notes: null,
|
|
createdAt: new Date(),
|
|
updatedAt: new Date(),
|
|
participant: null,
|
|
},
|
|
]);
|
|
|
|
vi.mocked(participantEVModel.getAllParticipantEVsForSeason).mockResolvedValue([
|
|
{
|
|
id: "ev-1",
|
|
participantId: "participant-1",
|
|
sportsSeasonId: "season-1",
|
|
probFirst: "0.1500",
|
|
probSecond: "0.1300",
|
|
probThird: "0.1200",
|
|
probFourth: "0.1100",
|
|
probFifth: "0.1000",
|
|
probSixth: "0.0900",
|
|
probSeventh: "0.0800",
|
|
probEighth: "0.0700",
|
|
expectedValue: "50.00",
|
|
source: "futures_odds",
|
|
sourceOdds: null,
|
|
calculatedAt: new Date(),
|
|
updatedAt: new Date(),
|
|
},
|
|
]);
|
|
|
|
const upsertSpy = vi.mocked(participantEVModel.upsertParticipantEV).mockResolvedValue({
|
|
id: "ev-1",
|
|
participantId: "participant-1",
|
|
sportsSeasonId: "season-1",
|
|
probFirst: "1.0000",
|
|
probSecond: "0.0000",
|
|
probThird: "0.0000",
|
|
probFourth: "0.0000",
|
|
probFifth: "0.0000",
|
|
probSixth: "0.0000",
|
|
probSeventh: "0.0000",
|
|
probEighth: "0.0000",
|
|
expectedValue: "100.00",
|
|
source: "manual",
|
|
sourceOdds: null,
|
|
calculatedAt: new Date(),
|
|
updatedAt: new Date(),
|
|
});
|
|
|
|
const result = await updateProbabilitiesAfterResult("season-1", false);
|
|
|
|
expect(result.finishedParticipants).toBe(1);
|
|
expect(result.updated).toBe(1);
|
|
expect(result.errors).toHaveLength(0);
|
|
|
|
// Verify upsert was called with correct probabilities
|
|
expect(upsertSpy).toHaveBeenCalled();
|
|
const callArgs = upsertSpy.mock.calls[0][0];
|
|
expect(callArgs.participantId).toBe("participant-1");
|
|
expect(callArgs.sportsSeasonId).toBe("season-1");
|
|
expect(callArgs.probabilities.probFirst).toBe(1);
|
|
expect(callArgs.probabilities.probSecond).toBe(0);
|
|
expect(callArgs.source).toBe("manual");
|
|
});
|
|
|
|
it("should handle multiple finished participants", async () => {
|
|
vi.mocked(participantResultModel.findParticipantResultsBySportsSeasonId).mockResolvedValue([
|
|
{
|
|
id: "result-1",
|
|
participantId: "participant-1",
|
|
sportsSeasonId: "season-1",
|
|
finalPosition: 1,
|
|
isPartialScore: false,
|
|
qualifyingPoints: null,
|
|
notes: null,
|
|
createdAt: new Date(),
|
|
updatedAt: new Date(),
|
|
participant: null,
|
|
},
|
|
{
|
|
id: "result-2",
|
|
participantId: "participant-2",
|
|
sportsSeasonId: "season-1",
|
|
finalPosition: 2,
|
|
isPartialScore: false,
|
|
qualifyingPoints: null,
|
|
notes: null,
|
|
createdAt: new Date(),
|
|
updatedAt: new Date(),
|
|
participant: null,
|
|
},
|
|
]);
|
|
|
|
vi.mocked(participantEVModel.getAllParticipantEVsForSeason).mockResolvedValue([
|
|
{
|
|
id: "ev-1",
|
|
participantId: "participant-1",
|
|
sportsSeasonId: "season-1",
|
|
probFirst: "0.2000",
|
|
probSecond: "0.1500",
|
|
probThird: "0.1000",
|
|
probFourth: "0.0800",
|
|
probFifth: "0.0700",
|
|
probSixth: "0.0600",
|
|
probSeventh: "0.0500",
|
|
probEighth: "0.0400",
|
|
expectedValue: "60.00",
|
|
source: "futures_odds",
|
|
sourceOdds: null,
|
|
calculatedAt: new Date(),
|
|
updatedAt: new Date(),
|
|
},
|
|
{
|
|
id: "ev-2",
|
|
participantId: "participant-2",
|
|
sportsSeasonId: "season-1",
|
|
probFirst: "0.1500",
|
|
probSecond: "0.1500",
|
|
probThird: "0.1200",
|
|
probFourth: "0.1000",
|
|
probFifth: "0.0900",
|
|
probSixth: "0.0800",
|
|
probSeventh: "0.0700",
|
|
probEighth: "0.0600",
|
|
expectedValue: "55.00",
|
|
source: "futures_odds",
|
|
sourceOdds: null,
|
|
calculatedAt: new Date(),
|
|
updatedAt: new Date(),
|
|
},
|
|
]);
|
|
|
|
const upsertSpy = vi.mocked(participantEVModel.upsertParticipantEV).mockResolvedValue({} as any);
|
|
|
|
const result = await updateProbabilitiesAfterResult("season-1", false);
|
|
|
|
expect(result.finishedParticipants).toBe(2);
|
|
expect(result.updated).toBe(2);
|
|
expect(result.errors).toHaveLength(0);
|
|
expect(upsertSpy).toHaveBeenCalledTimes(2);
|
|
});
|
|
|
|
it("should handle empty results", async () => {
|
|
vi.mocked(participantResultModel.findParticipantResultsBySportsSeasonId).mockResolvedValue([]);
|
|
vi.mocked(participantEVModel.getAllParticipantEVsForSeason).mockResolvedValue([]);
|
|
|
|
const result = await updateProbabilitiesAfterResult("season-1");
|
|
|
|
expect(result.finishedParticipants).toBe(0);
|
|
expect(result.unfishedParticipants).toBe(0);
|
|
expect(result.updated).toBe(0);
|
|
expect(result.errors).toHaveLength(0);
|
|
});
|
|
|
|
it("should handle results without final position", async () => {
|
|
vi.mocked(participantResultModel.findParticipantResultsBySportsSeasonId).mockResolvedValue([
|
|
{
|
|
id: "result-1",
|
|
participantId: "participant-1",
|
|
sportsSeasonId: "season-1",
|
|
finalPosition: null, // No position set yet
|
|
isPartialScore: false,
|
|
qualifyingPoints: "50.00",
|
|
notes: null,
|
|
createdAt: new Date(),
|
|
updatedAt: new Date(),
|
|
participant: null,
|
|
},
|
|
]);
|
|
|
|
vi.mocked(participantEVModel.getAllParticipantEVsForSeason).mockResolvedValue([]);
|
|
|
|
const result = await updateProbabilitiesAfterResult("season-1");
|
|
|
|
expect(result.finishedParticipants).toBe(0);
|
|
expect(result.updated).toBe(0);
|
|
});
|
|
|
|
it("should set all probabilities to 0% for eliminated participants (finalPosition = 0)", async () => {
|
|
// Mock: Participant eliminated (didn't make playoffs)
|
|
vi.mocked(participantResultModel.findParticipantResultsBySportsSeasonId).mockResolvedValue([
|
|
{
|
|
id: "result-1",
|
|
participantId: "participant-1",
|
|
sportsSeasonId: "season-1",
|
|
finalPosition: 0, // Eliminated
|
|
isPartialScore: false,
|
|
qualifyingPoints: null,
|
|
notes: null,
|
|
createdAt: new Date(),
|
|
updatedAt: new Date(),
|
|
participant: null,
|
|
},
|
|
]);
|
|
|
|
vi.mocked(participantEVModel.getAllParticipantEVsForSeason).mockResolvedValue([
|
|
{
|
|
id: "ev-1",
|
|
participantId: "participant-1",
|
|
sportsSeasonId: "season-1",
|
|
probFirst: "0.0500",
|
|
probSecond: "0.0800",
|
|
probThird: "0.1200",
|
|
probFourth: "0.1500",
|
|
probFifth: "0.1600",
|
|
probSixth: "0.1500",
|
|
probSeventh: "0.1400",
|
|
probEighth: "0.1500",
|
|
expectedValue: "20.00",
|
|
source: "futures_odds",
|
|
sourceOdds: null,
|
|
calculatedAt: new Date(),
|
|
updatedAt: new Date(),
|
|
},
|
|
]);
|
|
|
|
const upsertSpy = vi.mocked(participantEVModel.upsertParticipantEV).mockResolvedValue({} as any);
|
|
|
|
const result = await updateProbabilitiesAfterResult("season-1", false);
|
|
|
|
expect(result.finishedParticipants).toBe(1);
|
|
expect(result.updated).toBe(1);
|
|
|
|
// Verify all probabilities set to 0
|
|
const callArgs = upsertSpy.mock.calls[0][0];
|
|
expect(callArgs.probabilities.probFirst).toBe(0);
|
|
expect(callArgs.probabilities.probSecond).toBe(0);
|
|
expect(callArgs.probabilities.probThird).toBe(0);
|
|
expect(callArgs.probabilities.probFourth).toBe(0);
|
|
expect(callArgs.probabilities.probFifth).toBe(0);
|
|
expect(callArgs.probabilities.probSixth).toBe(0);
|
|
expect(callArgs.probabilities.probSeventh).toBe(0);
|
|
expect(callArgs.probabilities.probEighth).toBe(0);
|
|
});
|
|
|
|
it("does NOT treat a provisional floor as finished — the team is still playing", async () => {
|
|
// An AFL top-4 seed banks a provisional 5th-6th floor at seeding. Pinning them
|
|
// to 100% at 5th would erase their championship odds before they have played.
|
|
vi.mocked(participantResultModel.findParticipantResultsBySportsSeasonId).mockResolvedValue([
|
|
{
|
|
id: "result-1",
|
|
participantId: "participant-1",
|
|
sportsSeasonId: "season-1",
|
|
finalPosition: 5,
|
|
isPartialScore: true,
|
|
qualifyingPoints: null,
|
|
notes: null,
|
|
createdAt: new Date(),
|
|
updatedAt: new Date(),
|
|
participant: null,
|
|
},
|
|
] as never);
|
|
|
|
vi.mocked(participantEVModel.getAllParticipantEVsForSeason).mockResolvedValue([]);
|
|
const upsertSpy = vi.mocked(participantEVModel.upsertParticipantEV).mockResolvedValue({} as never);
|
|
|
|
const result = await updateProbabilitiesAfterResult("season-1", false);
|
|
|
|
expect(result.finishedParticipants).toBe(0);
|
|
expect(upsertSpy).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("still finalizes a 0-position elimination — those rows are not partial", async () => {
|
|
vi.mocked(participantResultModel.findParticipantResultsBySportsSeasonId).mockResolvedValue([
|
|
{
|
|
id: "result-1",
|
|
participantId: "participant-1",
|
|
sportsSeasonId: "season-1",
|
|
finalPosition: 0,
|
|
isPartialScore: false,
|
|
qualifyingPoints: null,
|
|
notes: null,
|
|
createdAt: new Date(),
|
|
updatedAt: new Date(),
|
|
participant: null,
|
|
},
|
|
] as never);
|
|
|
|
vi.mocked(participantEVModel.getAllParticipantEVsForSeason).mockResolvedValue([]);
|
|
const upsertSpy = vi.mocked(participantEVModel.upsertParticipantEV).mockResolvedValue({} as never);
|
|
|
|
const result = await updateProbabilitiesAfterResult("season-1", false);
|
|
|
|
expect(result.finishedParticipants).toBe(1);
|
|
expect(upsertSpy.mock.calls[0][0].probabilities.probFirst).toBe(0);
|
|
});
|
|
});
|
|
});
|
|
|
|
// ─── Bracket-aware simulator seasons ──────────────────────────────────────────
|
|
//
|
|
// The ICM branch re-derives a whole distribution from P(1st) alone and knows nothing about
|
|
// who is playing whom or what has already been decided, so it cannot see the placement floors
|
|
// an afl_10 seeding or a non-scoring-round win has already banked — it will happily value a
|
|
// team below points the league has paid out. Whenever the season has a simulator that reads
|
|
// its bracket, that simulator is the better answer and is re-run instead. Only a season whose
|
|
// simulator is bracket-blind (or has none) still goes through ICM.
|
|
|
|
const evRow = (participantId: string, source: string) => ({
|
|
id: `ev-${participantId}`,
|
|
participantId,
|
|
sportsSeasonId: "season-1",
|
|
probFirst: "0.1000",
|
|
probSecond: "0.1000",
|
|
probThird: "0.1000",
|
|
probFourth: "0.1000",
|
|
probFifth: "0.1000",
|
|
probSixth: "0.1000",
|
|
probSeventh: "0.1000",
|
|
probEighth: "0.1000",
|
|
expectedValue: "34.00",
|
|
source,
|
|
sourceOdds: null,
|
|
calculatedAt: new Date(),
|
|
updatedAt: new Date(),
|
|
});
|
|
|
|
const finishedResult = (participantId: string, finalPosition: number) => ({
|
|
id: `result-${participantId}`,
|
|
participantId,
|
|
sportsSeasonId: "season-1",
|
|
finalPosition,
|
|
isPartialScore: false,
|
|
qualifyingPoints: null,
|
|
notes: null,
|
|
createdAt: new Date(),
|
|
updatedAt: new Date(),
|
|
participant: null,
|
|
});
|
|
|
|
describe("updateProbabilitiesAfterResult — simulator-backed seasons", () => {
|
|
/** Wire up a season: which teams are done, what wrote the EVs, which simulator it has. */
|
|
async function setup(opts: {
|
|
evSource: string;
|
|
simulatorType: string | null;
|
|
results?: ReturnType<typeof finishedResult>[];
|
|
seasonStatus?: string;
|
|
}) {
|
|
const simulatorModel = await import("~/models/simulator");
|
|
const sportsSeasonModel = await import("~/models/sports-season");
|
|
const runner = await import("~/services/simulations/runner");
|
|
|
|
vi.mocked(sportsSeasonModel.findSportsSeasonById).mockResolvedValue({
|
|
id: "season-1",
|
|
status: opts.seasonStatus ?? "active",
|
|
} as never);
|
|
|
|
vi.mocked(participantResultModel.findParticipantResultsBySportsSeasonId).mockResolvedValue(
|
|
opts.results ?? []
|
|
);
|
|
vi.mocked(participantEVModel.getAllParticipantEVsForSeason).mockResolvedValue([
|
|
evRow("alive-1", opts.evSource),
|
|
evRow("alive-2", opts.evSource),
|
|
] as never);
|
|
vi.mocked(participantEVModel.upsertParticipantEV).mockResolvedValue({} as never);
|
|
vi.mocked(simulatorModel.getSportsSeasonSimulatorConfig).mockResolvedValue(
|
|
opts.simulatorType ? ({ simulatorType: opts.simulatorType, config: {} } as never) : null
|
|
);
|
|
const runSim = vi.mocked(runner.runSportsSeasonSimulation);
|
|
runSim.mockResolvedValue({} as never);
|
|
|
|
return { runner, runSim };
|
|
}
|
|
|
|
/** The ICM branch is the only thing that writes unfinished rows with this source. */
|
|
const icmWrites = () =>
|
|
upsertEV.mock.calls.filter(([arg]) => arg.source === "futures_odds");
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
});
|
|
|
|
it("re-runs a bracket-aware simulator instead of recalculating ICM", async () => {
|
|
const { runner } = await setup({ evSource: "elo_simulation", simulatorType: "afl_bracket" });
|
|
|
|
const result = await updateProbabilitiesAfterResult("season-1", true);
|
|
|
|
expect(runner.runSportsSeasonSimulation).toHaveBeenCalledWith("season-1", expect.anything());
|
|
expect(icmWrites()).toHaveLength(0);
|
|
expect(result.errors).toEqual([]);
|
|
});
|
|
|
|
it("asks the run for probabilities only, leaving standings and snapshots to the caller", async () => {
|
|
// recalculateAffectedLeagues detects change by diffing teamStandings across its own
|
|
// recalculation, and that diff gates the Discord standings post. A recalculation in here
|
|
// runs before it takes its "before" snapshot, so the diff comes back empty and the post is
|
|
// silently dropped — and previousRank gets rolled forward twice, erasing rank movement.
|
|
const { runSim } = await setup({ evSource: "elo_simulation", simulatorType: "afl_bracket" });
|
|
|
|
await updateProbabilitiesAfterResult("season-1", true);
|
|
|
|
expect(runSim).toHaveBeenCalledWith("season-1", {
|
|
skipStandingsRecalc: true,
|
|
skipSnapshots: true,
|
|
});
|
|
});
|
|
|
|
it("falls through to ICM on a completed season rather than failing every time", async () => {
|
|
// finalizeQualifyingPoints marks the season completed immediately before calling here, and
|
|
// runSportsSeasonSimulation rejects a completed season outright. Treating that as a failure
|
|
// would strand anyone still unfinished on stale probabilities forever.
|
|
const { runner } = await setup({
|
|
evSource: "elo_simulation",
|
|
simulatorType: "cs2_major_qualifying_points",
|
|
seasonStatus: "completed",
|
|
});
|
|
|
|
const result = await updateProbabilitiesAfterResult("season-1", true);
|
|
|
|
expect(runner.runSportsSeasonSimulation).not.toHaveBeenCalled();
|
|
expect(icmWrites().length).toBeGreaterThan(0);
|
|
expect(result.errors).toEqual([]);
|
|
});
|
|
|
|
it("still pins finished participants before re-running the simulator", async () => {
|
|
const { runner } = await setup({
|
|
evSource: "elo_simulation",
|
|
simulatorType: "afl_bracket",
|
|
results: [finishedResult("done-1", 2)],
|
|
});
|
|
|
|
await updateProbabilitiesAfterResult("season-1", true);
|
|
|
|
const pinned = upsertEV.mock.calls.find(([arg]) => arg.participantId === "done-1");
|
|
expect(pinned?.[0].probabilities.probSecond).toBe(1.0);
|
|
expect(runner.runSportsSeasonSimulation).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it("writes a finalized pin after the re-run, so the pin wins over the simulation", async () => {
|
|
// runSportsSeasonSimulation rewrites every participant in the season, finalized ones
|
|
// included. A finalized placement is a fact, not a projection, so it has to land last.
|
|
const { runSim } = await setup({
|
|
evSource: "elo_simulation",
|
|
simulatorType: "afl_bracket",
|
|
results: [finishedResult("done-1", 0)],
|
|
});
|
|
|
|
await updateProbabilitiesAfterResult("season-1", true);
|
|
|
|
const pinIndex = upsertEV.mock.calls.findIndex(([arg]) => arg.participantId === "done-1");
|
|
expect(pinIndex).toBeGreaterThanOrEqual(0);
|
|
expect(upsertEV.mock.invocationCallOrder[pinIndex]).toBeGreaterThan(
|
|
runSim.mock.invocationCallOrder[0]
|
|
);
|
|
});
|
|
|
|
it("leaves probabilities alone, and does not fall back to ICM, when the re-run fails", async () => {
|
|
const { runner } = await setup({ evSource: "elo_simulation", simulatorType: "afl_bracket" });
|
|
vi.mocked(runner.runSportsSeasonSimulation).mockRejectedValue(
|
|
new Error("A simulation is already running for this sports season.")
|
|
);
|
|
|
|
const result = await updateProbabilitiesAfterResult("season-1", true);
|
|
|
|
expect(icmWrites()).toHaveLength(0);
|
|
expect(result.errors).toHaveLength(1);
|
|
expect(result.errors[0]).toMatch(/Failed to re-run simulator/);
|
|
});
|
|
|
|
it("re-runs the simulator whatever wrote the EVs originally", async () => {
|
|
// The alternative is not leaving them alone — ICM would overwrite them either way — so
|
|
// futures-odds EVs are no reason to prefer the bracket-blind overwrite.
|
|
const { runner } = await setup({ evSource: "futures_odds", simulatorType: "afl_bracket" });
|
|
|
|
await updateProbabilitiesAfterResult("season-1", true);
|
|
|
|
expect(runner.runSportsSeasonSimulation).toHaveBeenCalledWith("season-1", expect.anything());
|
|
expect(icmWrites()).toHaveLength(0);
|
|
});
|
|
|
|
it("keeps the ICM path for a bracket-blind simulator", async () => {
|
|
// ncaa_football_bracket declares a "bracket" setup section but never reads playoff_matches,
|
|
// so re-running it would re-draw the field and hand equity back to eliminated teams.
|
|
const { runner } = await setup({
|
|
evSource: "elo_simulation",
|
|
simulatorType: "ncaa_football_bracket",
|
|
});
|
|
|
|
await updateProbabilitiesAfterResult("season-1", true);
|
|
|
|
expect(runner.runSportsSeasonSimulation).not.toHaveBeenCalled();
|
|
expect(icmWrites().length).toBeGreaterThan(0);
|
|
});
|
|
|
|
it("keeps the ICM path when the season has no simulator configured", async () => {
|
|
const { runner } = await setup({ evSource: "elo_simulation", simulatorType: null });
|
|
|
|
await updateProbabilitiesAfterResult("season-1", true);
|
|
|
|
expect(runner.runSportsSeasonSimulation).not.toHaveBeenCalled();
|
|
expect(icmWrites().length).toBeGreaterThan(0);
|
|
});
|
|
});
|