From 6144fa79109c3554c089bb8a72d7128c7d2a4016 Mon Sep 17 00:00:00 2001 From: Dorian Date: Thu, 12 Mar 2026 23:42:11 +0000 Subject: [PATCH] test: add queue test suite with 8 cases (cooldown, join, leave, snapshot) Co-Authored-By: Claude Opus 4.6 --- server/src/engine/queue.test.ts | 140 ++++++++++++++++++++++++++++++++ 1 file changed, 140 insertions(+) create mode 100644 server/src/engine/queue.test.ts diff --git a/server/src/engine/queue.test.ts b/server/src/engine/queue.test.ts new file mode 100644 index 0000000..30e722f --- /dev/null +++ b/server/src/engine/queue.test.ts @@ -0,0 +1,140 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' + +// Mock orchestrator +vi.mock('./orchestrator.js', () => ({ + runFightAsync: vi.fn().mockResolvedValue('fight-123'), + isInFight: vi.fn().mockReturnValue(false), + getActiveFightId: vi.fn().mockReturnValue(null), +})) + +// Mock mock.ts +vi.mock('./mock.js', () => ({ + seedMockBots: vi.fn().mockResolvedValue(undefined), +})) + +// Mock db +const mockBotRows = vi.fn() +vi.mock('../db/index.js', () => ({ + db: { + select: () => ({ + from: () => ({ + where: () => ({ + limit: () => mockBotRows(), + }), + // For matchAgainstMock's full select (no where clause on this chain) + }), + }), + }, + schema: { + bots: { + id: 'id', + name: 'name', + secretHash: 'secretHash', + webhookUrl: 'webhookUrl', + eloRating: 'eloRating', + isActive: 'isActive', + }, + }, +})) + +const { joinQueue, leaveQueue, getQueueSize, getQueueSnapshot, setCooldown } = await import('./queue.js') +const { isInFight } = await import('./orchestrator.js') + +const mockBot = (id: string, name = 'TestBot', elo = 1200) => [{ + id, + name, + webhookUrl: 'https://example.com/webhook', + eloRating: elo, + isActive: true, + secretHash: 'abc', + ownerPubkey: 'pub123', + wins: 0, + losses: 0, + draws: 0, + winStreak: 0, + createdAt: new Date().toISOString(), +}] + +describe('queue', () => { + beforeEach(() => { + vi.clearAllMocks() + // Drain queue + while (getQueueSize() > 0) { + const snap = getQueueSnapshot() + if (snap[0]) leaveQueue(snap[0].botId) + } + }) + + it('getQueueSize returns 0 when empty', () => { + expect(getQueueSize()).toBe(0) + }) + + it('setCooldown prevents joining during cooldown', async () => { + mockBotRows.mockResolvedValue(mockBot('bot-cool')) + setCooldown('bot-cool') + + await expect(joinQueue('bot-cool')).rejects.toThrow('Cooldown active') + }) + + it('prevents bot already in fight from joining', async () => { + mockBotRows.mockResolvedValue(mockBot('bot-fighting')) + vi.mocked(isInFight).mockReturnValueOnce(true) + + await expect(joinQueue('bot-fighting')).rejects.toThrow('already in a fight') + }) + + it('throws for non-existent bot', async () => { + mockBotRows.mockResolvedValue([]) + + await expect(joinQueue('nonexistent')).rejects.toThrow('Bot not found') + }) + + it('throws for deactivated bot', async () => { + const bot = mockBot('bot-inactive') + bot[0].isActive = false + mockBotRows.mockResolvedValue(bot) + + await expect(joinQueue('bot-inactive')).rejects.toThrow('deactivated') + }) + + it('leaveQueue returns false for unknown bot', () => { + expect(leaveQueue('not-in-queue')).toBe(false) + }) + + it('getQueueSnapshot returns queued bot info', async () => { + mockBotRows.mockResolvedValue(mockBot('bot-snap', 'SnapBot', 1400)) + + // joinQueue will wait in queue since no opponent — don't await + const promise = joinQueue('bot-snap') + + // Flush microtasks so the async DB query resolves and bot enters queue + await new Promise(r => setTimeout(r, 0)) + + const snap = getQueueSnapshot() + expect(snap.length).toBe(1) + expect(snap[0].botId).toBe('bot-snap') + expect(snap[0].botName).toBe('SnapBot') + expect(snap[0].eloRating).toBe(1400) + + // Cleanup: leave queue + leaveQueue('bot-snap') + await promise.catch(() => {}) // swallow 'Left queue' rejection + }) + + it('leaveQueue removes bot and returns true', async () => { + mockBotRows.mockResolvedValue(mockBot('bot-leave')) + + const promise = joinQueue('bot-leave') + + // Flush microtasks so the async DB query resolves and bot enters queue + await new Promise(r => setTimeout(r, 0)) + + expect(getQueueSize()).toBe(1) + + const left = leaveQueue('bot-leave') + expect(left).toBe(true) + expect(getQueueSize()).toBe(0) + + await promise.catch(() => {}) // swallow rejection + }) +})