Fix intermittent test timeouts in simulator-inputs and llws-simulator
All checks were successful
🚀 Deploy / 🧪 Test (pull_request) Successful in 2m27s
🚀 Deploy / ʦ TypeScript (pull_request) Successful in 1m16s
🚀 Deploy / 🔍 Lint (pull_request) Successful in 48s
🚀 Deploy / 🐳 Build (pull_request) Has been skipped
🚀 Deploy / 🚀 Deploy (pull_request) Has been skipped

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 <noreply@anthropic.com>
This commit is contained in:
Chris Parsons 2026-05-31 20:20:24 -07:00
parent 880b7faa6b
commit 3ebdd16b60
3 changed files with 30 additions and 29 deletions

View file

@ -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" },

View file

@ -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/);
});
});
});

View file

@ -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<SimulationResult[]> {
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,