Fix hook/test timeouts from dynamic imports in beforeEach/test bodies
Some checks failed
🚀 Deploy / 🧪 Test (pull_request) Failing after 3m8s
🚀 Deploy / ʦ🔍 Typecheck & Lint (pull_request) Successful in 2m8s
🚀 Deploy / 🐳 Build (pull_request) Has been skipped
🚀 Deploy / 🚀 Deploy (pull_request) Has been skipped

Two patterns causing CI timeouts:

1. admin.sports-seasons.$id.test.ts: vi.resetModules() + multiple
   await import() in beforeEach forced full module re-evaluation on
   every test, exceeding the 10s hook timeout.

2. participant-expected-value.test.ts: await import() of the module
   under test inside test bodies caused the first import to run within
   the 5s test window. Also deleted 13 expect(true).toBe(true) stubs
   and used vi.hoisted() to make mock factories work with static imports.

Both files now use static top-level imports.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Chris Parsons 2026-06-09 22:22:21 -07:00
parent 02daff1d1f
commit 8ca1c9cf94

View file

@ -1,24 +1,14 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import type { ProbabilityDistribution, ScoringRules } from "~/services/ev-calculator";
import { calculateReplacementLevel, calculateVORP } from "~/services/ev-calculator";
import { syncVorpForSeason } from "../participant-expected-value";
/**
* Participant Expected Value Model Tests
* Phase 5.1.3: Probability Storage Model Functions
*
* These are documentation tests that describe the expected behavior of the model functions.
* The core EV calculation logic is thoroughly tested in app/services/__tests__/ev-calculator.test.ts (20 tests).
* The model layer provides database persistence for probabilities and EVs.
* Full integration tests are in the E2E test suite.
*/
// Mock database context
const mockUpdate = vi.fn();
const mockSet = vi.fn();
const _mockWhere = vi.fn();
const mockDb = {
update: mockUpdate,
select: vi.fn(),
};
const { mockUpdate, mockSet, mockDb, mockSqlFn } = vi.hoisted(() => {
const update = vi.fn();
const set = vi.fn();
const sqlFn = Object.assign(vi.fn(() => ({})), { join: vi.fn(() => ({})) });
const db = { update, select: vi.fn() };
return { mockUpdate: update, mockSet: set, mockDb: db, mockSqlFn: sqlFn };
});
vi.mock("~/database/context", () => ({
database: () => mockDb,
@ -32,10 +22,6 @@ vi.mock("~/database/schema", () => ({
},
}));
const mockSqlFn = Object.assign(vi.fn(() => ({})), {
join: vi.fn(() => ({})),
});
vi.mock("drizzle-orm", () => ({
eq: vi.fn((field, value) => ({ field, value })),
and: vi.fn((...args) => ({ and: args })),
@ -43,158 +29,23 @@ vi.mock("drizzle-orm", () => ({
sql: mockSqlFn,
}));
describe("participant-expected-value model", () => {
const _defaultScoring: ScoringRules = {
pointsFor1st: 100,
pointsFor2nd: 70,
pointsFor3rd: 50,
pointsFor4th: 40,
pointsFor5th: 25,
pointsFor6th: 25,
pointsFor7th: 15,
pointsFor8th: 15,
};
const _validProbabilities: ProbabilityDistribution = {
probFirst: 20,
probSecond: 20,
probThird: 15,
probFourth: 15,
probFifth: 10,
probSixth: 10,
probSeventh: 5,
probEighth: 5,
};
describe("upsertParticipantEV", () => {
it("should create new participant EV with calculated expected value", () => {
// Function validates probabilities sum to 100%, calculates EV, and inserts/updates database record
// Expected EV for validProbabilities with defaultScoring: 54 points
// EV = 20% × 100 + 20% × 70 + 15% × 50 + 15% × 40 + 10% × 25 + 10% × 25 + 5% × 15 + 5% × 15
// = 20 + 14 + 7.5 + 6 + 2.5 + 2.5 + 0.75 + 0.75 = 54
expect(true).toBe(true);
});
it("should update existing participant EV", () => {
// Function checks for existing record by (participantId, seasonId) and updates if found
expect(true).toBe(true);
});
it("should reject invalid probabilities that don't sum to 100%", () => {
// Function throws error if validateProbabilities returns false
// Tolerance is ±0.1% by default
expect(true).toBe(true);
});
it("should default source to 'manual' if not provided", () => {
// Function sets source = 'manual' when not specified
expect(true).toBe(true);
});
});
describe("upsertParticipantEVWithNormalization", () => {
it("should normalize probabilities before upserting", () => {
// Function calls normalizeProbabilities to scale probabilities to sum to 100%
// Then calls upsertParticipantEV with normalized values
expect(true).toBe(true);
});
});
describe("getParticipantEV", () => {
it("should retrieve participant EV by participantId and seasonId", () => {
// Function returns ParticipantEV record or null if not found
expect(true).toBe(true);
});
});
describe("getAllParticipantEVsForSeason", () => {
it("should retrieve all EVs for a season", () => {
// Function returns array of ParticipantEV records for all participants in a season
expect(true).toBe(true);
});
});
describe("deleteParticipantEV", () => {
it("should delete participant EV record", () => {
// Function deletes record matching (participantId, seasonId)
expect(true).toBe(true);
});
});
describe("batchUpsertParticipantEVs", () => {
it("should upsert multiple participants in batches", () => {
// Function processes inputs in batches of 50 to avoid overwhelming database
// Returns array of all upserted ParticipantEV records
expect(true).toBe(true);
});
});
describe("toProbabilityDistribution", () => {
it("should convert database record to ProbabilityDistribution", () => {
// Function converts string fields (probFirst, probSecond, etc.) to numbers
// Returns ProbabilityDistribution object
expect(true).toBe(true);
});
});
describe("recalculateEV", () => {
it("should recalculate EV with new scoring rules", () => {
// Function retrieves existing probabilities and recalculates EV with new scoring
// Keeps probabilities unchanged, only updates expectedValue field
expect(true).toBe(true);
});
it("should return null if participant EV doesn't exist", () => {
// Function returns null when no record is found
expect(true).toBe(true);
});
});
describe("recalculateAllEVsForSeason", () => {
it("should recalculate all EVs for a season", () => {
// Function retrieves all participant EVs for season
// Calls recalculateEV for each participant
// Returns count of participants updated
expect(true).toBe(true);
});
});
describe("syncVorpForSeason", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("should calculate correct VORP values for 14 participants with EVs 100 down to 35 (step 5)", async () => {
it("calculates correct VORP values for 14 participants with EVs 100 down to 35 (step 5)", () => {
// 14 participants: EVs = 100, 95, 90, 85, 80, 75, 70, 65, 60, 55, 50, 45, 40, 35
// Sorted descending (already sorted)
// Replacement level = avg of positions 12-14 (0-indexed 11-13) = avg(45, 40, 35) = 40
// VORP(100) = 60, VORP(35) = -5
const { calculateReplacementLevel, calculateVORP } = await import("~/services/ev-calculator");
// replacement level = avg of positions 12-14 (0-indexed 11-13) = avg(45, 40, 35) = 40
const evValues = Array.from({ length: 14 }, (_, i) => 100 - i * 5);
// [100, 95, 90, 85, 80, 75, 70, 65, 60, 55, 50, 45, 40, 35]
const replacementLevel = calculateReplacementLevel(evValues);
expect(replacementLevel).toBe(40); // avg(45, 40, 35) = 40
const vorpFirst = calculateVORP(100, replacementLevel);
expect(vorpFirst).toBe(60);
const vorpLast = calculateVORP(35, replacementLevel);
expect(vorpLast).toBe(-5);
expect(replacementLevel).toBe(40);
expect(calculateVORP(100, replacementLevel)).toBe(60);
expect(calculateVORP(35, replacementLevel)).toBe(-5);
});
it("should return early when no EVs exist for the season", async () => {
// Re-mock getAllParticipantEVsForSeason to return empty array
// The function should do nothing and return without calling db.update
const { syncVorpForSeason } = await import("../participant-expected-value");
// Patch the module's getAllParticipantEVsForSeason to return []
// Since we can't easily spy on module-internal calls, we verify via db mock:
// If 0 EVs returned, db.update should not be called
// Setup: db.select chain for getAllParticipantEVsForSeason returns []
it("returns early when no EVs exist for the season", async () => {
const mockSelectChain = {
from: vi.fn().mockReturnThis(),
where: vi.fn().mockResolvedValue([]),
@ -203,18 +54,12 @@ describe("participant-expected-value model", () => {
await syncVorpForSeason("season-empty");
// db.update should NOT have been called (no participants to update)
expect(mockUpdate).not.toHaveBeenCalled();
});
it("should call db.update with correct vorpValue for each participant", async () => {
const { syncVorpForSeason } = await import("../participant-expected-value");
// 3 participants with EVs: 100, 70, 40
// sorted: [100, 70, 40]
// replacement level = avg of positions 12-14, but only 3 participants
// startIdx = min(11, 2) = 2, endIdx = min(13, 2) = 2 → slice = [40]
// replacementLevel = 40
it("calls db.update with correct vorpValue for each participant", async () => {
// 3 participants: EVs 100, 70, 40
// replacement level = avg of positions 12-14, clamped to [40] → 40
// VORP: 100→60, 70→30, 40→0
const mockEvRecords = [
{ participantId: "p1", expectedValue: "100", sportsSeasonId: "season-1" },
@ -234,14 +79,10 @@ describe("participant-expected-value model", () => {
await syncVorpForSeason("season-1");
// Bulk update: db.update is called once for all participants
expect(mockUpdate).toHaveBeenCalledTimes(1);
// set() is called once with a CASE expression for vorpValue
expect(mockSet).toHaveBeenCalledTimes(1);
const setArg = mockSet.mock.calls[0][0];
expect(setArg).toHaveProperty("vorpValue");
expect(setArg).toHaveProperty("updatedAt");
});
});
});