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 <noreply@anthropic.com>
This commit is contained in:
Dorian
2026-03-13 12:35:48 +00:00
co-authored by Claude Opus 4.6
parent a358374d71
commit 2f5fe4f350
+95
View File
@@ -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 (
/(?<!\w)setTimeout\(/.test(line) &&
!line.trim().startsWith('//') &&
!line.trim().startsWith('*') &&
!line.includes('cleanupTimers.delete') && // trackedTimeout impl
!line.includes('_audioTimers.delete') && // audio tracked timer impl
!line.includes('SPRITE_LOAD_TIMEOUT_MS') // sprite loading timeout
) {
violations.push(`${path.relative(gameDir, file)}:${i + 1}: ${line.trim()}`)
}
}
}
expect(violations).toEqual([])
})
// BUG-F2: no silent .catch(() => {}) 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([])
})
})