/** * Tests for executeAutoPick timer behavior across chess_clock and standard modes. * * chess_clock: auto-picks earn the increment (bank += increment) so a team that * times out doesn't get permanently frozen at 0 on all future turns. * standard: auto-picks reset the bank to exactly draftIncrementTime. */ import { describe, it, expect, vi, beforeEach } from "vitest"; import { executeAutoPick } from "../draft-utils"; // ── Module-level mocks ──────────────────────────────────────────────────────── vi.mock("~/server/socket", () => ({ getSocketIO: vi.fn(), })); vi.mock("~/models/draft-pick", () => ({ getDraftPicksWithSports: vi.fn(), getTeamDraftPicksWithSports: vi.fn(), isParticipantDrafted: vi.fn(), })); vi.mock("~/models/participant", () => ({ getParticipantsForSeasonWithSports: vi.fn(), })); vi.mock("~/models/season-sport", () => ({ getSeasonSportsSimple: vi.fn(), })); vi.mock("~/models/draft-queue", () => ({ getTeamQueue: vi.fn(), getAllQueuesForSeason: vi.fn(), })); vi.mock("~/lib/draft-eligibility", () => ({ calculateDraftEligibility: vi.fn(), })); // database context not needed — we pass db directly to executeAutoPick vi.mock("~/database/context"); import { getDraftPicksWithSports, getTeamDraftPicksWithSports, isParticipantDrafted } from "~/models/draft-pick"; import { getParticipantsForSeasonWithSports } from "~/models/participant"; import { getSeasonSportsSimple } from "~/models/season-sport"; import { getTeamQueue, getAllQueuesForSeason } from "~/models/draft-queue"; import { calculateDraftEligibility } from "~/lib/draft-eligibility"; // ── Fixtures ────────────────────────────────────────────────────────────────── const SEASON_ID = "season-1"; const TEAM_ID = "team-1"; const NEXT_TEAM_ID = "team-2"; const PARTICIPANT_ID = "p-1"; const SPORT_ID = "sport-1"; const mockParticipantForQueue = { id: PARTICIPANT_ID, name: "Test Player", sportsSeason: { sport: { id: SPORT_ID, name: "NFL" } }, }; const mockParticipantFull = { id: PARTICIPANT_ID, name: "Test Player", sportsSeason: { id: "ss-1", sport: { id: SPORT_ID, name: "NFL" }, }, }; const mockDraftPick = { id: "pick-1", seasonId: SEASON_ID, teamId: TEAM_ID, participantId: PARTICIPANT_ID, pickNumber: 1, round: 1, pickInRound: 1, pickedByUserId: "", pickedByType: "auto", }; const mockDraftSlots = [ { id: "slot-1", seasonId: SEASON_ID, teamId: TEAM_ID, draftOrder: 1, team: { id: TEAM_ID, name: "Team 1", seasonId: SEASON_ID, ownerId: "owner-1" }, }, { id: "slot-2", seasonId: SEASON_ID, teamId: NEXT_TEAM_ID, draftOrder: 2, team: { id: NEXT_TEAM_ID, name: "Team 2", seasonId: SEASON_ID, ownerId: "owner-2" }, }, ]; function makeSeason(overrides: Record = {}) { return { id: SEASON_ID, leagueId: "league-1", status: "draft", draftRounds: 3, draftInitialTime: 120, draftIncrementTime: 15, draftTimerMode: "chess_clock", currentPickNumber: 1, draftPaused: false, ...overrides, }; } /** * Build a mock DB for executeAutoPick. * * The `returning` mock is left unconfigured so individual tests can set up * the sequence of values they expect (pick insert, then timer update). */ function makeMockDb(seasonOverrides: Record = {}) { const mockDb: any = { query: { // Race-condition guard — no existing pick at this slot draftPicks: { findFirst: vi.fn().mockResolvedValue(null) }, seasons: { findFirst: vi.fn().mockResolvedValue(makeSeason(seasonOverrides)) }, draftSlots: { findMany: vi.fn().mockResolvedValue(mockDraftSlots) }, participants: { // findMany: used by autoPickForTeam queue path findMany: vi.fn().mockResolvedValue([mockParticipantForQueue]), // findFirst: used by executeAutoPick to fetch full participant details findFirst: vi.fn().mockResolvedValue(mockParticipantFull), }, draftTimers: { // Default: timer is at 0 (expired), which is the normal auto-pick trigger state findFirst: vi.fn().mockResolvedValue({ id: "timer-1", seasonId: SEASON_ID, teamId: TEAM_ID, timeRemaining: 0 }), }, // checkAndTriggerNextAutodraft reads this; returning null prevents any recursion autodraftSettings: { findFirst: vi.fn().mockResolvedValue(null) }, }, insert: vi.fn().mockReturnThis(), values: vi.fn().mockReturnThis(), onConflictDoNothing: vi.fn().mockReturnThis(), returning: vi.fn(), // configured per-test update: vi.fn().mockReturnThis(), set: vi.fn().mockReturnThis(), where: vi.fn().mockReturnThis(), delete: vi.fn().mockReturnThis(), }; return mockDb; } // ── Tests ────────────────────────────────────────────────────────────────────── describe("executeAutoPick — timer mode behavior", () => { let mockSocketIO: any; beforeEach(async () => { vi.clearAllMocks(); mockSocketIO = { to: vi.fn().mockReturnThis(), emit: vi.fn() }; const { getSocketIO } = await import("~/server/socket"); vi.mocked(getSocketIO).mockReturnValue(mockSocketIO); // autoPickForTeam external dependencies — return minimal happy-path values vi.mocked(getDraftPicksWithSports).mockResolvedValue([]); vi.mocked(getTeamDraftPicksWithSports).mockResolvedValue([]); vi.mocked(getParticipantsForSeasonWithSports).mockResolvedValue([]); vi.mocked(getSeasonSportsSimple).mockResolvedValue([]); vi.mocked(calculateDraftEligibility).mockReturnValue({ eligibleSportIds: new Set([SPORT_ID]), ineligibleReasons: {}, } as ReturnType); // Queue has one item so autoPickForTeam selects it without hitting the EV path vi.mocked(getTeamQueue).mockResolvedValue([ { id: "q-1", participantId: PARTICIPANT_ID, queuePosition: 1 } as any, ]); vi.mocked(isParticipantDrafted).mockResolvedValue(false); // pruneIneligibleQueueItems calls getAllQueuesForSeason — return empty Map to skip pruning vi.mocked(getAllQueuesForSeason).mockResolvedValue(new Map() as any); }); // ── chess_clock mode ──────────────────────────────────────────────────────── describe("chess_clock mode", () => { it("emits timer-update with bank + increment after a timer-triggered pick", async () => { const mockDb = makeMockDb({ draftTimerMode: "chess_clock", draftIncrementTime: 15 }); // Timer expired at 0; 0 + 15 = 15 after the increment mockDb.returning .mockResolvedValueOnce([mockDraftPick]) // insert draft pick .mockResolvedValueOnce([{ id: "timer-1", teamId: TEAM_ID, timeRemaining: 15 }]); // update timer await executeAutoPick({ seasonId: SEASON_ID, teamId: TEAM_ID, pickNumber: 1, triggeredBy: "timer", autodraftSettings: null, db: mockDb, }); expect(mockSocketIO.emit).toHaveBeenCalledWith( "timer-update", expect.objectContaining({ teamId: TEAM_ID, timeRemaining: 15 }) ); }); it("writes the timer increment to the DB", async () => { const mockDb = makeMockDb({ draftTimerMode: "chess_clock", draftIncrementTime: 15 }); mockDb.returning .mockResolvedValueOnce([mockDraftPick]) .mockResolvedValueOnce([{ id: "timer-1", teamId: TEAM_ID, timeRemaining: 15 }]); await executeAutoPick({ seasonId: SEASON_ID, teamId: TEAM_ID, pickNumber: 1, triggeredBy: "timer", autodraftSettings: null, db: mockDb, }); expect(mockDb.set).toHaveBeenCalledWith( expect.objectContaining({ timeRemaining: expect.anything(), updatedAt: expect.any(Date) }) ); }); it("applies the configured increment, not a hard-coded default", async () => { // Use a non-default increment of 30 to confirm the value is read from the season const mockDb = makeMockDb({ draftTimerMode: "chess_clock", draftIncrementTime: 30 }); mockDb.returning .mockResolvedValueOnce([mockDraftPick]) .mockResolvedValueOnce([{ id: "timer-1", teamId: TEAM_ID, timeRemaining: 30 }]); await executeAutoPick({ seasonId: SEASON_ID, teamId: TEAM_ID, pickNumber: 1, triggeredBy: "timer", autodraftSettings: null, db: mockDb, }); expect(mockSocketIO.emit).toHaveBeenCalledWith( "timer-update", expect.objectContaining({ teamId: TEAM_ID, timeRemaining: 30 }) ); }); it("returns success after the pick", async () => { const mockDb = makeMockDb({ draftTimerMode: "chess_clock", draftIncrementTime: 15 }); mockDb.returning .mockResolvedValueOnce([mockDraftPick]) .mockResolvedValueOnce([{ id: "timer-1", teamId: TEAM_ID, timeRemaining: 15 }]); const result = await executeAutoPick({ seasonId: SEASON_ID, teamId: TEAM_ID, pickNumber: 1, triggeredBy: "timer", autodraftSettings: null, db: mockDb, }); expect(result.success).toBe(true); }); }); // ── standard mode ─────────────────────────────────────────────────────────── describe("standard mode", () => { it("emits timer-update with exactly draftIncrementTime after a timer-triggered pick", async () => { const mockDb = makeMockDb({ draftTimerMode: "standard", draftIncrementTime: 30 }); mockDb.returning .mockResolvedValueOnce([mockDraftPick]) .mockResolvedValueOnce([{ id: "timer-1", teamId: TEAM_ID, timeRemaining: 30 }]); await executeAutoPick({ seasonId: SEASON_ID, teamId: TEAM_ID, pickNumber: 1, triggeredBy: "timer", autodraftSettings: null, db: mockDb, }); expect(mockSocketIO.emit).toHaveBeenCalledWith( "timer-update", expect.objectContaining({ teamId: TEAM_ID, timeRemaining: 30 }) ); }); it("writes the timer reset to the DB", async () => { const mockDb = makeMockDb({ draftTimerMode: "standard", draftIncrementTime: 30 }); mockDb.returning .mockResolvedValueOnce([mockDraftPick]) .mockResolvedValueOnce([{ id: "timer-1", teamId: TEAM_ID, timeRemaining: 30 }]); await executeAutoPick({ seasonId: SEASON_ID, teamId: TEAM_ID, pickNumber: 1, triggeredBy: "timer", autodraftSettings: null, db: mockDb, }); expect(mockDb.update).toHaveBeenCalledTimes(2); }); it("uses custom draftIncrementTime when configured", async () => { const mockDb = makeMockDb({ draftTimerMode: "standard", draftIncrementTime: 90 }); mockDb.returning .mockResolvedValueOnce([mockDraftPick]) .mockResolvedValueOnce([{ id: "timer-1", teamId: TEAM_ID, timeRemaining: 90 }]); await executeAutoPick({ seasonId: SEASON_ID, teamId: TEAM_ID, pickNumber: 1, triggeredBy: "timer", autodraftSettings: null, db: mockDb, }); expect(mockSocketIO.emit).toHaveBeenCalledWith( "timer-update", expect.objectContaining({ teamId: TEAM_ID, timeRemaining: 90 }) ); }); it("returns success after the pick", async () => { const mockDb = makeMockDb({ draftTimerMode: "standard", draftIncrementTime: 30 }); mockDb.returning .mockResolvedValueOnce([mockDraftPick]) .mockResolvedValueOnce([{ id: "timer-1", teamId: TEAM_ID, timeRemaining: 30 }]); const result = await executeAutoPick({ seasonId: SEASON_ID, teamId: TEAM_ID, pickNumber: 1, triggeredBy: "timer", autodraftSettings: null, db: mockDb, }); expect(result.success).toBe(true); }); }); });