test: add human-responses unit tests

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Dorian
2026-03-12 22:36:32 +00:00
co-authored by Claude Opus 4.6
parent e48a984d96
commit 48847d879c
+88
View File
@@ -0,0 +1,88 @@
import { describe, it, expect, vi, afterEach } from 'vitest'
import { waitForHumanResponse, submitHumanResponse, getPendingChallenge, clearAllPending } from './human-responses.js'
import type { Challenge } from './challenges.js'
const mockChallenge: Challenge = {
type: 'bitcoin_trivia',
label: 'Bitcoin Trivia',
prompt: 'What year was Bitcoin launched?',
answers: ['2009'],
scoring: 'factual',
timeout_ms: 8000,
difficulty: 'medium',
choices: ['2009', '2010', '2008'],
}
afterEach(() => {
clearAllPending()
})
describe('waitForHumanResponse race condition fix', () => {
it('pending challenge is stored synchronously before promise resolves', () => {
// Call waitForHumanResponse — this must store the pending challenge
// SYNCHRONOUSLY (inside Promise constructor), not in a microtask
const { promise, choices } = waitForHumanResponse('fight-1', 'bot-1', mockChallenge, 1)
// IMMEDIATELY after, getPendingChallenge must find it
// (no await needed — this proves it's synchronous)
const pending = getPendingChallenge('fight-1', 'bot-1')
expect(pending).not.toBeNull()
expect(pending!.prompt).toBe('What year was Bitcoin launched?')
expect(pending!.choices).toEqual(choices)
// Clean up by submitting
submitHumanResponse('fight-1', 'bot-1', '2009')
})
it('choices are returned from waitForHumanResponse for SSE inclusion', () => {
const { choices } = waitForHumanResponse('fight-2', 'bot-2', mockChallenge, 1)
// Choices must be non-empty and include the correct answer
expect(choices.length).toBeGreaterThanOrEqual(2)
expect(choices).toContain('2009')
submitHumanResponse('fight-2', 'bot-2', '2009')
})
})
describe('waitForHumanResponse timeout', () => {
it('times out and resolves with null answer', async () => {
vi.useFakeTimers()
const shortChallenge = { ...mockChallenge, timeout_ms: 100 }
const { promise } = waitForHumanResponse('fight-3', 'bot-3', shortChallenge, 1)
// Advance past timeout (100ms challenge + 5000ms human extra)
vi.advanceTimersByTime(5200)
const result = await promise
expect(result.timedOut).toBe(true)
expect(result.answer).toBeNull()
vi.useRealTimers()
})
})
describe('submitHumanResponse', () => {
it('accepts submission and resolves promise', async () => {
const { promise } = waitForHumanResponse('fight-4', 'bot-4', mockChallenge, 1)
const accepted = submitHumanResponse('fight-4', 'bot-4', '2009', 'Bitcoin fixes this')
expect(accepted).toBe(true)
const result = await promise
expect(result.answer).toBe('2009')
expect(result.trashTalk).toBe('Bitcoin fixes this')
expect(result.timedOut).toBe(false)
})
it('returns false for unknown fight/bot', () => {
const accepted = submitHumanResponse('nonexistent', 'nobody', 'answer')
expect(accepted).toBe(false)
})
it('truncates long answers to 2000 chars', async () => {
const { promise } = waitForHumanResponse('fight-5', 'bot-5', mockChallenge, 1)
const longAnswer = 'x'.repeat(3000)
submitHumanResponse('fight-5', 'bot-5', longAnswer)
const result = await promise
expect(result.answer!.length).toBe(2000)
})
})