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:
co-authored by
Claude Opus 4.6
parent
a358374d71
commit
2f5fe4f350
@@ -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([])
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user