From 3ebdd16b60cc913fca3b906520e86c94c312e3e0 Mon Sep 17 00:00:00 2001 From: Chris Parsons Date: Sun, 31 May 2026 20:20:24 -0700 Subject: [PATCH] Fix intermittent test timeouts in simulator-inputs and llws-simulator simulator-inputs: move dynamic import to top level so heavy module initialization happens during file load (no timeout) instead of inside the test body. llws-simulator: make iteration count injectable via constructor (default 50_000 for production, tests pass 1_000). Cuts test suite from ~4s per test to <150ms for all 21 tests combined. Co-Authored-By: Claude Sonnet 4.6 --- app/models/__tests__/simulator-inputs.test.ts | 3 +- .../__tests__/llws-simulator.test.ts | 42 +++++++++---------- app/services/simulations/llws-simulator.ts | 14 ++++--- 3 files changed, 30 insertions(+), 29 deletions(-) diff --git a/app/models/__tests__/simulator-inputs.test.ts b/app/models/__tests__/simulator-inputs.test.ts index ca3b5fd..e333d1f 100644 --- a/app/models/__tests__/simulator-inputs.test.ts +++ b/app/models/__tests__/simulator-inputs.test.ts @@ -1,4 +1,5 @@ import { describe, expect, it, vi, beforeEach } from "vitest"; +import { getParticipantSimulatorInputs } from "../simulator"; const mockDb = vi.hoisted(() => ({ query: { @@ -18,8 +19,6 @@ describe("simulator input model", () => { }); it("does not treat previously generated ratings as direct rating inputs", async () => { - const { getParticipantSimulatorInputs } = await import("../simulator"); - mockDb.query.seasonParticipants.findMany.mockResolvedValue([ { id: "direct-rating" }, { id: "generated-rating" }, diff --git a/app/services/simulations/__tests__/llws-simulator.test.ts b/app/services/simulations/__tests__/llws-simulator.test.ts index e6ccb94..15c236a 100644 --- a/app/services/simulations/__tests__/llws-simulator.test.ts +++ b/app/services/simulations/__tests__/llws-simulator.test.ts @@ -70,13 +70,13 @@ describe("LLWSSimulator", () => { describe("output structure", () => { it("returns one result per participant (20 total)", async () => { setupMockDb(defaultParticipants(), makeEvRows(ALL_IDS)); - const results = await new LLWSSimulator().simulate("season-1"); + const results = await new LLWSSimulator(1_000).simulate("season-1"); expect(results).toHaveLength(20); }); it("every result has source 'llws_monte_carlo'", async () => { setupMockDb(defaultParticipants(), makeEvRows(ALL_IDS)); - const results = await new LLWSSimulator().simulate("season-1"); + const results = await new LLWSSimulator(1_000).simulate("season-1"); for (const r of results) { expect(r.source).toBe("llws_monte_carlo"); } @@ -84,7 +84,7 @@ describe("LLWSSimulator", () => { it("all probability values are between 0 and 1", async () => { setupMockDb(defaultParticipants(), makeEvRows(ALL_IDS)); - const results = await new LLWSSimulator().simulate("season-1"); + const results = await new LLWSSimulator(1_000).simulate("season-1"); for (const r of results) { const p = r.probabilities; for (const v of Object.values(p)) { @@ -100,35 +100,35 @@ describe("LLWSSimulator", () => { describe("probability conservation (one winner per sim)", () => { it("probFirst sums to ~1.0 across all participants", async () => { setupMockDb(defaultParticipants(), makeEvRows(ALL_IDS)); - const results = await new LLWSSimulator().simulate("season-1"); + const results = await new LLWSSimulator(1_000).simulate("season-1"); const total = results.reduce((s, r) => s + r.probabilities.probFirst, 0); expect(total).toBeCloseTo(1.0, 1); }); it("probSecond sums to ~1.0 across all participants", async () => { setupMockDb(defaultParticipants(), makeEvRows(ALL_IDS)); - const results = await new LLWSSimulator().simulate("season-1"); + const results = await new LLWSSimulator(1_000).simulate("season-1"); const total = results.reduce((s, r) => s + r.probabilities.probSecond, 0); expect(total).toBeCloseTo(1.0, 1); }); it("probThird sums to ~1.0 across all participants", async () => { setupMockDb(defaultParticipants(), makeEvRows(ALL_IDS)); - const results = await new LLWSSimulator().simulate("season-1"); + const results = await new LLWSSimulator(1_000).simulate("season-1"); const total = results.reduce((s, r) => s + r.probabilities.probThird, 0); expect(total).toBeCloseTo(1.0, 1); }); it("probFourth sums to ~1.0 across all participants", async () => { setupMockDb(defaultParticipants(), makeEvRows(ALL_IDS)); - const results = await new LLWSSimulator().simulate("season-1"); + const results = await new LLWSSimulator(1_000).simulate("season-1"); const total = results.reduce((s, r) => s + r.probabilities.probFourth, 0); expect(total).toBeCloseTo(1.0, 1); }); it("sum of probFifth across participants equals ~1.0 (4 bracket losers, split evenly)", async () => { setupMockDb(defaultParticipants(), makeEvRows(ALL_IDS)); - const results = await new LLWSSimulator().simulate("season-1"); + const results = await new LLWSSimulator(1_000).simulate("season-1"); // 4 bracket losers per sim, each assigned bracketLoser/(4*N) → sum = 1.0 const total = results.reduce((s, r) => s + r.probabilities.probFifth, 0); expect(total).toBeCloseTo(1.0, 1); @@ -136,7 +136,7 @@ describe("LLWSSimulator", () => { it("probFifth through probEighth are equal for every participant (even bracket-loser split)", async () => { setupMockDb(defaultParticipants(), makeEvRows(ALL_IDS)); - const results = await new LLWSSimulator().simulate("season-1"); + const results = await new LLWSSimulator(1_000).simulate("season-1"); for (const r of results) { const p = r.probabilities; expect(p.probFifth).toBeCloseTo(p.probSixth, 10); @@ -151,7 +151,7 @@ describe("LLWSSimulator", () => { describe("odds-driven win probability", () => { it("strong favourite (us-1) has higher probFirst than a weak team", async () => { setupMockDb(defaultParticipants(), makeEvRows(ALL_IDS, { includeOdds: true })); - const results = await new LLWSSimulator().simulate("season-1"); + const results = await new LLWSSimulator(1_000).simulate("season-1"); const byId = new Map(results.map((r) => [r.participantId, r])); const us1prob = byId.get("us-1")?.probabilities.probFirst ?? 0; const us10prob = byId.get("us-10")?.probabilities.probFirst ?? 0; @@ -160,7 +160,7 @@ describe("LLWSSimulator", () => { it("works when no odds are entered (all 50/50 fallback)", async () => { setupMockDb(defaultParticipants(), makeEvRows(ALL_IDS)); // no odds - const results = await new LLWSSimulator().simulate("season-1"); + const results = await new LLWSSimulator(1_000).simulate("season-1"); const total = results.reduce((s, r) => s + r.probabilities.probFirst, 0); expect(total).toBeCloseTo(1.0, 1); }); @@ -169,7 +169,7 @@ describe("LLWSSimulator", () => { // Equal positive odds (+5000 for every team) → vig-removed prob ≈ 1/20 each. const eqOddsRows = ALL_IDS.map((id) => ({ participantId: id, sourceOdds: 5000 })); setupMockDb(defaultParticipants(), eqOddsRows); - const results = await new LLWSSimulator().simulate("season-1"); + const results = await new LLWSSimulator(1_000).simulate("season-1"); for (const r of results) { // With equal odds and random pools, each team should win ~5% of the time. // Allow a generous band given Monte Carlo variance. @@ -184,7 +184,7 @@ describe("LLWSSimulator", () => { describe("pool assignment modes", () => { it("fixed pools (US:A / US:B / Intl:A / Intl:B) produce valid results", async () => { setupMockDb(defaultParticipants("fixed"), makeEvRows(ALL_IDS)); - const results = await new LLWSSimulator().simulate("season-1"); + const results = await new LLWSSimulator(1_000).simulate("season-1"); expect(results).toHaveLength(20); const total = results.reduce((s, r) => s + r.probabilities.probFirst, 0); expect(total).toBeCloseTo(1.0, 1); @@ -197,7 +197,7 @@ describe("LLWSSimulator", () => { ...INTL_IDS.map((id) => ({ id, name: `Team ${id}`, externalId: "Intl" })), ]; setupMockDb(participants, makeEvRows(ALL_IDS)); - const results = await new LLWSSimulator().simulate("season-1"); + const results = await new LLWSSimulator(1_000).simulate("season-1"); expect(results).toHaveLength(20); const total = results.reduce((s, r) => s + r.probabilities.probFirst, 0); expect(total).toBeCloseTo(1.0, 1); @@ -215,7 +215,7 @@ describe("LLWSSimulator", () => { externalId: i < 10 ? "US" : "Intl", })); setupMockDb(participants, makeEvRows(nineteen)); - await expect(new LLWSSimulator().simulate("season-1")).rejects.toThrow(/exactly 20/); + await expect(new LLWSSimulator(1_000).simulate("season-1")).rejects.toThrow(/exactly 20/); }); it("infers US side from name prefix when externalId is null", async () => { @@ -224,7 +224,7 @@ describe("LLWSSimulator", () => { ...INTL_IDS.map((id) => ({ id, name: `Japan ${id}`, externalId: null })), ]; setupMockDb(participants, makeEvRows(ALL_IDS)); - const results = await new LLWSSimulator().simulate("season-1"); + const results = await new LLWSSimulator(1_000).simulate("season-1"); expect(results).toHaveLength(20); const total = results.reduce((s, r) => s + r.probabilities.probFirst, 0); expect(total).toBeCloseTo(1.0, 1); @@ -236,7 +236,7 @@ describe("LLWSSimulator", () => { ...INTL_IDS.map((id) => ({ id, name: `Team ${id}`, externalId: null })), ]; setupMockDb(participants, makeEvRows(ALL_IDS)); - const results = await new LLWSSimulator().simulate("season-1"); + const results = await new LLWSSimulator(1_000).simulate("season-1"); expect(results).toHaveLength(20); const total = results.reduce((s, r) => s + r.probabilities.probFirst, 0); expect(total).toBeCloseTo(1.0, 1); @@ -249,7 +249,7 @@ describe("LLWSSimulator", () => { { id: "intl-10", name: "Team intl-10", externalId: "CANADA" }, // unrecognized ]; setupMockDb(participants, makeEvRows(ALL_IDS)); - await expect(new LLWSSimulator().simulate("season-1")).rejects.toThrow(/invalid externalId/); + await expect(new LLWSSimulator(1_000).simulate("season-1")).rejects.toThrow(/invalid externalId/); }); it("throws when US team count is not 10", async () => { @@ -259,7 +259,7 @@ describe("LLWSSimulator", () => { ...Array.from({ length: 9 }, (_, i) => ({ id: `intl-${i + 1}`, name: `Team ${i + 1}`, externalId: "Intl" })), ]; setupMockDb(participants, makeEvRows(ALL_IDS)); - await expect(new LLWSSimulator().simulate("season-1")).rejects.toThrow(/10 US teams/); + await expect(new LLWSSimulator(1_000).simulate("season-1")).rejects.toThrow(/10 US teams/); }); it("throws when fixed pools have unequal A/B split", async () => { @@ -270,7 +270,7 @@ describe("LLWSSimulator", () => { ...INTL_IDS.map((id) => ({ id, name: `Team ${id}`, externalId: "Intl" })), ]; setupMockDb(participants, makeEvRows(ALL_IDS)); - await expect(new LLWSSimulator().simulate("season-1")).rejects.toThrow(/exactly 5 teams each/); + await expect(new LLWSSimulator(1_000).simulate("season-1")).rejects.toThrow(/exactly 5 teams each/); }); it("throws when US externalIds mix pool suffixes and bare side", async () => { @@ -281,7 +281,7 @@ describe("LLWSSimulator", () => { ...INTL_IDS.map((id) => ({ id, name: `Team ${id}`, externalId: "Intl" })), ]; setupMockDb(participants, makeEvRows(ALL_IDS)); - await expect(new LLWSSimulator().simulate("season-1")).rejects.toThrow(/mixed externalId formats/); + await expect(new LLWSSimulator(1_000).simulate("season-1")).rejects.toThrow(/mixed externalId formats/); }); }); }); diff --git a/app/services/simulations/llws-simulator.ts b/app/services/simulations/llws-simulator.ts index b6dfcb3..c633b08 100644 --- a/app/services/simulations/llws-simulator.ts +++ b/app/services/simulations/llws-simulator.ts @@ -259,6 +259,8 @@ function determineRandomized(sideTeams: Team[], sideName: string): boolean { // ─── Simulator ──────────────────────────────────────────────────────────────── export class LLWSSimulator implements Simulator { + constructor(private numSimulations = NUM_SIMULATIONS) {} + async simulate(sportsSeasonId: string): Promise { const db = database(); @@ -345,7 +347,7 @@ export class LLWSSimulator implements Simulator { }; // 6. Run Monte Carlo simulations. - for (let s = 0; s < NUM_SIMULATIONS; s++) { + for (let s = 0; s < this.numSimulations; s++) { // Assign pools for this simulation. const [usPoolA, usPoolB] = assignPools(usTeams, usRandomized); const [intlPoolA, intlPoolB] = assignPools(intlTeams, intlRandomized); @@ -376,7 +378,7 @@ export class LLWSSimulator implements Simulator { // 7. Convert counts to probability distributions. // bracketLosers: 4 per sim (2 US + 2 Intl) → split evenly. const bracketLosersPerSim = 4; - const bracketDivisor = bracketLosersPerSim * NUM_SIMULATIONS; + const bracketDivisor = bracketLosersPerSim * this.numSimulations; const zeroCounts: PlacementCounts = { champion: 0, finalist: 0, thirdPlace: 0, fourthPlace: 0, bracketLoser: 0 }; return allIds.map((id) => { @@ -385,10 +387,10 @@ export class LLWSSimulator implements Simulator { return { participantId: id, probabilities: { - probFirst: c.champion / NUM_SIMULATIONS, - probSecond: c.finalist / NUM_SIMULATIONS, - probThird: c.thirdPlace / NUM_SIMULATIONS, - probFourth: c.fourthPlace / NUM_SIMULATIONS, + probFirst: c.champion / this.numSimulations, + probSecond: c.finalist / this.numSimulations, + probThird: c.thirdPlace / this.numSimulations, + probFourth: c.fourthPlace / this.numSimulations, probFifth: bracketProb, probSixth: bracketProb, probSeventh: bracketProb,