From 728afacc5c6333df04cec651416e924a0f48dbb2 Mon Sep 17 00:00:00 2001 From: Dorian Date: Sat, 7 Mar 2026 23:58:41 +0000 Subject: [PATCH] =?UTF-8?q?test:=20add=20mock=20and=20arena=20tests=20?= =?UTF-8?q?=E2=80=94=2021=20more=20tests=20(86=20total)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - mock.test.ts: 7 tests for ELO-weighted response quality, failure rates, timing - arenas.test.ts: 14 tests for arena data integrity, pickArena, randomArena Co-Authored-By: Claude Opus 4.6 --- server/src/engine/arenas.test.ts | 105 +++++++++++++++++++++++++++++++ server/src/engine/mock.test.ts | 104 ++++++++++++++++++++++++++++++ 2 files changed, 209 insertions(+) create mode 100644 server/src/engine/arenas.test.ts create mode 100644 server/src/engine/mock.test.ts diff --git a/server/src/engine/arenas.test.ts b/server/src/engine/arenas.test.ts new file mode 100644 index 0000000..9664d0a --- /dev/null +++ b/server/src/engine/arenas.test.ts @@ -0,0 +1,105 @@ +import { describe, it, expect } from 'vitest' +import { ARENAS, pickArena, randomArena } from './arenas.js' + +describe('ARENAS', () => { + it('has 25 arenas', () => { + expect(ARENAS.length).toBe(25) + }) + + it('all arenas have required fields', () => { + for (const arena of ARENAS) { + expect(arena.id).toBeTruthy() + expect(arena.name).toBeTruthy() + expect(arena.description).toBeTruthy() + expect(typeof arena.id).toBe('string') + expect(typeof arena.name).toBe('string') + expect(typeof arena.description).toBe('string') + } + }) + + it('all arena IDs are unique', () => { + const ids = ARENAS.map(a => a.id) + expect(new Set(ids).size).toBe(ids.length) + }) + + it('all arena names are unique', () => { + const names = ARENAS.map(a => a.name) + expect(new Set(names).size).toBe(names.length) + }) + + it('arenas with modifier also have modifierDescription', () => { + for (const arena of ARENAS) { + if (arena.modifier) { + expect(arena.modifierDescription).toBeTruthy() + } + } + }) + + it('arenas without modifier have null modifierDescription', () => { + for (const arena of ARENAS) { + if (!arena.modifier) { + expect(arena.modifierDescription).toBeNull() + } + } + }) + + it('known modifiers are present', () => { + const modifiers = ARENAS.map(a => a.modifier).filter(Boolean) + expect(modifiers).toContain('speed_2x') + expect(modifiers).toContain('roast_2x') + expect(modifiers).toContain('food_2x') + expect(modifiers).toContain('meme_2x') + }) +}) + +describe('pickArena', () => { + it('same choices return The Singularity', () => { + const arena = pickArena(5, 5) + expect(arena.id).toBe('the_singularity') + }) + + it('different choices return a non-singularity arena', () => { + const arena = pickArena(3, 7) + expect(arena.id).not.toBe('the_singularity') + }) + + it('deterministic for same inputs', () => { + const a1 = pickArena(10, 20) + const a2 = pickArena(10, 20) + expect(a1.id).toBe(a2.id) + }) + + it('different inputs give different arenas (most of the time)', () => { + const arenas = new Set() + for (let i = 0; i < 50; i++) { + arenas.add(pickArena(i, i + 1).id) + } + expect(arenas.size).toBeGreaterThan(3) + }) +}) + +describe('randomArena', () => { + it('never returns The Singularity', () => { + for (let i = 0; i < 50; i++) { + const arena = randomArena() + expect(arena.id).not.toBe('the_singularity') + } + }) + + it('returns variety', () => { + const arenas = new Set() + for (let i = 0; i < 100; i++) { + arenas.add(randomArena().id) + } + expect(arenas.size).toBeGreaterThan(5) + }) + + it('returns valid arena objects', () => { + for (let i = 0; i < 20; i++) { + const arena = randomArena() + expect(arena.id).toBeTruthy() + expect(arena.name).toBeTruthy() + expect(arena.description).toBeTruthy() + } + }) +}) diff --git a/server/src/engine/mock.test.ts b/server/src/engine/mock.test.ts new file mode 100644 index 0000000..79f34b2 --- /dev/null +++ b/server/src/engine/mock.test.ts @@ -0,0 +1,104 @@ +import { describe, it, expect } from 'vitest' +import { mockResponse } from './mock.js' +import { pickChallenge } from './challenges.js' + +describe('mockResponse', () => { + it('returns a valid response structure', () => { + const challenge = pickChallenge(new Set(), null) + const resp = mockResponse(challenge, 'confident', 1500) + expect(resp).toHaveProperty('answer') + expect(resp).toHaveProperty('trashTalk') + expect(resp).toHaveProperty('timeMs') + expect(resp).toHaveProperty('timedOut') + expect(resp).toHaveProperty('error') + expect(typeof resp.timeMs).toBe('number') + expect(typeof resp.timedOut).toBe('boolean') + expect(typeof resp.error).toBe('boolean') + }) + + it('high ELO bots answer correctly more often (factual)', () => { + let highCorrect = 0 + let lowCorrect = 0 + const runs = 200 + + for (let i = 0; i < runs; i++) { + const challenge = pickChallenge(new Set(), null) + if (challenge.scoring !== 'factual' || !challenge.answers?.length) continue + + const highResp = mockResponse(challenge, 'confident', 1900) + const lowResp = mockResponse(challenge, 'clueless', 900) + + if (!highResp.timedOut && !highResp.error && challenge.answers.includes(highResp.answer)) highCorrect++ + if (!lowResp.timedOut && !lowResp.error && challenge.answers.includes(lowResp.answer)) lowCorrect++ + } + + // High ELO should answer correctly more often + expect(highCorrect).toBeGreaterThan(lowCorrect) + }) + + it('low ELO bots time out / error more often', () => { + let highFails = 0 + let lowFails = 0 + const runs = 500 + + for (let i = 0; i < runs; i++) { + const challenge = pickChallenge(new Set(), null) + const highResp = mockResponse(challenge, 'confident', 1900) + const lowResp = mockResponse(challenge, 'clueless', 900) + + if (highResp.timedOut || highResp.error) highFails++ + if (lowResp.timedOut || lowResp.error) lowFails++ + } + + expect(lowFails).toBeGreaterThan(highFails) + }) + + it('high ELO bots respond faster on average', () => { + let highTotal = 0 + let lowTotal = 0 + let highCount = 0 + let lowCount = 0 + const runs = 200 + + for (let i = 0; i < runs; i++) { + const challenge = pickChallenge(new Set(), null) + const highResp = mockResponse(challenge, 'confident', 1900) + const lowResp = mockResponse(challenge, 'clueless', 900) + + if (!highResp.timedOut && !highResp.error) { highTotal += highResp.timeMs; highCount++ } + if (!lowResp.timedOut && !lowResp.error) { lowTotal += lowResp.timeMs; lowCount++ } + } + + const highAvg = highTotal / Math.max(highCount, 1) + const lowAvg = lowTotal / Math.max(lowCount, 1) + expect(highAvg).toBeLessThan(lowAvg) + }) + + it('creative challenges return non-empty answers', () => { + for (let i = 0; i < 50; i++) { + const challenge = pickChallenge(new Set(), null) + if (challenge.scoring !== 'creative') continue + + const resp = mockResponse(challenge, 'witty', 1500) + if (!resp.timedOut && !resp.error) { + expect(resp.answer.length).toBeGreaterThan(0) + } + } + }) + + it('trash talk is always a string', () => { + for (let i = 0; i < 20; i++) { + const challenge = pickChallenge(new Set(), null) + const resp = mockResponse(challenge, 'confident', 1900) + expect(typeof resp.trashTalk).toBe('string') + } + }) + + it('timeMs is always positive', () => { + for (let i = 0; i < 50; i++) { + const challenge = pickChallenge(new Set(), null) + const resp = mockResponse(challenge, 'confident', 1500) + expect(resp.timeMs).toBeGreaterThan(0) + } + }) +})