From 17a9fca4b5550f5c1c0343bf0a08db62c68db1b4 Mon Sep 17 00:00:00 2001 From: Dorian Date: Mon, 9 Mar 2026 08:53:19 +0000 Subject: [PATCH] test: add 10,000 fight crash test and all-16-types scoring verification MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two new regression tests: - 10,000 fights with varied elos, personalities, and round counts (50k+ rounds) — zero crashes - All 16 challenge types tested with 5 response scenarios each (normal, timeout, error, double-timeout, arena+combo) Co-Authored-By: Claude Opus 4.6 --- server/src/engine/lifecycle.test.ts | 125 ++++++++++++++++++++++++++++ 1 file changed, 125 insertions(+) diff --git a/server/src/engine/lifecycle.test.ts b/server/src/engine/lifecycle.test.ts index 71b4fa2..8bc76ed 100644 --- a/server/src/engine/lifecycle.test.ts +++ b/server/src/engine/lifecycle.test.ts @@ -295,6 +295,131 @@ describe('fight lifecycle', () => { } }) + it('10,000 automated fight simulations — zero crashes', () => { + const personalities = ['confident', 'clueless', 'witty', 'aggressive', 'zen'] + const elos = [900, 1000, 1200, 1400, 1600, 1800, 2000] + let totalRounds = 0 + + for (let f = 0; f < 10000; f++) { + const eloA = elos[f % elos.length] + const eloB = elos[(f * 3 + 1) % elos.length] + const persA = personalities[f % personalities.length] + const persB = personalities[(f + 2) % personalities.length] + const rounds = 3 + (f % 8) // 3 to 10 rounds + + const botA = { id: 'sim-a', name: 'SimA' } + const botB = { id: 'sim-b', name: 'SimB' } + const usedTypes = new Set() + let comboA = 0 + let comboB = 0 + + for (let r = 0; r < rounds; r++) { + const challenge = pickChallenge(usedTypes, null, undefined, r + 1) + usedTypes.add(challenge.type) + + const respA = mockResponse(challenge, persA, eloA) + const respB = mockResponse(challenge, persB, eloB) + + const result = scoreRound( + challenge, botA, botB, + { answer: respA.answer, timeMs: respA.timeMs, timedOut: respA.timedOut, error: respA.error }, + { answer: respB.answer, timeMs: respB.timeMs, timedOut: respB.timedOut, error: respB.error }, + null, comboA, comboB, + ) + + // Verify result integrity + expect(result.botAScore).toBeGreaterThanOrEqual(0) + expect(result.botBScore).toBeGreaterThanOrEqual(0) + expect(result.botADamage).toBeGreaterThanOrEqual(0) + expect(result.botBDamage).toBeGreaterThanOrEqual(0) + expect(typeof result.narration).toBe('string') + expect(result.narration.length).toBeGreaterThan(0) + expect(Number.isFinite(result.botAScore)).toBe(true) + expect(Number.isFinite(result.botBScore)).toBe(true) + + if (result.winnerId === botA.id) { comboA++; comboB = 0 } + else if (result.winnerId === botB.id) { comboB++; comboA = 0 } + + totalRounds++ + } + } + + // Verify we actually ran a substantial number of rounds + expect(totalRounds).toBeGreaterThan(50000) + }) + + it('all 16 challenge types score correctly', () => { + const allTypes = getAllChallengeTypes() + expect(allTypes.length).toBe(16) + + const botA = { id: 'type-a', name: 'TypeTestA' } + const botB = { id: 'type-b', name: 'TypeTestB' } + + for (const type of allTypes) { + // Pick a challenge of this specific type + const usedTypes = new Set(allTypes.filter(t => t !== type)) + const challenge = pickChallenge(usedTypes, null) + expect(challenge.type).toBe(type) + + // Test all response scenarios for this type: + + // 1. Both answer correctly/well + const respA1 = mockResponse(challenge, 'confident', 1800) + const respB1 = mockResponse(challenge, 'confident', 1800) + const r1 = scoreRound( + challenge, botA, botB, + { answer: respA1.answer, timeMs: 200, timedOut: false, error: false }, + { answer: respB1.answer, timeMs: 300, timedOut: false, error: false }, + null, 0, 0, + ) + expect(r1.botAScore).toBeGreaterThanOrEqual(0) + expect(r1.botBScore).toBeGreaterThanOrEqual(0) + expect(typeof r1.narration).toBe('string') + + // 2. A times out + const r2 = scoreRound( + challenge, botA, botB, + { answer: '', timeMs: 8000, timedOut: true, error: false }, + { answer: respB1.answer, timeMs: 300, timedOut: false, error: false }, + null, 0, 0, + ) + expect(r2.winnerId).toBe(botB.id) + expect(r2.botAScore).toBe(0) + + // 3. B errors + const r3 = scoreRound( + challenge, botA, botB, + { answer: respA1.answer, timeMs: 200, timedOut: false, error: false }, + { answer: '', timeMs: 0, timedOut: false, error: true }, + null, 0, 0, + ) + expect(r3.winnerId).toBe(botA.id) + expect(r3.botBScore).toBe(0) + + // 4. Both timeout + const r4 = scoreRound( + challenge, botA, botB, + { answer: '', timeMs: 8000, timedOut: true, error: false }, + { answer: '', timeMs: 8000, timedOut: true, error: false }, + null, 0, 0, + ) + expect(r4.winnerId).toBeNull() + expect(r4.botADamage).toBe(0) + expect(r4.botBDamage).toBe(0) + + // 5. With arena modifier and combo + const r5 = scoreRound( + challenge, botA, botB, + { answer: respA1.answer, timeMs: 200, timedOut: false, error: false }, + { answer: respB1.answer, timeMs: 500, timedOut: false, error: false }, + 'speed_2x', 3, 0, + ) + expect(r5.botAScore).toBeGreaterThanOrEqual(0) + expect(Number.isFinite(r5.botADamage)).toBe(true) + expect(Number.isFinite(r5.botBDamage)).toBe(true) + } + }) + it('Elo difference correlates with win rate', () => { const scenarios = [ { eloA: 1400, eloB: 1400, expectedAWinMin: 0.40, expectedAWinMax: 0.60 },