import { describe, it, expect, vi, afterEach } from 'vitest' import { waitForPollResponse, submitPollResponse, getPendingPollChallenge, clearAllPendingPolls, isPollingBot, } from './poll-responses.js' import type { Challenge } from './challenges.js' const mockChallenge: Challenge = { type: 'speed_blitz', label: 'Speed Blitz', prompt: 'What year was Bitcoin created?', answers: ['2009'], scoring: 'factual', timeout_ms: 8000, baseDamage: 20, } const mockOpponent = { name: 'TestBot', wins: 5, losses: 3 } afterEach(() => { clearAllPendingPolls() vi.useRealTimers() }) describe('isPollingBot', () => { it('returns true for poll sentinel URL', () => { expect(isPollingBot('http://poll.local/')).toBe(true) }) it('returns false for other URLs', () => { expect(isPollingBot('https://example.com/webhook')).toBe(false) }) }) describe('waitForPollResponse + getPendingPollChallenge', () => { it('stores challenge and makes it retrievable', () => { void waitForPollResponse('f1', 'b1', mockChallenge, 1, mockOpponent, 'arena1', null) const pending = getPendingPollChallenge('b1') expect(pending).not.toBeNull() expect(pending!.fightId).toBe('f1') expect(pending!.type).toBe('speed_blitz') expect(pending!.prompt).toBe('What year was Bitcoin created?') expect(pending!.roundNumber).toBe(1) expect(pending!.opponent.name).toBe('TestBot') expect(pending!.arena).toBe('arena1') expect(pending!.constraints.max_tokens).toBe(500) }) it('returns null for unknown bot', () => { expect(getPendingPollChallenge('nonexistent')).toBeNull() }) }) describe('submitPollResponse', () => { it('accepts submission and resolves promise', async () => { const promise = waitForPollResponse('f2', 'b2', mockChallenge, 1, mockOpponent, 'arena1', null) const accepted = submitPollResponse('b2', '2009', 'ez') expect(accepted).toBe(true) const result = await promise expect(result.answer).toBe('2009') expect(result.trashTalk).toBe('ez') expect(result.timedOut).toBe(false) }) it('returns false for unknown bot (no pending challenge)', () => { expect(submitPollResponse('nobody', 'answer')).toBe(false) }) it('rejects duplicate submission (second submit returns false)', async () => { void waitForPollResponse('f3', 'b3', mockChallenge, 1, mockOpponent, 'arena1', null) expect(submitPollResponse('b3', 'first')).toBe(true) expect(submitPollResponse('b3', 'second')).toBe(false) }) it('truncates answer to 2000 chars', async () => { const promise = waitForPollResponse('f4', 'b4', mockChallenge, 1, mockOpponent, 'arena1', null) submitPollResponse('b4', 'x'.repeat(3000)) const result = await promise expect(result.answer!.length).toBe(2000) }) }) describe('timeout', () => { it('times out and resolves with null answer', async () => { vi.useFakeTimers() const shortChallenge = { ...mockChallenge, timeout_ms: 100 } const promise = waitForPollResponse('f5', 'b5', shortChallenge, 1, mockOpponent, 'arena1', null) // Advance past timeout (100ms + POLL_GRACE_MS=10_000 = 10_100ms) vi.advanceTimersByTime(10_200) const result = await promise expect(result.timedOut).toBe(true) expect(result.answer).toBeNull() }) it('clears pending after timeout', async () => { vi.useFakeTimers() const shortChallenge = { ...mockChallenge, timeout_ms: 100 } void waitForPollResponse('f6', 'b6', shortChallenge, 1, mockOpponent, 'arena1', null) vi.advanceTimersByTime(10_200) expect(getPendingPollChallenge('b6')).toBeNull() }) })