brackt/server/__tests__/timer-autodraft.test.ts
Claude f36480c828
Phase 1: Replace per-second timer tick with event-driven scheduler
Eliminates the setInterval(1s) + per-second DB write by storing picksExpiresAt
in draft_timers and using a targeted setTimeout per pick. Clients count down
locally from the expiresAt timestamp, removing server-pushed timer-update events.

Key changes:
- database/schema.ts: add picksExpiresAt and picksStartedAt to draft_timers
- server/timer.ts: full rewrite — schedulePickForSeason, rescheduleTimer,
  30s recovery interval instead of 1s tick, overnight-pause resume scheduling
- server/socket.ts: new timer-pick-started / timer-overnight-paused events,
  updated draft-state-sync to include expiresAt for reconnect recovery
- draft.make-pick / draft.force-manual-pick: compute actual remaining from
  picksExpiresAt at pick time; call rescheduleTimer after the autodraft chain
- useDraftSocketEvents: handle new timer events, restore countdown on reconnect
- $leagueId.draft.$seasonId: client-side countdown useEffect from expiresAt
- plans/zero-downtime-scaling.md: full 4-phase scaling plan for future reference

Resolves 2351 unit tests (all passing).

https://claude.ai/code/session_019k5J6Ty7uP5HxSx6CsbiBK
2026-06-03 03:04:46 +00:00

342 lines
14 KiB
TypeScript

import { describe, it, expect, vi, beforeEach } from 'vitest';
// Mock dependencies before imports
vi.mock('drizzle-orm/postgres-js', () => ({
drizzle: vi.fn(() => mockDb),
}));
vi.mock('postgres', () => ({
default: vi.fn(() => ({})),
}));
vi.mock('../socket', () => ({
getSocketIO: vi.fn(() => mockSocketIO),
}));
let mockDb: any;
let mockSocketIO: any;
// Setup mocks
beforeEach(() => {
mockSocketIO = {
to: vi.fn().mockReturnThis(),
emit: vi.fn(),
};
mockDb = {
query: {
seasons: { findMany: vi.fn(), findFirst: vi.fn() },
draftSlots: { findMany: vi.fn() },
draftTimers: { findFirst: vi.fn() },
autodraftSettings: { findFirst: vi.fn() },
draftPicks: { findMany: vi.fn(), findFirst: vi.fn() },
draftQueue: { findMany: vi.fn() },
seasonParticipants: { findMany: vi.fn() },
seasonTemplateSports: { findMany: vi.fn() },
},
update: vi.fn().mockReturnThis(),
set: vi.fn().mockReturnThis(),
where: vi.fn().mockReturnThis(),
returning: vi.fn(),
insert: vi.fn().mockReturnThis(),
values: vi.fn().mockReturnThis(),
delete: vi.fn().mockReturnThis(),
select: vi.fn().mockReturnThis(),
from: vi.fn().mockReturnThis(),
innerJoin: vi.fn().mockReturnThis(),
orderBy: vi.fn().mockReturnThis(),
limit: vi.fn().mockReturnThis(),
};
});
describe('Timer Autodraft Integration', () => {
describe('Autodraft Settings Check', () => {
it('should check autodraft settings when timer expires', async () => {
const seasonId = 'season-123';
const teamId = 'team-456';
mockDb.query.seasons.findMany.mockResolvedValue([{
id: seasonId, status: 'draft', draftPaused: false,
currentPickNumber: 1, draftInitialTime: 120, draftIncrementTime: 30, draftRounds: 10,
}]);
mockDb.query.draftSlots.findMany.mockResolvedValue([
{ id: 'slot-1', teamId, draftOrder: 1 },
{ id: 'slot-2', teamId: 'team-2', draftOrder: 2 },
]);
mockDb.query.draftTimers.findFirst.mockResolvedValue({ id: 'timer-1', seasonId, teamId, timeRemaining: 0 });
mockDb.query.autodraftSettings.findFirst.mockResolvedValue({
id: 'settings-1', seasonId, teamId, isEnabled: true, mode: 'next_pick', queueOnly: false,
});
expect(mockDb.query.autodraftSettings.findFirst).toBeDefined();
});
it('should use regular auto-pick when autodraft is disabled', async () => {
const seasonId = 'season-123';
const teamId = 'team-456';
mockDb.query.autodraftSettings.findFirst.mockResolvedValue({
id: 'settings-1', seasonId, teamId, isEnabled: false, mode: 'next_pick', queueOnly: false,
});
const settings = await mockDb.query.autodraftSettings.findFirst();
expect(settings.isEnabled).toBe(false);
});
it('should handle missing autodraft settings', async () => {
mockDb.query.autodraftSettings.findFirst.mockResolvedValue(null);
const settings = await mockDb.query.autodraftSettings.findFirst();
expect(settings).toBeNull();
});
});
describe('Autodraft Mode Handling', () => {
it('should disable autodraft after pick when mode is next_pick', async () => {
const seasonId = 'season-123';
const teamId = 'team-456';
const autodraftSettings = { id: 'settings-1', seasonId, teamId, isEnabled: true, mode: 'next_pick', queueOnly: false };
mockDb.returning.mockResolvedValue([{ ...autodraftSettings, isEnabled: false, updatedAt: new Date() }]);
if (autodraftSettings.isEnabled && autodraftSettings.mode === 'next_pick') {
await mockDb.update();
await mockDb.set({ isEnabled: false, updatedAt: new Date() });
await mockDb.where();
const result = await mockDb.returning();
expect(result[0].isEnabled).toBe(false);
}
});
it('should keep autodraft enabled when mode is while_on (All Picks)', async () => {
const autodraftSettings = { isEnabled: true, mode: 'while_on', queueOnly: false };
if (autodraftSettings.mode === 'while_on') {
expect(autodraftSettings.isEnabled).toBe(true);
}
});
});
describe('Autodraft Pick Logic', () => {
it('should pick from queue first when autodraft is enabled', async () => {
mockDb.query.draftQueue.findMany.mockResolvedValue([
{ id: 'queue-1', participantId: 'participant-1', queuePosition: 1 },
{ id: 'queue-2', participantId: 'participant-2', queuePosition: 2 },
]);
mockDb.query.draftPicks.findMany.mockResolvedValue([]);
const queueItems = await mockDb.query.draftQueue.findMany();
const draftedPicks = await mockDb.query.draftPicks.findMany();
const draftedParticipantIds = draftedPicks.map((p: any) => p.participantId);
const availableQueueItem = queueItems.find((item: any) => !draftedParticipantIds.includes(item.participantId));
expect(availableQueueItem).toBeDefined();
expect(availableQueueItem.participantId).toBe('participant-1');
});
it('should skip drafted participants in queue and try next', async () => {
mockDb.query.draftQueue.findMany.mockResolvedValue([
{ id: 'queue-1', participantId: 'participant-1', queuePosition: 1 },
{ id: 'queue-2', participantId: 'participant-2', queuePosition: 2 },
]);
mockDb.query.draftPicks.findMany.mockResolvedValue([{ id: 'pick-1', participantId: 'participant-1' }]);
const queueItems = await mockDb.query.draftQueue.findMany();
const draftedPicks = await mockDb.query.draftPicks.findMany();
const draftedParticipantIds = draftedPicks.map((p: any) => p.participantId);
const availableQueueItem = queueItems.find((item: any) => !draftedParticipantIds.includes(item.participantId));
expect(availableQueueItem.participantId).toBe('participant-2');
});
it('should fall back to highest EV when queue is empty and queueOnly is OFF', async () => {
mockDb.query.draftQueue.findMany.mockResolvedValue([]);
mockDb.query.seasonParticipants.findMany.mockResolvedValue([
{ id: 'participant-high-ev', name: 'High EV Participant', expectedValue: 1000 },
]);
const queueItems = await mockDb.query.draftQueue.findMany();
expect(queueItems.length).toBe(0);
const queueOnly = false;
const availableParticipants = await mockDb.query.seasonParticipants.findMany();
const selectedParticipant = queueOnly ? null : availableParticipants[0];
expect(selectedParticipant).not.toBeNull();
expect(selectedParticipant?.expectedValue).toBe(1000);
});
});
describe('Queue-Only Constraint (AC2 & AC3)', () => {
it('should NOT fall back to highest EV when queueOnly is ON and queue is empty', async () => {
mockDb.query.draftQueue.findMany.mockResolvedValue([]);
const queueItems = await mockDb.query.draftQueue.findMany();
expect(queueItems.length).toBe(0);
const queueOnly = true;
const selectedParticipant = queueOnly ? null : { id: 'participant-high-ev' };
expect(selectedParticipant).toBeNull();
});
it('should disable autodraft when queueOnly is ON and queue becomes empty (AC3)', async () => {
const seasonId = 'season-123';
const teamId = 'team-456';
const autodraftSettings = { id: 'settings-1', seasonId, teamId, isEnabled: true, mode: 'next_pick', queueOnly: true };
mockDb.returning.mockResolvedValue([{ ...autodraftSettings, isEnabled: false, updatedAt: new Date() }]);
const participantId = null; // empty queue + queueOnly
if (!participantId && autodraftSettings.queueOnly) {
await mockDb.update();
await mockDb.set({ isEnabled: false, updatedAt: new Date() });
await mockDb.where();
const result = await mockDb.returning();
expect(result[0].isEnabled).toBe(false);
mockSocketIO.to(`draft-${seasonId}`).emit('autodraft-updated', {
teamId, isEnabled: false, mode: autodraftSettings.mode, queueOnly: autodraftSettings.queueOnly,
});
expect(mockSocketIO.emit).toHaveBeenCalledWith('autodraft-updated', {
teamId, isEnabled: false, mode: 'next_pick', queueOnly: true,
});
}
});
it('should skip drafted queue items and continue to next available item', async () => {
const queue = [
{ id: 'queue-1', participantId: 'participant-1', queuePosition: 1 },
{ id: 'queue-2', participantId: 'participant-2', queuePosition: 2 },
];
const drafted = new Set(['participant-1']);
const availableQueueItem = queue.find((item) => !drafted.has(item.participantId));
expect(availableQueueItem?.participantId).toBe('participant-2');
});
it('should auto-disable when all queue items are drafted and queueOnly is ON', async () => {
const queue = [
{ id: 'queue-1', participantId: 'participant-1', queuePosition: 1 },
{ id: 'queue-2', participantId: 'participant-2', queuePosition: 2 },
];
const drafted = new Set(['participant-1', 'participant-2']);
const queueOnly = true;
const availableQueueItem = queue.find((item) => !drafted.has(item.participantId));
expect(availableQueueItem).toBeUndefined();
const selectedParticipant = queueOnly ? null : { id: 'fallback-ev' };
expect(selectedParticipant).toBeNull();
});
});
describe('Three-State Autodraft (AC1)', () => {
it('should map Off state to isEnabled=false', () => {
const offState = { isEnabled: false, mode: 'next_pick', queueOnly: false };
expect(offState.isEnabled).toBe(false);
});
it('should map Next Pick state to isEnabled=true, mode=next_pick', () => {
const nextPickState = { isEnabled: true, mode: 'next_pick', queueOnly: false };
expect(nextPickState.isEnabled).toBe(true);
expect(nextPickState.mode).toBe('next_pick');
});
it('should map All Picks state to isEnabled=true, mode=while_on', () => {
const allPicksState = { isEnabled: true, mode: 'while_on', queueOnly: false };
expect(allPicksState.isEnabled).toBe(true);
expect(allPicksState.mode).toBe('while_on');
});
it('should transition Off → Next Pick correctly', () => {
const initial = { isEnabled: false, mode: 'next_pick' };
const updated = { isEnabled: true, mode: 'next_pick' };
expect(updated.isEnabled).not.toBe(initial.isEnabled);
expect(updated.mode).toBe('next_pick');
});
it('should transition Next Pick → All Picks correctly', () => {
const nextPick = { isEnabled: true, mode: 'next_pick' };
const allPicks = { ...nextPick, mode: 'while_on' };
expect(allPicks.mode).toBe('while_on');
expect(allPicks.isEnabled).toBe(true);
});
it('should auto-disable if queue is empty and queueOnly is ON (AC3)', () => {
const settings = { isEnabled: true, mode: 'next_pick', queueOnly: true };
const queue: any[] = [];
const shouldAutoDisable = settings.queueOnly && queue.length === 0;
const finalSettings = shouldAutoDisable ? { ...settings, isEnabled: false } : settings;
expect(finalSettings.isEnabled).toBe(false);
});
it('should NOT auto-disable if queue is empty and queueOnly is OFF', () => {
const settings = { isEnabled: true, mode: 'next_pick', queueOnly: false };
const queue: any[] = [];
const shouldAutoDisable = settings.queueOnly && queue.length === 0;
const finalSettings = shouldAutoDisable ? { ...settings, isEnabled: false } : settings;
expect(finalSettings.isEnabled).toBe(true);
});
});
describe('Socket Event Emission', () => {
it('should emit autodraft-updated with queueOnly when disabling next_pick mode', async () => {
const seasonId = 'season-123';
const teamId = 'team-456';
mockSocketIO.to(`draft-${seasonId}`).emit('autodraft-updated', {
teamId, isEnabled: false, mode: 'next_pick', queueOnly: false,
});
expect(mockSocketIO.emit).toHaveBeenCalledWith('autodraft-updated', {
teamId, isEnabled: false, mode: 'next_pick', queueOnly: false,
});
});
it('should emit pick-made event after autodraft pick', async () => {
const seasonId = 'season-123';
const pickData = { pick: { id: 'pick-1', participantId: 'participant-1', pickNumber: 1 }, nextPickNumber: 2, isDraftComplete: false };
mockSocketIO.to(`draft-${seasonId}`).emit('pick-made', pickData);
expect(mockSocketIO.emit).toHaveBeenCalledWith('pick-made', pickData);
});
it('should emit autodraft-updated with queueOnly preserved on auto-disable (AC3)', async () => {
const seasonId = 'season-123';
const teamId = 'team-456';
mockSocketIO.to(`draft-${seasonId}`).emit('autodraft-updated', {
teamId, isEnabled: false, mode: 'next_pick', queueOnly: true,
});
expect(mockSocketIO.emit).toHaveBeenCalledWith('autodraft-updated', {
teamId, isEnabled: false, mode: 'next_pick', queueOnly: true,
});
});
});
describe('Back-to-Back Picks', () => {
it('should only autodraft first pick when mode is next_pick', async () => {
const seasonId = 'season-123';
const teamId = 'team-456';
const autodraftSettings = { id: 'settings-1', seasonId, teamId, isEnabled: true, mode: 'next_pick', queueOnly: false };
if (autodraftSettings.isEnabled && autodraftSettings.mode === 'next_pick') {
const updatedSettings = { ...autodraftSettings, isEnabled: false };
expect(updatedSettings.isEnabled).toBe(false);
}
const settingsForSecondPick = { ...autodraftSettings, isEnabled: false };
expect(settingsForSecondPick.isEnabled).toBe(false);
});
it('should continue autodrafting consecutive picks when mode is while_on (All Picks)', async () => {
const autodraftSettings = { isEnabled: true, mode: 'while_on', queueOnly: false };
if (autodraftSettings.mode === 'while_on') expect(autodraftSettings.isEnabled).toBe(true);
expect(autodraftSettings.isEnabled).toBe(true);
});
});
describe('Time Increment', () => {
it('should add increment time after autodraft pick', () => {
expect(0 + 30).toBe(30);
});
it('should emit timer-pick-started after scheduling next pick', async () => {
const seasonId = 'season-123';
const teamId = 'team-456';
mockSocketIO.to(`draft-${seasonId}`).emit('timer-pick-started', {
seasonId, teamId, pickNumber: 1, expiresAt: Date.now() + 30000, timeRemaining: 30,
});
expect(mockSocketIO.emit).toHaveBeenCalledWith('timer-pick-started',
expect.objectContaining({ seasonId, teamId, timeRemaining: 30 })
);
});
});
});