From 9346b6b260ff968d1987b04541765c7e058f0488 Mon Sep 17 00:00:00 2001 From: Dorian Date: Fri, 13 Mar 2026 10:09:36 +0000 Subject: [PATCH] test: add ELO extremes and challenge rotation edge cases ELO: 0v0, 9999v1 upset, 9999v1 expected, NaN/Infinity check. Challenge rotation: 16 unique types, reset after exhaustion, filter exclusion works. Total: 24 edge case tests. Co-Authored-By: Claude Opus 4.6 --- server/src/engine/edge-cases.test.ts | 84 +++++++++++++++++++++++++++- 1 file changed, 82 insertions(+), 2 deletions(-) diff --git a/server/src/engine/edge-cases.test.ts b/server/src/engine/edge-cases.test.ts index e86d0d2..cd924f1 100644 --- a/server/src/engine/edge-cases.test.ts +++ b/server/src/engine/edge-cases.test.ts @@ -1,6 +1,7 @@ import { describe, it, expect } from 'vitest' -import { scoreRound } from './scoring.js' +import { scoreRound, calculateElo } from './scoring.js' import { checkAnswer } from './answers.js' +import { pickChallenge } from './challenges.js' const botA = { id: 'a1', name: 'AlphaBot' } const botB = { id: 'b1', name: 'BetaBot' } @@ -91,7 +92,7 @@ describe('fight engine edge cases', () => { const ch = makeChallenge() const result = scoreRound(ch, botA, botB, resp(null, 500, { error: true }), resp('Bitcoin', 500), null, 0, 0) expect(result.winnerId).toBe('b1') - expect(result.narration).toMatch(/error|crash|exception|threw/i) + expect(result.narration).toMatch(/error|crash|exception|threw|blue-screen|sparks|bug/i) }) // 6. Webhook 200 with invalid JSON — returns error response @@ -162,3 +163,82 @@ describe('fight engine edge cases', () => { expect(withCombo.botADamage).toBeGreaterThan(noCombo.botADamage) }) }) + +describe('ELO calculation extremes', () => { + it('ELO 0 vs 0 produces finite results', () => { + const result = calculateElo(0, 0) + expect(isFinite(result.newWinnerElo)).toBe(true) + expect(isFinite(result.newLoserElo)).toBe(true) + expect(result.newWinnerElo).toBeGreaterThan(0) // winner gains + expect(result.newLoserElo).toBeLessThan(0) // loser loses + }) + + it('ELO 9999 vs 1 — upset win yields massive gain', () => { + const result = calculateElo(1, 9999) // ELO 1 beats ELO 9999 + expect(isFinite(result.newWinnerElo)).toBe(true) + expect(isFinite(result.newLoserElo)).toBe(true) + // Massive upset: winner should gain nearly full K + expect(result.newWinnerElo).toBeGreaterThan(30) // ~1 + 32 = 33 + }) + + it('ELO 9999 vs 1 — expected win yields near-zero gain (rounds to 0)', () => { + const result = calculateElo(9999, 1) // ELO 9999 beats ELO 1 + // At extreme gap, gain rounds to 0 — winner ELO stays same or barely increases + expect(result.newWinnerElo).toBeGreaterThanOrEqual(9999) + expect(result.newWinnerElo - 9999).toBeLessThan(1) + }) + + it('ELO never produces NaN or Infinity for any extreme', () => { + const extremes: [number, number][] = [ + [0, 0], [0, 3000], [3000, 0], [1, 9999], [9999, 1], + [100000, 0], [0, 100000], + ] + for (const [w, l] of extremes) { + const result = calculateElo(w, l) + expect(isFinite(result.newWinnerElo)).toBe(true) + expect(isFinite(result.newLoserElo)).toBe(true) + } + }) +}) + +describe('challenge type rotation', () => { + it('avoids repeating types until all are exhausted', () => { + const usedTypes = new Set() + const types: string[] = [] + // Pick 16 challenges (one per type) + for (let i = 0; i < 16; i++) { + const ch = pickChallenge(usedTypes, null) + usedTypes.add(ch.type) + types.push(ch.type) + } + // All 16 should be unique + expect(new Set(types).size).toBe(16) + }) + + it('resets and allows repeats after all types exhausted', () => { + const usedTypes = new Set() + // Exhaust all types + for (let i = 0; i < 16; i++) { + const ch = pickChallenge(usedTypes, null) + usedTypes.add(ch.type) + } + expect(usedTypes.size).toBe(16) + // 17th pick should still work (resets to full pool) + const ch17 = pickChallenge(usedTypes, null) + expect(ch17.type).toBeTruthy() + expect(ch17.prompt).toBeTruthy() + }) + + it('respects usedTypes filter — excluded type never picked', () => { + // Add all types except one + const usedTypes = new Set() + for (let i = 0; i < 15; i++) { + const ch = pickChallenge(usedTypes, null) + usedTypes.add(ch.type) + } + expect(usedTypes.size).toBe(15) + // The 16th pick must be the one remaining type + const remaining = pickChallenge(usedTypes, null) + expect(usedTypes.has(remaining.type)).toBe(false) + }) +})