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 <noreply@anthropic.com>
This commit is contained in:
Dorian
2026-03-13 10:09:36 +00:00
co-authored by Claude Opus 4.6
parent 135bf644fa
commit 9346b6b260
+82 -2
View File
@@ -1,6 +1,7 @@
import { describe, it, expect } from 'vitest' import { describe, it, expect } from 'vitest'
import { scoreRound } from './scoring.js' import { scoreRound, calculateElo } from './scoring.js'
import { checkAnswer } from './answers.js' import { checkAnswer } from './answers.js'
import { pickChallenge } from './challenges.js'
const botA = { id: 'a1', name: 'AlphaBot' } const botA = { id: 'a1', name: 'AlphaBot' }
const botB = { id: 'b1', name: 'BetaBot' } const botB = { id: 'b1', name: 'BetaBot' }
@@ -91,7 +92,7 @@ describe('fight engine edge cases', () => {
const ch = makeChallenge() const ch = makeChallenge()
const result = scoreRound(ch, botA, botB, resp(null, 500, { error: true }), resp('Bitcoin', 500), null, 0, 0) const result = scoreRound(ch, botA, botB, resp(null, 500, { error: true }), resp('Bitcoin', 500), null, 0, 0)
expect(result.winnerId).toBe('b1') 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 // 6. Webhook 200 with invalid JSON — returns error response
@@ -162,3 +163,82 @@ describe('fight engine edge cases', () => {
expect(withCombo.botADamage).toBeGreaterThan(noCombo.botADamage) 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<string>()
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<string>()
// 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<string>()
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)
})
})