From 2f5fe4f350da0d1d162d735167d141f195056aeb Mon Sep 17 00:00:00 2001 From: Dorian Date: Fri, 13 Mar 2026 12:35:48 +0000 Subject: [PATCH] test: add regression tests for BUG-4, BUG-6, BUG-7, BUG-F2 Source pattern verification tests: - BUG-6: webhook calls wrapped in Promise.all (parallel, not sequential) - BUG-7: SSE maps (spectatorCounts, fightReactions, ssePerIp) cleaned on disconnect - BUG-4: no raw setTimeout in game code (all use trackedTimeout) - BUG-F2: no silent .catch(() => {}) in frontend source 43 regression tests total, all passing. Co-Authored-By: Claude Opus 4.6 --- server/src/engine/regression.test.ts | 95 ++++++++++++++++++++++++++++ 1 file changed, 95 insertions(+) diff --git a/server/src/engine/regression.test.ts b/server/src/engine/regression.test.ts index 32fd70d..6011b6a 100644 --- a/server/src/engine/regression.test.ts +++ b/server/src/engine/regression.test.ts @@ -4,6 +4,8 @@ * BUG-1 through BUG-12, BUG-S1 through BUG-S10, BUG-F1 through BUG-F14. */ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' +import fs from 'node:fs' +import path from 'node:path' // --- Pure function imports (no DB mocking needed) --- import { scoreRound, calculateElo, calculateTier } from './scoring.js' @@ -349,3 +351,96 @@ describe('regression tests — events cleanup', () => { unsub() }) }) + +/** Recursively collect .ts and .vue files from a directory */ +function walkTs(dir: string): string[] { + const results: string[] = [] + if (!fs.existsSync(dir)) return results + for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { + const full = path.join(dir, entry.name) + if (entry.isDirectory()) { + if (entry.name === 'node_modules' || entry.name === 'dist') continue + results.push(...walkTs(full)) + } else if (/\.(ts|vue)$/.test(entry.name)) { + results.push(full) + } + } + return results +} + +describe('regression tests — source pattern checks', () => { + // BUG-6: Webhook calls must be parallel (Promise.all), not sequential + it('BUG-6: bot webhook calls are wrapped in Promise.all', () => { + const src = fs.readFileSync( + path.resolve(__dirname, 'orchestrator.ts'), + 'utf-8', + ) + // getBotResponse calls must be inside Promise.all + expect(src).toMatch(/Promise\.all\(\[\s*\n?\s*getBotResponse\(/) + // trackWebhookResult calls also parallel + expect(src).toMatch(/Promise\.all\(\[\s*\n?\s*trackWebhookResult\(/) + }) + + // BUG-7: SSE maps cleaned on fight end + it('BUG-7: SSE maps are cleaned up when spectator disconnects', () => { + const src = fs.readFileSync( + path.resolve(__dirname, '../routes/fights.ts'), + 'utf-8', + ) + expect(src).toContain('spectatorCounts.delete(fightId)') + expect(src).toContain('fightReactions.delete(fightId)') + expect(src).toContain('ssePerIp.delete(clientIp)') + }) + + // BUG-4: no raw setTimeout in frontend game code (must use trackedTimeout) + it('BUG-4: no raw setTimeout in frontend game code', () => { + const gameDir = path.resolve(__dirname, '../../../frontend/src/game') + const files = walkTs(gameDir) + const violations: string[] = [] + + for (const file of files) { + // Skip test files, TTS (Web Worker), and audio infrastructure (has own tracking) + if (file.includes('.test.') || file.includes('.spec.') || file.endsWith('tts.ts') || file.endsWith('tts-worker.ts') || file.includes('/audio/')) continue + const src = fs.readFileSync(file, 'utf-8') + const lines = src.split('\n') + for (let i = 0; i < lines.length; i++) { + const line = lines[i] + // Allow: trackedTimeout implementations (which wrap raw setTimeout), + // sprite load timeout promises, and audio timer implementations + if ( + /(? {}) swallowing errors + it('BUG-F2: no silent .catch(() => {}) in frontend source', () => { + const srcDir = path.resolve(__dirname, '../../../frontend/src') + const files = walkTs(srcDir) + const violations: string[] = [] + + for (const file of files) { + if (file.includes('.test.') || file.includes('.spec.') || file.includes('node_modules')) continue + const src = fs.readFileSync(file, 'utf-8') + const lines = src.split('\n') + for (let i = 0; i < lines.length; i++) { + const line = lines[i] + if (/\.catch\(\(\)\s*=>\s*\{\s*\}\)/.test(line)) { + violations.push(`${path.relative(srcDir, file)}:${i + 1}: ${line.trim()}`) + } + } + } + + expect(violations).toEqual([]) + }) +})