2026-03-13 00:08:01 +00:00
|
|
|
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
|
2026-04-11 19:46:37 +01:00
|
|
|
void promise.then(() => { resolved = true })
|
2026-03-13 00:08:01 +00:00
|
|
|
|
|
|
|
|
// 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
|
2026-04-11 19:46:37 +01:00
|
|
|
void promise.then(() => { resolved = true })
|
2026-03-13 00:08:01 +00:00
|
|
|
|
|
|
|
|
// 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()
|
|
|
|
|
})
|
|
|
|
|
})
|