From e48a984d96f6c1474f18a6a05943a2e2abe9d0d8 Mon Sep 17 00:00:00 2001 From: Dorian Date: Thu, 12 Mar 2026 22:36:04 +0000 Subject: [PATCH] test: verify human challenge race condition fix ordering (BUG-S2) waitForHumanResponse synchronously stores pending challenge before returning, ensuring SSE emit happens after storage. Tests verify ordering, choice consistency, promise resolution, and cleanup. Co-Authored-By: Claude Opus 4.6 --- .../engine/human-responses-ordering.test.ts | 80 +++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 server/src/engine/human-responses-ordering.test.ts diff --git a/server/src/engine/human-responses-ordering.test.ts b/server/src/engine/human-responses-ordering.test.ts new file mode 100644 index 0000000..5a1149b --- /dev/null +++ b/server/src/engine/human-responses-ordering.test.ts @@ -0,0 +1,80 @@ +import { describe, it, expect } from 'vitest' +import { waitForHumanResponse, getPendingChallenge, submitHumanResponse } from './human-responses.js' +import type { Challenge } from './challenges.js' + +const testChallenge: Challenge = { + type: 'bitcoin_trivia', + label: 'Bitcoin Trivia', + prompt: 'What year was Bitcoin launched?', + answers: ['2009'], + scoring: 'factual', + timeout_ms: 8000, + difficulty: 'easy', + choices: ['2009', '2010', '2008'], +} + +describe('waitForHumanResponse ordering', () => { + it('pending challenge is stored synchronously before promise is awaited', () => { + const fightId = `fight-ordering-${Date.now()}` + const botId = 'bot-ordering-test' + + // Call waitForHumanResponse — the pending entry must be stored synchronously + const { promise, choices } = waitForHumanResponse(fightId, botId, testChallenge, 1) + + // IMMEDIATELY check that getPendingChallenge returns data (before awaiting) + const pending = getPendingChallenge(fightId, botId) + expect(pending).not.toBeNull() + expect(pending!.prompt).toBe('What year was Bitcoin launched?') + expect(pending!.roundNumber).toBe(1) + expect(choices).toBeInstanceOf(Array) + expect(choices.length).toBe(3) + + // Clean up: submit answer to resolve the promise + submitHumanResponse(fightId, botId, '2009') + // Consume promise to prevent unhandled rejection + return promise + }) + + it('choices returned by waitForHumanResponse match pending challenge choices', () => { + const fightId = `fight-choices-${Date.now()}` + const botId = 'bot-choices-test' + + const { promise, choices } = waitForHumanResponse(fightId, botId, testChallenge, 2) + + const pending = getPendingChallenge(fightId, botId) + expect(pending).not.toBeNull() + expect(pending!.choices).toEqual(choices) + + submitHumanResponse(fightId, botId, '2009') + return promise + }) + + it('submitHumanResponse resolves the waiting promise', async () => { + const fightId = `fight-submit-${Date.now()}` + const botId = 'bot-submit-test' + + const { promise } = waitForHumanResponse(fightId, botId, testChallenge, 3) + + // Submit after a microtask delay to simulate browser response + setTimeout(() => submitHumanResponse(fightId, botId, '2009', 'nice!'), 10) + + const result = await promise + expect(result.answer).toBe('2009') + expect(result.trashTalk).toBe('nice!') + expect(result.timedOut).toBe(false) + }) + + it('pending is cleared after submit', () => { + const fightId = `fight-clear-${Date.now()}` + const botId = 'bot-clear-test' + + const { promise } = waitForHumanResponse(fightId, botId, testChallenge, 4) + + submitHumanResponse(fightId, botId, '2009') + + const pending = getPendingChallenge(fightId, botId) + expect(pending).toBeNull() + + return promise + }) +})