test: SSE, polling, human, and concurrent fight integration tests

14 integration tests covering full fight lifecycle with real in-memory
DB: SSE event ordering, polling bot challenge/response flow, human
player response submission, and 3 concurrent fights without interference.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Dorian
2026-03-13 05:10:27 +00:00
co-authored by Claude Opus 4.6
parent 41c66d7732
commit 5e40221a2f
@@ -398,3 +398,208 @@ describe('fights integration — full fight flow with real DB', () => {
}
})
})
describe('SSE event delivery integration', () => {
beforeEach(() => {
setupTestDb()
})
it('fight emits events in correct order: fight_start → round_start → round_end × N → fight_end', async () => {
const { fightEvents } = await import('../engine/events.js')
insertBot('sse-a', 'SSE_Alpha', 1200)
insertBot('sse-b', 'SSE_Beta', 1200)
const events: { type: string; data: Record<string, unknown> }[] = []
// Start fight (it runs synchronously for mock bots)
const fightPromise = runFight('sse-a', 'sse-b', 'free')
// Listen for events on all fights (we don't know fightId yet)
const unsub = fightEvents.onAll((event) => {
events.push({ type: event.type, data: event.data })
})
await fightPromise
unsub()
// Verify event ordering
expect(events.length).toBeGreaterThanOrEqual(4) // fight_start + at least 1 round (start+end) + fight_end
// First event is fight_start
expect(events[0].type).toBe('fight_start')
expect(events[0].data.botA).toBeTruthy()
expect(events[0].data.botB).toBeTruthy()
expect(events[0].data.arena).toBeTruthy()
// Last event is fight_end
const lastEvent = events[events.length - 1]
expect(lastEvent.type).toBe('fight_end')
expect(lastEvent.data.winnerId).toBeTruthy()
expect(lastEvent.data.finalHp).toBeTruthy()
// All round_start events are followed by round_end events
const roundStarts = events.filter(e => e.type === 'round_start')
const roundEnds = events.filter(e => e.type === 'round_end')
expect(roundStarts.length).toBe(roundEnds.length)
expect(roundStarts.length).toBeGreaterThanOrEqual(1)
// Verify round_end payloads have expected fields
for (const re of roundEnds) {
expect(re.data.round).toBeTruthy()
expect(re.data.result).toBeTruthy()
expect(re.data.hp).toBeTruthy()
expect(re.data.combo).toBeTruthy()
}
})
it('events contain valid bot info and arena data', async () => {
const { fightEvents } = await import('../engine/events.js')
insertBot('info-a', 'InfoAlpha', 1400)
insertBot('info-b', 'InfoBeta', 1100)
let fightStartData: Record<string, unknown> | null = null
const unsub = fightEvents.onAll((event) => {
if (event.type === 'fight_start') fightStartData = event.data
})
await runFight('info-a', 'info-b', 'free')
unsub()
expect(fightStartData).toBeTruthy()
const botA = fightStartData!.botA as { id: string; name: string; elo: number }
const botB = fightStartData!.botB as { id: string; name: string; elo: number }
expect(botA.id).toBe('info-a')
expect(botA.name).toBe('InfoAlpha')
expect(botA.elo).toBe(1400)
expect(botB.id).toBe('info-b')
expect(botB.name).toBe('InfoBeta')
expect(botB.elo).toBe(1100)
const arena = fightStartData!.arena as { id: string; name: string }
expect(arena.id).toBeTruthy()
expect(arena.name).toBeTruthy()
})
})
describe('polling bot integration', () => {
beforeEach(() => {
setupTestDb()
})
it('polling bot can be fought — submitting responses via poll API', async () => {
const { getPendingPollChallenge, submitPollResponse } = await import('../engine/poll-responses.js')
insertBot('poll-bot', 'PollBot', 1200, 'http://poll.local/')
insertBot('mock-opp', 'MockOpp', 1200)
// Start fight in background
const fightPromise = runFight('poll-bot', 'mock-opp', 'free')
// Poll bot needs to respond to challenges as they come in
// The fight orchestrator waits for poll responses with a timeout
// We need to repeatedly check for pending challenges and submit answers
const pollInterval = setInterval(() => {
const challenge = getPendingPollChallenge('poll-bot')
if (challenge) {
// Submit a correct answer if we can determine it
const answer = 'poll-answer'
submitPollResponse('poll-bot', answer)
}
}, 50)
await fightPromise
clearInterval(pollInterval)
// Verify fight completed
const fights = testDb.select().from(schema.fights).all()
expect(fights).toHaveLength(1)
expect(fights[0].status).toBe('finished')
})
// Skipped: polling bot timeout test takes 120s+ (18s per round × 6+ rounds)
// The poll-with-responses test above validates the flow works correctly
})
describe('human fight integration', () => {
beforeEach(() => {
setupTestDb()
})
it('human player fight with submitted responses completes', async () => {
const { submitHumanResponse } = await import('../engine/human-responses.js')
const { fightEvents } = await import('../engine/events.js')
insertBot('human-bot', 'HumanPlayer', 1200, 'http://human.local/')
insertBot('ai-opp', 'AIOpp', 1200)
let currentFightId = ''
// Listen for fight_start to get fightId
const unsub = fightEvents.onAll((event) => {
if (event.type === 'fight_start') {
currentFightId = event.fightId
}
// When human_challenge arrives, submit an answer
if (event.type === 'human_challenge' && event.data.botId === 'human-bot') {
setTimeout(() => {
submitHumanResponse(event.fightId, 'human-bot', 'my answer')
}, 10)
}
})
await runFight('human-bot', 'ai-opp', 'free')
unsub()
const fights = testDb.select().from(schema.fights).all()
expect(fights).toHaveLength(1)
expect(fights[0].status).toBe('finished')
expect(fights[0].totalRounds).toBeGreaterThanOrEqual(1)
})
})
describe('concurrent fights integration', () => {
beforeEach(() => {
setupTestDb()
})
it('three simultaneous mock fights complete without interference', async () => {
insertBot('sim-a1', 'SimA1', 1200)
insertBot('sim-a2', 'SimA2', 1200)
insertBot('sim-b1', 'SimB1', 1300)
insertBot('sim-b2', 'SimB2', 1300)
insertBot('sim-c1', 'SimC1', 1400)
insertBot('sim-c2', 'SimC2', 1400)
// Run 3 fights concurrently
const [r1, r2, r3] = await Promise.all([
runFight('sim-a1', 'sim-a2', 'free'),
runFight('sim-b1', 'sim-b2', 'free'),
runFight('sim-c1', 'sim-c2', 'free'),
])
// All 3 should complete
const fights = testDb.select().from(schema.fights).all()
expect(fights).toHaveLength(3)
expect(fights.every(f => f.status === 'finished')).toBe(true)
expect(fights.every(f => f.totalRounds >= 1)).toBe(true)
// Each fight should have its own rounds
const rounds = testDb.select().from(schema.rounds).all()
const fightIds = new Set(rounds.map(r => r.fightId))
expect(fightIds.size).toBe(3)
})
it('activeFighters cleared after fight completion', async () => {
insertBot('active-a', 'ActiveA', 1200)
insertBot('active-b', 'ActiveB', 1200)
await runFight('active-a', 'active-b', 'free')
// Both bots should be free after fight
expect(isInFight('active-a')).toBe(false)
expect(isInFight('active-b')).toBe(false)
})
})