test: add static analysis tests for memory leak destroy contracts
Verifies FightScene.destroy(), FightViewer.vue onUnmounted, and audio module cleanup invariants via source-code scanning — ensures future changes don't silently break resource cleanup. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
854b1cd1df
commit
08437cdf5c
@@ -0,0 +1,107 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { readFileSync } from 'fs'
|
||||
import { resolve, dirname } from 'path'
|
||||
|
||||
const gameDir = resolve(dirname(import.meta.url.replace('file://', '')), '..')
|
||||
const readGame = (rel: string) => readFileSync(resolve(gameDir, rel), 'utf-8')
|
||||
|
||||
describe('memory leak audit — static analysis', () => {
|
||||
describe('FightScene.destroy() cleanup contract', () => {
|
||||
const src = readGame('FightScene.ts')
|
||||
|
||||
it('stops music', () => {
|
||||
expect(src).toContain('stopMusic()')
|
||||
})
|
||||
|
||||
it('stops talking animations', () => {
|
||||
expect(src).toContain("stopTalking('a')")
|
||||
expect(src).toContain("stopTalking('b')")
|
||||
})
|
||||
|
||||
it('clears speech bubble arrays', () => {
|
||||
expect(src).toContain('activeBubbleA.length = 0')
|
||||
expect(src).toContain('activeBubbleB.length = 0')
|
||||
})
|
||||
|
||||
it('clears tracked timers', () => {
|
||||
expect(src).toContain('cleanupTimers.clear()')
|
||||
})
|
||||
|
||||
it('clears audio timers', () => {
|
||||
expect(src).toContain('clearAllAudioTimers()')
|
||||
})
|
||||
|
||||
it('destroys all Kaplay objects', () => {
|
||||
expect(src).toMatch(/k\.get\('\*'\)\.forEach/)
|
||||
})
|
||||
|
||||
it('quits Kaplay engine', () => {
|
||||
expect(src).toContain('k.quit()')
|
||||
})
|
||||
})
|
||||
|
||||
describe('FightViewer.vue cleanup in onUnmounted', () => {
|
||||
const src = readGame('../components/FightViewer.vue')
|
||||
|
||||
it('calls scene.destroy()', () => {
|
||||
expect(src).toContain('scene.destroy()')
|
||||
})
|
||||
|
||||
it('nullifies scene reference', () => {
|
||||
expect(src).toContain('scene = null')
|
||||
})
|
||||
|
||||
it('calls stopAllAudio()', () => {
|
||||
expect(src).toContain('stopAllAudio()')
|
||||
})
|
||||
|
||||
it('removes canvas webglcontextlost listener', () => {
|
||||
expect(src).toContain("removeEventListener('webglcontextlost'")
|
||||
})
|
||||
|
||||
it('removes canvas webglcontextrestored listener', () => {
|
||||
expect(src).toContain("removeEventListener('webglcontextrestored'")
|
||||
})
|
||||
|
||||
it('nullifies canvas handler references', () => {
|
||||
expect(src).toContain('canvasContextLostHandler = null')
|
||||
expect(src).toContain('canvasContextRestoredHandler = null')
|
||||
})
|
||||
|
||||
it('clears cleanup timer handles', () => {
|
||||
expect(src).toMatch(/clearTimeout\(cleanupTimerHandle\)/)
|
||||
})
|
||||
|
||||
it('cancels speechSynthesis', () => {
|
||||
expect(src).toContain('speechSynthesis.cancel()')
|
||||
})
|
||||
})
|
||||
|
||||
describe('audio module does not leak per-scene resources', () => {
|
||||
it('audioTimeout cleanup function exists', () => {
|
||||
const ctx = readGame('audio/context.ts')
|
||||
expect(ctx).toContain('export function clearAllAudioTimers()')
|
||||
})
|
||||
|
||||
it('TTS cache has bounded size (MAX_CACHE)', () => {
|
||||
const tts = readGame('tts.ts')
|
||||
expect(tts).toMatch(/const MAX_CACHE\s*=\s*\d+/)
|
||||
})
|
||||
|
||||
it('TTS cache uses LRU eviction', () => {
|
||||
const tts = readGame('tts.ts')
|
||||
expect(tts).toContain('lastAccess')
|
||||
expect(tts).toMatch(/lruKey|lruTime/)
|
||||
})
|
||||
|
||||
it('kokoroClearCache export exists', () => {
|
||||
const tts = readGame('tts.ts')
|
||||
expect(tts).toContain('export function kokoroClearCache()')
|
||||
})
|
||||
|
||||
it('kokoroStop export exists', () => {
|
||||
const tts = readGame('tts.ts')
|
||||
expect(tts).toContain('export function kokoroStop()')
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user