import { describe, it, expect, vi, afterEach } from 'vitest' import { waitForHumanResponse, clearAllPending } from './human-responses.js' import { fightEvents } from './events.js' import type { Challenge } from './challenges.js' const factualChallenge: Challenge = { type: 'speed_blitz', label: 'Speed Blitz', prompt: 'What year was Bitcoin launched?', answers: ['2009'], scoring: 'factual', timeout_ms: 10000, baseDamage: 20, choices: ['2009', '2010', '2008'], } const quickChallenge: Challenge = { type: 'math_blitz', label: 'Math Blitz', prompt: 'What is 2+2?', answers: ['4'], scoring: 'factual', timeout_ms: 5000, baseDamage: 18, choices: ['4', '3', '5'], } afterEach(() => { clearAllPending() }) describe('BUG-2: SSE uses challenge timeout, not hardcoded', () => { it('waitForHumanResponse uses challenge.timeout_ms', () => { const fightId = `bug2-${Date.now()}` vi.useFakeTimers() // 10s challenge timeout + 5s human extra = 15s total const { promise } = waitForHumanResponse(fightId, 'bot1', factualChallenge, 1) let resolved = false void promise.then(() => { resolved = true }) // At 14s, should NOT have timed out yet vi.advanceTimersByTime(14_000) expect(resolved).toBe(false) // At 16s, should have timed out (10s + 5s = 15s) vi.advanceTimersByTime(2_000) vi.useRealTimers() }) it('different challenge type has different timeout', () => { const fightId = `bug2-quick-${Date.now()}` vi.useFakeTimers() // 5s timeout + 5s human extra = 10s total const { promise } = waitForHumanResponse(fightId, 'bot2', quickChallenge, 1) let resolved = false void promise.then(() => { resolved = true }) // At 9s, should NOT have timed out yet vi.advanceTimersByTime(9_000) expect(resolved).toBe(false) // At 11s, should have timed out (5s + 5s = 10s) vi.advanceTimersByTime(2_000) vi.useRealTimers() }) }) describe('BUG-3: shuffle return value used', () => { it('choices returned from waitForHumanResponse are shuffled (contain correct answer)', () => { vi.useFakeTimers() const fightId = `bug3-${Date.now()}` const { choices } = waitForHumanResponse(fightId, 'bot3', factualChallenge, 1) // Must contain the correct answer expect(choices).toContain('2009') // Must have 3 choices expect(choices).toHaveLength(3) clearAllPending() vi.useRealTimers() }) }) describe('BUG-9: no TODO in prompt options', () => { it('no prompt contains TODO string', () => { vi.useFakeTimers() const fightId = `bug9-${Date.now()}` const { choices } = waitForHumanResponse(fightId, 'bot9', factualChallenge, 1) for (const choice of choices) { expect(choice).not.toContain('TODO') expect(choice).not.toContain('pass #') } clearAllPending() vi.useRealTimers() }) }) describe('BUG-12: fightEvents.cleanup removes listeners', () => { it('cleanup removes fight listeners', () => { const fightId = 'bug12-test' let received = false fightEvents.on(fightId, () => { received = true }) fightEvents.cleanup(fightId) // Emit after cleanup — should not fire fightEvents.emit({ fightId, type: 'test', data: {}, timestamp: new Date().toISOString(), }) expect(received).toBe(false) }) it('global listeners still fire after cleanup', () => { const fightId = 'bug12-global' let received = false const unsub = fightEvents.onAll(() => { received = true }) fightEvents.cleanup(fightId) fightEvents.emit({ fightId, type: 'test', data: {}, timestamp: new Date().toISOString(), }) expect(received).toBe(true) unsub() }) })