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:
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)
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user