stuff
This commit is contained in:
@@ -0,0 +1,313 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
||||
import { ref, type Ref } from 'vue'
|
||||
import { useHumanChallenge } from '../useHumanChallenge'
|
||||
|
||||
// Mock global fetch
|
||||
const mockFetch = vi.fn()
|
||||
vi.stubGlobal('fetch', mockFetch)
|
||||
|
||||
function makeChallengeData(overrides: Record<string, unknown> = {}) {
|
||||
return {
|
||||
type: 'bitcoin_trivia',
|
||||
label: 'Bitcoin Trivia',
|
||||
prompt: 'Who is Satoshi Nakamoto?',
|
||||
roundNumber: 1,
|
||||
timeoutMs: 10000,
|
||||
scoring: 'factual',
|
||||
choices: [],
|
||||
...overrides,
|
||||
}
|
||||
}
|
||||
|
||||
describe('useHumanChallenge', () => {
|
||||
let fightId: Ref<string>
|
||||
let myBotId: Ref<string | null>
|
||||
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers()
|
||||
vi.clearAllMocks()
|
||||
fightId = ref('fight-1') as Ref<string>
|
||||
myBotId = ref<string | null>('bot-1')
|
||||
mockFetch.mockResolvedValue({
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: () => Promise.resolve({}),
|
||||
})
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers()
|
||||
})
|
||||
|
||||
it('applyChallenge sets challenge state correctly', () => {
|
||||
const hc = useHumanChallenge(fightId, myBotId)
|
||||
const data = makeChallengeData({ roundNumber: 3, choices: ['A', 'B', 'C'] })
|
||||
|
||||
hc.applyChallenge(data)
|
||||
|
||||
expect(hc.humanChallenge.value).not.toBeNull()
|
||||
expect(hc.humanChallenge.value!.type).toBe('bitcoin_trivia')
|
||||
expect(hc.humanChallenge.value!.prompt).toBe('Who is Satoshi Nakamoto?')
|
||||
expect(hc.humanChallenge.value!.roundNumber).toBe(3)
|
||||
expect(hc.humanChallenge.value!.scoring).toBe('factual')
|
||||
expect(hc.humanChoices.value).toEqual(['A', 'B', 'C'])
|
||||
expect(hc.humanAnswer.value).toBe('')
|
||||
expect(hc.humanSubmitted.value).toBe(false)
|
||||
expect(hc.humanTimer.value).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
it('applyChallenge deduplicates same round', () => {
|
||||
const hc = useHumanChallenge(fightId, myBotId)
|
||||
const data = makeChallengeData({ roundNumber: 1 })
|
||||
|
||||
hc.applyChallenge(data)
|
||||
const firstChallenge = hc.humanChallenge.value
|
||||
|
||||
// Apply same round again — should be no-op
|
||||
hc.applyChallenge(data)
|
||||
expect(hc.humanChallenge.value).toBe(firstChallenge)
|
||||
})
|
||||
|
||||
it('applyChallenge allows different round numbers', () => {
|
||||
const hc = useHumanChallenge(fightId, myBotId)
|
||||
|
||||
hc.applyChallenge(makeChallengeData({ roundNumber: 1 }))
|
||||
expect(hc.humanChallenge.value!.roundNumber).toBe(1)
|
||||
|
||||
hc.applyChallenge(makeChallengeData({ roundNumber: 2, prompt: 'New prompt' }))
|
||||
expect(hc.humanChallenge.value!.roundNumber).toBe(2)
|
||||
expect(hc.humanChallenge.value!.prompt).toBe('New prompt')
|
||||
})
|
||||
|
||||
it('timer counts down each second', () => {
|
||||
const hc = useHumanChallenge(fightId, myBotId)
|
||||
hc.applyChallenge(makeChallengeData({ timeoutMs: 8000 }))
|
||||
|
||||
const initialTimer = hc.humanTimer.value
|
||||
|
||||
vi.advanceTimersByTime(1000)
|
||||
expect(hc.humanTimer.value).toBe(initialTimer - 1)
|
||||
|
||||
vi.advanceTimersByTime(1000)
|
||||
expect(hc.humanTimer.value).toBe(initialTimer - 2)
|
||||
})
|
||||
|
||||
it('timeout clears challenge state and submits timeout', async () => {
|
||||
const hc = useHumanChallenge(fightId, myBotId)
|
||||
hc.applyChallenge(makeChallengeData({ timeoutMs: 6000 }))
|
||||
|
||||
// Advance past all timer ticks until timer reaches 0
|
||||
const timerVal = hc.humanTimer.value
|
||||
vi.advanceTimersByTime(timerVal * 1000)
|
||||
|
||||
expect(hc.humanSubmitted.value).toBe(true)
|
||||
|
||||
// submitTimeout should have been called — verify the fetch
|
||||
await vi.runAllTimersAsync()
|
||||
expect(mockFetch).toHaveBeenCalledWith(
|
||||
'/api/fights/fight-1/respond/bot-1',
|
||||
expect.objectContaining({
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ answer: '', timeout: true }),
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('submitHumanAnswer sends answer to API', async () => {
|
||||
const hc = useHumanChallenge(fightId, myBotId)
|
||||
hc.applyChallenge(makeChallengeData())
|
||||
hc.humanAnswer.value = 'A cypherpunk legend'
|
||||
|
||||
await hc.submitHumanAnswer()
|
||||
|
||||
expect(hc.humanSubmitted.value).toBe(true)
|
||||
expect(mockFetch).toHaveBeenCalledWith(
|
||||
'/api/fights/fight-1/respond/bot-1',
|
||||
expect.objectContaining({
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ answer: 'A cypherpunk legend' }),
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('submitHumanAnswer does not submit empty answer', async () => {
|
||||
const hc = useHumanChallenge(fightId, myBotId)
|
||||
hc.applyChallenge(makeChallengeData())
|
||||
hc.humanAnswer.value = ' '
|
||||
|
||||
await hc.submitHumanAnswer()
|
||||
|
||||
expect(hc.humanSubmitted.value).toBe(false)
|
||||
expect(mockFetch).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('submitHumanAnswer does not submit without botId', async () => {
|
||||
myBotId.value = null
|
||||
const hc = useHumanChallenge(fightId, myBotId)
|
||||
hc.applyChallenge(makeChallengeData())
|
||||
hc.humanAnswer.value = 'test'
|
||||
|
||||
await hc.submitHumanAnswer()
|
||||
|
||||
expect(hc.humanSubmitted.value).toBe(false)
|
||||
expect(mockFetch).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('submitChoice sets answer and submits', () => {
|
||||
const hc = useHumanChallenge(fightId, myBotId)
|
||||
hc.applyChallenge(makeChallengeData({ choices: ['21M', '42M', '100M'] }))
|
||||
|
||||
hc.submitChoice('21M')
|
||||
|
||||
expect(hc.humanAnswer.value).toBe('21M')
|
||||
expect(hc.humanSubmitted.value).toBe(true)
|
||||
expect(mockFetch).toHaveBeenCalledWith(
|
||||
'/api/fights/fight-1/respond/bot-1',
|
||||
expect.objectContaining({
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ answer: '21M' }),
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('submitChoice prevents double-tap', () => {
|
||||
const hc = useHumanChallenge(fightId, myBotId)
|
||||
hc.applyChallenge(makeChallengeData({ choices: ['A', 'B'] }))
|
||||
|
||||
hc.submitChoice('A')
|
||||
mockFetch.mockClear()
|
||||
|
||||
// Second tap should be ignored
|
||||
hc.submitChoice('B')
|
||||
expect(hc.humanAnswer.value).toBe('A')
|
||||
expect(mockFetch).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('cooldown prevents immediate resubmission', () => {
|
||||
const hc = useHumanChallenge(fightId, myBotId)
|
||||
hc.startCooldown(3)
|
||||
|
||||
expect(hc.roundCooldown.value).toBe(3)
|
||||
|
||||
vi.advanceTimersByTime(1000)
|
||||
expect(hc.roundCooldown.value).toBe(2)
|
||||
|
||||
vi.advanceTimersByTime(1000)
|
||||
expect(hc.roundCooldown.value).toBe(1)
|
||||
|
||||
vi.advanceTimersByTime(1000)
|
||||
expect(hc.roundCooldown.value).toBe(0)
|
||||
})
|
||||
|
||||
it('cooldown applies pending challenge when finished', () => {
|
||||
const hc = useHumanChallenge(fightId, myBotId)
|
||||
const pendingData = makeChallengeData({ roundNumber: 5 })
|
||||
|
||||
// Queue a pending challenge during cooldown
|
||||
hc.startCooldown(2)
|
||||
hc.pendingChallengeData.value = { data: pendingData, receivedAt: Date.now() }
|
||||
|
||||
// Advance past cooldown
|
||||
vi.advanceTimersByTime(2000)
|
||||
|
||||
expect(hc.roundCooldown.value).toBe(0)
|
||||
expect(hc.humanChallenge.value).not.toBeNull()
|
||||
expect(hc.humanChallenge.value!.roundNumber).toBe(5)
|
||||
expect(hc.pendingChallengeData.value).toBeNull()
|
||||
})
|
||||
|
||||
it('resetState clears all state', () => {
|
||||
const hc = useHumanChallenge(fightId, myBotId)
|
||||
hc.applyChallenge(makeChallengeData({ choices: ['X'] }))
|
||||
hc.humanAnswer.value = 'test answer'
|
||||
|
||||
hc.resetState()
|
||||
|
||||
expect(hc.humanChallenge.value).toBeNull()
|
||||
expect(hc.humanAnswer.value).toBe('')
|
||||
expect(hc.humanSubmitted.value).toBe(false)
|
||||
expect(hc.humanChoices.value).toEqual([])
|
||||
expect(hc.roundCooldown.value).toBe(0)
|
||||
expect(hc.pendingChallengeData.value).toBeNull()
|
||||
expect(hc.entrancePlaying.value).toBe(false)
|
||||
expect(hc.animatingRound.value).toBe(false)
|
||||
})
|
||||
|
||||
it('handleSSEChallenge queues when entrance is playing', () => {
|
||||
const hc = useHumanChallenge(fightId, myBotId)
|
||||
hc.entrancePlaying.value = true
|
||||
|
||||
const data = makeChallengeData({ roundNumber: 1 })
|
||||
hc.handleSSEChallenge(data)
|
||||
|
||||
// Should be queued, not applied
|
||||
expect(hc.humanChallenge.value).toBeNull()
|
||||
expect(hc.pendingChallengeData.value).not.toBeNull()
|
||||
expect(hc.pendingChallengeData.value!.data).toEqual(data)
|
||||
})
|
||||
|
||||
it('handleSSEChallenge applies immediately when not blocked', () => {
|
||||
const hc = useHumanChallenge(fightId, myBotId)
|
||||
|
||||
const data = makeChallengeData({ roundNumber: 1 })
|
||||
hc.handleSSEChallenge(data)
|
||||
|
||||
expect(hc.humanChallenge.value).not.toBeNull()
|
||||
expect(hc.humanChallenge.value!.roundNumber).toBe(1)
|
||||
})
|
||||
|
||||
it('setEntrancePlaying applies pending challenge when entrance ends', () => {
|
||||
const hc = useHumanChallenge(fightId, myBotId)
|
||||
hc.entrancePlaying.value = true
|
||||
|
||||
const data = makeChallengeData({ roundNumber: 2 })
|
||||
hc.pendingChallengeData.value = { data, receivedAt: Date.now() }
|
||||
|
||||
hc.setEntrancePlaying(false)
|
||||
|
||||
expect(hc.entrancePlaying.value).toBe(false)
|
||||
expect(hc.humanChallenge.value).not.toBeNull()
|
||||
expect(hc.humanChallenge.value!.roundNumber).toBe(2)
|
||||
expect(hc.pendingChallengeData.value).toBeNull()
|
||||
})
|
||||
|
||||
it('applyChallenge guarantees minimum display time', () => {
|
||||
const hc = useHumanChallenge(fightId, myBotId)
|
||||
// remainingMs of 2000 is below MIN_DISPLAY_MS (5000), so it should clamp up
|
||||
hc.applyChallenge(makeChallengeData({ remainingMs: 2000 }))
|
||||
|
||||
// Timer should be at least 5 seconds (MIN_DISPLAY_MS / 1000)
|
||||
expect(hc.humanTimer.value).toBeGreaterThanOrEqual(5)
|
||||
expect(hc.humanChallenge.value!.remainingMs).toBeGreaterThanOrEqual(5000)
|
||||
})
|
||||
|
||||
it('stopHumanPolling clears all interval handles', () => {
|
||||
const hc = useHumanChallenge(fightId, myBotId)
|
||||
hc.applyChallenge(makeChallengeData())
|
||||
hc.startCooldown(5)
|
||||
|
||||
// This should not throw and should clean up intervals
|
||||
hc.stopHumanPolling()
|
||||
|
||||
// Advancing timers should not change state
|
||||
const timerVal = hc.humanTimer.value
|
||||
const cooldownVal = hc.roundCooldown.value
|
||||
vi.advanceTimersByTime(5000)
|
||||
expect(hc.humanTimer.value).toBe(timerVal)
|
||||
expect(hc.roundCooldown.value).toBe(cooldownVal)
|
||||
})
|
||||
|
||||
it('clearChallenge removes challenge but preserves other state', () => {
|
||||
const hc = useHumanChallenge(fightId, myBotId)
|
||||
hc.applyChallenge(makeChallengeData({ choices: ['A', 'B'] }))
|
||||
hc.startCooldown(3)
|
||||
|
||||
hc.clearChallenge()
|
||||
|
||||
expect(hc.humanChallenge.value).toBeNull()
|
||||
expect(hc.humanSubmitted.value).toBe(false)
|
||||
expect(hc.pendingChallengeData.value).toBeNull()
|
||||
// Cooldown should still be running (clearChallenge doesn't touch it)
|
||||
expect(hc.roundCooldown.value).toBe(3)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user