UNPKG

@ursamu/core

Version:

Ursamu - Modular MUD Engine with sandboxed scripting and plugin system

1,525 lines (1,213 loc) 40.8 kB
# Debugging and Testing Guide This comprehensive guide covers debugging techniques, testing strategies, and troubleshooting methods for Ursamu. ## Table of Contents - [Debugging Overview](#debugging-overview) - [Debug Configuration](#debug-configuration) - [Logging and Monitoring](#logging-and-monitoring) - [Interactive Debugging](#interactive-debugging) - [Performance Profiling](#performance-profiling) - [Testing Strategies](#testing-strategies) - [Unit Testing](#unit-testing) - [Integration Testing](#integration-testing) - [Load Testing](#load-testing) - [Debugging Plugins](#debugging-plugins) - [Debugging Commands](#debugging-commands) - [Hot-Reload Debugging](#hot-reload-debugging) - [Network Debugging](#network-debugging) - [Database Debugging](#database-debugging) - [Common Issues](#common-issues) - [Troubleshooting Tools](#troubleshooting-tools) - [Production Debugging](#production-debugging) ## Debugging Overview The MUD engine provides comprehensive debugging capabilities through: ### Debug Levels ```typescript enum DebugLevel { TRACE = 0, DEBUG = 1, INFO = 2, WARN = 3, ERROR = 4, FATAL = 5 } ``` ### Debug Categories - **Engine Core** - Main engine systems - **Plugins** - Plugin loading and execution - **Commands** - Command processing and execution - **Network** - Protocol and connection handling - **Database** - Data persistence and queries - **Hot-Reload** - File watching and reloading - **Virtual Machine** - Script execution and sandboxing ## Debug Configuration ### Environment Variables ```bash # General debugging DEBUG=ursamu:* # Enable all debug output DEBUG=ursamu:core,ursamu:plugins # Enable specific categories DEBUG_LEVEL=debug # Set minimum debug level DEBUG_COLOR=true # Enable colorized output # Component-specific debugging DEBUG_PLUGINS=true # Plugin debugging DEBUG_COMMANDS=true # Command debugging DEBUG_HOTRELOAD=true # Hot-reload debugging DEBUG_NETWORK=true # Network debugging DEBUG_VM=true # Virtual machine debugging # Performance debugging DEBUG_PERFORMANCE=true # Performance monitoring DEBUG_MEMORY=true # Memory usage tracking DEBUG_TIMINGS=true # Execution timing ``` ### Configuration File ```json // config/debug.json { "debug": { "enabled": true, "level": "debug", "categories": { "core": true, "plugins": true, "commands": true, "network": false, "database": true, "hotreload": true, "vm": true }, "output": { "console": true, "file": "logs/debug.log", "structured": true, "timestamps": true, "colors": true }, "performance": { "enabled": true, "slowQueryThreshold": 100, "slowCommandThreshold": 1000, "memoryTrackingInterval": 30000 } } } ``` ### Runtime Debug Control ```typescript // Enable/disable debugging at runtime import { DebugManager } from './src/debug/DebugManager.js'; const debugManager = new DebugManager(); // Toggle debug categories debugManager.enable('plugins'); debugManager.disable('network'); // Set debug level debugManager.setLevel('debug'); // Get debug status const status = debugManager.getStatus(); ``` ## Logging and Monitoring ### Structured Logging ```typescript import { Logger } from './src/utils/Logger.js'; export class ExamplePlugin implements Plugin { private logger: Logger; async onLoad(context: PluginContext): Promise<void> { this.logger = context.logger.createChild('ExamplePlugin'); // Structured logging this.logger.info('Plugin loading', { pluginName: this.name, version: this.version, loadTime: Date.now() }); // Context-aware logging this.logger.debug('Registering commands', { commands: ['example', 'test'], category: 'setup' }); // Error logging with stack traces try { await this.riskyOperation(); } catch (error) { this.logger.error('Failed to initialize plugin', { error: error.message, stack: error.stack, operation: 'riskyOperation' }); } } private async commandHandler(player: Player, args: string[]): Promise<CommandResult> { const timer = this.logger.startTimer(); this.logger.debug('Command execution started', { player: player.name, command: 'example', args }); try { const result = await this.executeCommand(player, args); timer.done('Command completed', { success: result.success, duration: timer.duration }); return result; } catch (error) { this.logger.error('Command execution failed', { player: player.name, error: error.message, duration: timer.duration }); throw error; } } } ``` ### Log Analysis ```bash # Real-time log monitoring tail -f logs/debug.log | grep -E "(ERROR|WARN)" # Search for specific patterns grep -i "plugin.*error" logs/*.log # Analyze performance logs awk '/Command.*duration/ { print $0 }' logs/debug.log | sort -k6 -n # Extract plugin loading times grep "Plugin.*loaded" logs/debug.log | \ sed 's/.*loaded in \([0-9]*\)ms.*/\1/' | \ awk '{ sum += $1; count++ } END { print "Average:", sum/count, "ms" }' ``` ### Monitoring Dashboard ```typescript // Real-time debug dashboard export class DebugDashboard { private metrics = new Map<string, any>(); private connections = new Set<WebSocket>(); startServer(port: number = 3001): void { const server = new WebSocketServer({ port }); server.on('connection', (ws) => { this.connections.add(ws); // Send current metrics ws.send(JSON.stringify({ type: 'metrics', data: Object.fromEntries(this.metrics) })); ws.on('close', () => { this.connections.delete(ws); }); }); // Update metrics every second setInterval(() => { this.updateMetrics(); this.broadcastMetrics(); }, 1000); } private updateMetrics(): void { const engine = MUDEngine.getInstance(); this.metrics.set('players', { online: engine.playerManager.getOnlineCount(), total: engine.playerManager.getTotalCount() }); this.metrics.set('performance', { memory: process.memoryUsage(), uptime: process.uptime(), cpu: process.cpuUsage() }); this.metrics.set('plugins', { loaded: engine.pluginManager.getLoadedCount(), active: engine.pluginManager.getActiveCount(), errors: engine.pluginManager.getErrorCount() }); } private broadcastMetrics(): void { const data = JSON.stringify({ type: 'metrics', data: Object.fromEntries(this.metrics), timestamp: Date.now() }); for (const ws of this.connections) { if (ws.readyState === WebSocket.OPEN) { ws.send(data); } } } } ``` ## Interactive Debugging ### Debug Console ```typescript // Built-in debug console export class DebugConsole { private commands = new Map<string, Function>(); constructor(private engine: MUDEngine) { this.registerDebugCommands(); } private registerDebugCommands(): void { // Plugin debugging this.commands.set('plugins', () => { const plugins = this.engine.pluginManager.getAll(); return plugins.map(p => ({ name: p.name, version: p.version, status: p.status, loaded: p.loadTime, errors: p.errors.length })); }); // Player debugging this.commands.set('players', () => { return this.engine.playerManager.getOnlinePlayers().map(p => ({ name: p.name, id: p.id, room: p.getCurrentRoom()?.id, connected: Date.now() - p.connectionTime })); }); // Performance debugging this.commands.set('perf', () => { return { memory: process.memoryUsage(), cpu: process.cpuUsage(), uptime: process.uptime(), eventLoop: this.getEventLoopLag() }; }); // Command debugging this.commands.set('commands', (pattern?: string) => { const commands = this.engine.commandRegistry.getAll(); const filtered = pattern ? commands.filter(c => c.name.includes(pattern)) : commands; return filtered.map(c => ({ name: c.name, category: c.category, executions: c.executionCount, errors: c.errorCount, avgTime: c.averageExecutionTime })); }); // Hot-reload debugging this.commands.set('hotreload', () => { const status = this.engine.hotReload.getStatus(); const history = this.engine.hotReload.getHistory(5); return { active: status.active, reloads: status.totalReloads, failures: status.failedReloads, recentHistory: history }; }); } async execute(command: string, ...args: any[]): Promise<any> { const handler = this.commands.get(command); if (!handler) { throw new Error(`Unknown debug command: ${command}`); } return await handler(...args); } getAvailableCommands(): string[] { return Array.from(this.commands.keys()); } } // Usage in REPL const console = new DebugConsole(engine); await console.execute('plugins'); await console.execute('commands', 'attack'); ``` ### Breakpoint System ```typescript // Conditional breakpoints export class Breakpoints { private static breakpoints = new Map<string, Function>(); static set(id: string, condition: () => boolean): void { this.breakpoints.set(id, condition); } static check(id: string, context?: any): boolean { const condition = this.breakpoints.get(id); if (!condition) return false; if (condition()) { console.log(`Breakpoint hit: ${id}`, context); debugger; // Trigger debugger return true; } return false; } static remove(id: string): void { this.breakpoints.delete(id); } } // Usage in code export class CommandProcessor { async execute(player: Player, input: string): Promise<CommandResult> { // Conditional breakpoint Breakpoints.check('command-execute', { player: player.name, command: input, room: player.getCurrentRoom()?.id }); // Set breakpoint condition Breakpoints.set('slow-command', () => { return Date.now() - startTime > 1000; // Break on slow commands }); const result = await this.processCommand(player, input); Breakpoints.check('slow-command'); return result; } } ``` ## Performance Profiling ### CPU Profiling ```typescript import { createRequire } from 'module'; const require = createRequire(import.meta.url); const v8Profiler = require('v8-profiler-next'); export class CPUProfiler { private profiling = false; private startTime = 0; start(title: string): void { if (this.profiling) { throw new Error('Profiler already running'); } this.profiling = true; this.startTime = Date.now(); v8Profiler.startProfiling(title); } stop(title: string): any { if (!this.profiling) { throw new Error('Profiler not running'); } const profile = v8Profiler.stopProfiling(title); this.profiling = false; const duration = Date.now() - this.startTime; return { duration, profile: profile.export(), summary: this.analyzeProfil(profile) }; } private analyzeProfil(profile: any): any { // Analyze CPU profile data const nodes = profile.nodes || []; const samples = profile.samples || []; // Find hotspots const hotspots = nodes .filter(node => node.hitCount > 0) .sort((a, b) => b.hitCount - a.hitCount) .slice(0, 10); return { totalSamples: samples.length, hotspots: hotspots.map(node => ({ function: node.callFrame.functionName, file: node.callFrame.url, line: node.callFrame.lineNumber, hits: node.hitCount })) }; } } // Usage const profiler = new CPUProfiler(); profiler.start('command-processing'); await processCommands(); const results = profiler.stop('command-processing'); console.log('Profiling Results:', results.summary); ``` ### Memory Profiling ```typescript export class MemoryProfiler { private snapshots: any[] = []; takeSnapshot(label: string): void { const snapshot = { label, timestamp: Date.now(), memory: process.memoryUsage(), heap: v8.getHeapSnapshot ? v8.getHeapSnapshot() : null }; this.snapshots.push(snapshot); } compareSnapshots(before: string, after: string): any { const beforeSnapshot = this.snapshots.find(s => s.label === before); const afterSnapshot = this.snapshots.find(s => s.label === after); if (!beforeSnapshot || !afterSnapshot) { throw new Error('Snapshots not found'); } const memoryDiff = { rss: afterSnapshot.memory.rss - beforeSnapshot.memory.rss, heapUsed: afterSnapshot.memory.heapUsed - beforeSnapshot.memory.heapUsed, heapTotal: afterSnapshot.memory.heapTotal - beforeSnapshot.memory.heapTotal, external: afterSnapshot.memory.external - beforeSnapshot.memory.external }; return { duration: afterSnapshot.timestamp - beforeSnapshot.timestamp, memoryDiff, leakSuspected: memoryDiff.heapUsed > 10 * 1024 * 1024 // 10MB threshold }; } detectLeaks(): any[] { if (this.snapshots.length < 2) { return []; } const leaks = []; for (let i = 1; i < this.snapshots.length; i++) { const comparison = this.compareSnapshots( this.snapshots[i-1].label, this.snapshots[i].label ); if (comparison.leakSuspected) { leaks.push({ from: this.snapshots[i-1].label, to: this.snapshots[i].label, ...comparison }); } } return leaks; } } // Usage const memProfiler = new MemoryProfiler(); memProfiler.takeSnapshot('before-plugin-load'); await loadPlugins(); memProfiler.takeSnapshot('after-plugin-load'); const comparison = memProfiler.compareSnapshots('before-plugin-load', 'after-plugin-load'); console.log('Memory impact of plugin loading:', comparison); ``` ### Performance Metrics ```typescript export class PerformanceMonitor { private metrics = new Map<string, any>(); private timers = new Map<string, number>(); startTimer(name: string): void { this.timers.set(name, Date.now()); } endTimer(name: string): number { const startTime = this.timers.get(name); if (!startTime) { throw new Error(`Timer '${name}' not found`); } const duration = Date.now() - startTime; this.timers.delete(name); this.recordMetric(name, duration); return duration; } recordMetric(name: string, value: number): void { if (!this.metrics.has(name)) { this.metrics.set(name, { count: 0, sum: 0, min: Infinity, max: -Infinity, avg: 0 }); } const metric = this.metrics.get(name); metric.count++; metric.sum += value; metric.min = Math.min(metric.min, value); metric.max = Math.max(metric.max, value); metric.avg = metric.sum / metric.count; } getMetric(name: string): any { return this.metrics.get(name) || null; } getAllMetrics(): any { return Object.fromEntries(this.metrics); } reset(name?: string): void { if (name) { this.metrics.delete(name); } else { this.metrics.clear(); } } } // Usage with decorators function monitor(metricName?: string) { return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) { const originalMethod = descriptor.value; const name = metricName || `${target.constructor.name}.${propertyKey}`; descriptor.value = async function(...args: any[]) { const monitor = PerformanceMonitor.getInstance(); monitor.startTimer(name); try { const result = await originalMethod.apply(this, args); return result; } finally { monitor.endTimer(name); } }; return descriptor; }; } // Usage export class CommandProcessor { @monitor('command-processing') async processCommand(player: Player, input: string): Promise<CommandResult> { // Command processing logic } } ``` ## Testing Strategies ### Test Structure ``` tests/ ├── unit/ # Unit tests ├── core/ # Core engine tests ├── plugins/ # Plugin tests ├── commands/ # Command tests └── utils/ # Utility tests ├── integration/ # Integration tests ├── plugin-system/ # Plugin system integration ├── hot-reload/ # Hot-reload integration └── protocols/ # Protocol integration ├── e2e/ # End-to-end tests ├── gameplay/ # Gameplay scenarios ├── admin/ # Admin functionality └── load/ # Load testing ├── fixtures/ # Test data and mocks ├── helpers/ # Test utilities └── performance/ # Performance tests ``` ### Test Configuration ```json // jest.config.js { "preset": "ts-jest", "testEnvironment": "node", "setupFilesAfterEnv": ["<rootDir>/tests/setup.ts"], "testMatch": [ "**/tests/**/*.test.ts", "**/src/**/__tests__/**/*.test.ts" ], "collectCoverageFrom": [ "src/**/*.ts", "!src/**/*.d.ts", "!src/**/__tests__/**", "!src/**/index.ts" ], "coverageThreshold": { "global": { "branches": 80, "functions": 80, "lines": 80, "statements": 80 } }, "testTimeout": 30000, "maxWorkers": 4 } ``` ## Unit Testing ### Plugin Testing ```typescript // tests/unit/plugins/example-plugin.test.ts import { ExamplePlugin } from '../../../src/plugins/ExamplePlugin.js'; import { createMockContext, createMockPlayer } from '../../helpers/mocks.js'; describe('ExamplePlugin', () => { let plugin: ExamplePlugin; let mockContext: MockPluginContext; beforeEach(() => { plugin = new ExamplePlugin(); mockContext = createMockContext(); }); afterEach(() => { jest.clearAllMocks(); }); describe('Plugin Lifecycle', () => { test('should load successfully', async () => { await plugin.onLoad(mockContext); expect(mockContext.logger.info).toHaveBeenCalledWith( expect.stringContaining('loaded successfully') ); expect(mockContext.engine.commandRegistry.register).toHaveBeenCalled(); }); test('should handle load errors gracefully', async () => { mockContext.engine.commandRegistry.register.mockRejectedValue( new Error('Registration failed') ); await expect(plugin.onLoad(mockContext)).rejects.toThrow('Registration failed'); }); test('should unload cleanly', async () => { await plugin.onLoad(mockContext); await plugin.onUnload(mockContext); expect(mockContext.engine.commandRegistry.unregister).toHaveBeenCalled(); expect(mockContext.eventBus.off).toHaveBeenCalled(); }); }); describe('Command Handling', () => { beforeEach(async () => { await plugin.onLoad(mockContext); }); test('should execute example command', async () => { const player = createMockPlayer('TestPlayer'); const result = await plugin.exampleCommand(player, ['test', 'args']); expect(result.success).toBe(true); expect(result.message).toContain('TestPlayer'); expect(player.send).toHaveBeenCalled(); }); test('should handle command errors', async () => { const player = createMockPlayer('TestPlayer'); player.getCurrentRoom.mockReturnValue(null); const result = await plugin.exampleCommand(player, []); expect(result.success).toBe(false); expect(result.message).toContain('error'); }); }); describe('Event Handling', () => { test('should handle player login events', async () => { await plugin.onLoad(mockContext); const player = createMockPlayer('TestPlayer'); const loginHandler = mockContext.eventBus.on.mock.calls .find(call => call[0] === 'player:login')[1]; await loginHandler(player); // Verify welcome message sent after delay await new Promise(resolve => setTimeout(resolve, 1100)); expect(player.send).toHaveBeenCalledWith( expect.stringContaining('Welcome') ); }); }); }); ``` ### Command Testing ```typescript // tests/unit/commands/take.test.ts import { takeCommand } from '../../../src/commands/basic-commands.js'; import { createMockPlayer, createMockRoom, createMockItem } from '../../helpers/mocks.js'; describe('takeCommand', () => { let player: MockPlayer; let room: MockRoom; let context: MockCommandContext; beforeEach(() => { player = createMockPlayer('TestPlayer'); room = createMockRoom('test-room'); context = createMockContext(player, room); }); test('should take item from room', async () => { const sword = createMockItem('iron sword'); room.findItem.mockReturnValue(sword); player.canCarry.mockReturnValue(true); const result = await takeCommand.handler(player, ['sword'], context); expect(result.success).toBe(true); expect(room.removeItem).toHaveBeenCalledWith(sword, 1); expect(player.addItem).toHaveBeenCalledWith(sword, 1); expect(room.broadcast).toHaveBeenCalled(); }); test('should handle item not found', async () => { room.findItem.mockReturnValue(null); const result = await takeCommand.handler(player, ['nonexistent'], context); expect(result.success).toBe(false); expect(result.message).toContain("don't see"); }); test('should handle untakeable items', async () => { const fixture = createMockItem('heavy boulder'); fixture.canTake = false; room.findItem.mockReturnValue(fixture); const result = await takeCommand.handler(player, ['boulder'], context); expect(result.success).toBe(false); expect(result.message).toContain("can't take"); }); test('should handle carrying capacity', async () => { const heavyItem = createMockItem('anvil'); room.findItem.mockReturnValue(heavyItem); player.canCarry.mockReturnValue(false); const result = await takeCommand.handler(player, ['anvil'], context); expect(result.success).toBe(false); expect(result.message).toContain('too heavy'); }); test('should handle quantity', async () => { const coins = createMockItem('gold coins'); coins.quantity = 100; room.findItem.mockReturnValue(coins); player.canCarry.mockReturnValue(true); const result = await takeCommand.handler(player, ['coins', '50'], context); expect(result.success).toBe(true); expect(room.removeItem).toHaveBeenCalledWith(coins, 50); expect(player.addItem).toHaveBeenCalledWith(coins, 50); }); }); ``` ## Integration Testing ### Plugin System Integration ```typescript // tests/integration/plugin-system.test.ts import { MUDEngine } from '../../src/index.js'; import { ExamplePlugin } from '../../src/plugins/ExamplePlugin.js'; describe('Plugin System Integration', () => { let engine: MUDEngine; beforeEach(async () => { engine = new MUDEngine({ database: ':memory:', protocols: [], debug: { enabled: false } }); await engine.start(); }); afterEach(async () => { await engine.stop(); }); test('should load and activate plugin', async () => { const plugin = new ExamplePlugin(); await engine.pluginManager.loadPlugin(plugin); expect(engine.pluginManager.isLoaded(plugin.name)).toBe(true); expect(engine.pluginManager.isActive(plugin.name)).toBe(true); // Verify commands were registered const exampleCmd = engine.commandRegistry.get('example'); expect(exampleCmd).toBeDefined(); }); test('should handle plugin dependencies', async () => { const dependentPlugin = new DependentPlugin(); dependentPlugin.dependencies = ['example-plugin']; // Should fail without dependency await expect( engine.pluginManager.loadPlugin(dependentPlugin) ).rejects.toThrow('Missing dependency'); // Should succeed with dependency await engine.pluginManager.loadPlugin(new ExamplePlugin()); await engine.pluginManager.loadPlugin(dependentPlugin); expect(engine.pluginManager.isLoaded(dependentPlugin.name)).toBe(true); }); test('should unload plugins cleanly', async () => { const plugin = new ExamplePlugin(); await engine.pluginManager.loadPlugin(plugin); await engine.pluginManager.unloadPlugin(plugin.name); expect(engine.pluginManager.isLoaded(plugin.name)).toBe(false); expect(engine.commandRegistry.get('example')).toBeUndefined(); }); }); ``` ### Hot-Reload Integration ```typescript // tests/integration/hot-reload.test.ts import { promises as fs } from 'fs'; import { join } from 'path'; import { MUDEngine } from '../../src/index.js'; describe('Hot-Reload Integration', () => { let engine: MUDEngine; let tempDir: string; beforeEach(async () => { tempDir = join(tmpdir(), `hotreload-test-${Date.now()}`); await fs.mkdir(tempDir, { recursive: true }); engine = new MUDEngine({ hotReload: { enabled: true, watchPaths: [tempDir], debounceMs: 100 } }); await engine.start(); }); afterEach(async () => { await engine.stop(); await fs.rm(tempDir, { recursive: true, force: true }); }); test('should reload on file changes', async () => { const testFile = join(tempDir, 'test.ts'); const originalContent = 'export const test = "original";'; await fs.writeFile(testFile, originalContent); // Wait for initial compilation await waitForReload(engine, 1); // Modify file await fs.writeFile(testFile, 'export const test = "modified";'); // Wait for reload await waitForReload(engine, 2); const status = engine.hotReload.getStatus(); expect(status.totalReloads).toBe(2); expect(status.successfulReloads).toBe(2); }); test('should handle compilation errors', async () => { const testFile = join(tempDir, 'error.ts'); const invalidContent = 'export const test = "unclosed string'; await fs.writeFile(testFile, invalidContent); await waitForReload(engine, 1); const status = engine.hotReload.getStatus(); expect(status.failedReloads).toBe(1); }); test('should preserve state during reload', async () => { // Create plugin with state const pluginFile = join(tempDir, 'stateful-plugin.ts'); const pluginContent = ` export class StatefulPlugin { private counter = 0; increment() { this.counter++; } getCounter() { return this.counter; } } `; await fs.writeFile(pluginFile, pluginContent); await waitForReload(engine, 1); // Interact with plugin to create state const plugin = engine.pluginManager.get('stateful-plugin'); plugin.increment(); plugin.increment(); // Modify plugin file const modifiedContent = pluginContent.replace('counter++', 'counter += 2'); await fs.writeFile(pluginFile, modifiedContent); await waitForReload(engine, 2); // State should be preserved const reloadedPlugin = engine.pluginManager.get('stateful-plugin'); expect(reloadedPlugin.getCounter()).toBe(2); }); }); async function waitForReload(engine: MUDEngine, expectedReloads: number): Promise<void> { return new Promise((resolve) => { const checkStatus = () => { const status = engine.hotReload.getStatus(); if (status.totalReloads >= expectedReloads) { resolve(); } else { setTimeout(checkStatus, 50); } }; checkStatus(); }); } ``` ## Load Testing ### Connection Load Testing ```typescript // tests/performance/load.test.ts import WebSocket from 'ws'; import { MUDEngine } from '../../src/index.js'; describe('Load Testing', () => { let engine: MUDEngine; beforeAll(async () => { engine = new MUDEngine({ protocols: { websocket: { port: 8081, enabled: true } } }); await engine.start(); }); afterAll(async () => { await engine.stop(); }); test('should handle multiple concurrent connections', async () => { const connectionCount = 100; const connections: WebSocket[] = []; // Create connections for (let i = 0; i < connectionCount; i++) { const ws = new WebSocket('ws://localhost:8081/ws'); connections.push(ws); } // Wait for all connections to open await Promise.all( connections.map(ws => new Promise(resolve => { ws.on('open', resolve); })) ); expect(connections.length).toBe(connectionCount); expect(engine.protocolManager.getConnectionCount()).toBe(connectionCount); // Clean up connections.forEach(ws => ws.close()); }, 30000); test('should handle high command throughput', async () => { const commandCount = 1000; const commands = Array.from({ length: commandCount }, (_, i) => `test ${i}`); const player = await engine.playerManager.createTestPlayer('LoadTester'); const startTime = Date.now(); const results = await Promise.all( commands.map(cmd => engine.commandProcessor.execute(player, cmd)) ); const duration = Date.now() - startTime; const commandsPerSecond = commandCount / (duration / 1000); console.log(`Processed ${commandCount} commands in ${duration}ms (${commandsPerSecond.toFixed(2)} cmd/s)`); expect(results.every(r => r.success)).toBe(true); expect(commandsPerSecond).toBeGreaterThan(100); // At least 100 commands per second }); test('should maintain performance under memory pressure', async () => { const iterations = 10; const itemsPerIteration = 1000; for (let i = 0; i < iterations; i++) { // Create many objects to stress memory const items = Array.from({ length: itemsPerIteration }, (_, j) => engine.itemManager.createItem({ name: `item_${i}_${j}`, type: 'test' }) ); const startTime = Date.now(); // Perform operations const player = await engine.playerManager.createTestPlayer(`Tester${i}`); items.forEach(item => player.addItem(item)); const operationTime = Date.now() - startTime; console.log(`Iteration ${i + 1}: ${operationTime}ms for ${itemsPerIteration} operations`); // Performance shouldn't degrade significantly expect(operationTime).toBeLessThan(5000); // 5 seconds max // Clean up await engine.playerManager.removePlayer(player.id); } }); }); ``` ## Debugging Plugins ### Plugin Debug Utilities ```typescript // src/debug/PluginDebugger.ts export class PluginDebugger { private breakpoints = new Map<string, Function>(); private watchedProperties = new Map<string, any>(); constructor(private plugin: Plugin) { this.setupDebugging(); } private setupDebugging(): void { // Wrap plugin methods with debugging const methods = Object.getOwnPropertyNames(Object.getPrototypeOf(this.plugin)) .filter(name => typeof (this.plugin as any)[name] === 'function'); for (const method of methods) { this.wrapMethod(method); } } private wrapMethod(methodName: string): void { const originalMethod = (this.plugin as any)[methodName]; (this.plugin as any)[methodName] = async (...args: any[]) => { console.log(`[${this.plugin.name}] Calling ${methodName}`, args); const startTime = Date.now(); try { const result = await originalMethod.apply(this.plugin, args); const duration = Date.now() - startTime; console.log(`[${this.plugin.name}] ${methodName} completed in ${duration}ms`, result); return result; } catch (error) { console.error(`[${this.plugin.name}] ${methodName} failed:`, error); throw error; } }; } setBreakpoint(condition: string, callback?: Function): void { this.breakpoints.set(condition, callback || (() => debugger)); } watchProperty(propertyPath: string): void { const obj = this.resolvePropertyPath(propertyPath); const property = propertyPath.split('.').pop()!; let value = obj[property]; Object.defineProperty(obj, property, { get: () => value, set: (newValue) => { console.log(`[${this.plugin.name}] Property ${propertyPath} changed:`, value, '->', newValue); value = newValue; } }); } private resolvePropertyPath(path: string): any { return path.split('.').reduce((obj, prop) => obj[prop], this.plugin); } getState(): any { return { name: this.plugin.name, version: this.plugin.version, status: this.plugin.status, properties: this.getPublicProperties(), breakpoints: Array.from(this.breakpoints.keys()), watchedProperties: Array.from(this.watchedProperties.keys()) }; } private getPublicProperties(): any { const properties: any = {}; for (const key in this.plugin) { if (!key.startsWith('_') && typeof (this.plugin as any)[key] !== 'function') { properties[key] = (this.plugin as any)[key]; } } return properties; } } // Usage const debugger = new PluginDebugger(myPlugin); debugger.setBreakpoint('player.name === "admin"'); debugger.watchProperty('commandCount'); ``` ## Common Issues ### Memory Leaks ```typescript // Debug memory leaks in plugins export class MemoryLeakDetector { private snapshots: any[] = []; private intervals: NodeJS.Timeout[] = []; startMonitoring(plugin: Plugin): void { const interval = setInterval(() => { const snapshot = this.takeSnapshot(plugin); this.snapshots.push(snapshot); // Keep only last 10 snapshots if (this.snapshots.length > 10) { this.snapshots.shift(); } // Check for leaks if (this.snapshots.length > 3) { this.analyzeLeaks(plugin); } }, 30000); // Every 30 seconds this.intervals.push(interval); } private takeSnapshot(plugin: Plugin): any { return { timestamp: Date.now(), memory: process.memoryUsage(), pluginState: this.getPluginMemoryUsage(plugin), eventListeners: this.countEventListeners(plugin) }; } private getPluginMemoryUsage(plugin: Plugin): any { // Estimate plugin memory usage const state = JSON.stringify(plugin); return { stateSize: state.length, objectCount: (state.match(/\{/g) || []).length }; } private countEventListeners(plugin: Plugin): number { // Count event listeners if plugin uses EventEmitter if (plugin instanceof EventEmitter) { return plugin.listenerCount(); } return 0; } private analyzeLeaks(plugin: Plugin): void { const recent = this.snapshots.slice(-3); const trend = recent.map(s => s.memory.heapUsed); // Check if memory is consistently growing if (trend[2] > trend[1] && trend[1] > trend[0]) { const growth = trend[2] - trend[0]; if (growth > 10 * 1024 * 1024) { // 10MB growth console.warn(`Potential memory leak in plugin ${plugin.name}:`, { growth: `${Math.round(growth / 1024 / 1024)}MB`, trend }); } } } } ``` ### Performance Issues ```typescript // Debug performance bottlenecks export class PerformanceAnalyzer { private measurements: Map<string, number[]> = new Map(); measure<T>(name: string, fn: () => Promise<T>): Promise<T> { const start = Date.now(); return fn().then(result => { const duration = Date.now() - start; this.recordMeasurement(name, duration); return result; }); } private recordMeasurement(name: string, duration: number): void { if (!this.measurements.has(name)) { this.measurements.set(name, []); } const measurements = this.measurements.get(name)!; measurements.push(duration); // Keep only recent measurements if (measurements.length > 100) { measurements.shift(); } } analyzeBottlenecks(): any[] { const bottlenecks = []; for (const [name, measurements] of this.measurements) { const avg = measurements.reduce((a, b) => a + b, 0) / measurements.length; const max = Math.max(...measurements); const p95 = this.percentile(measurements, 95); if (avg > 100 || max > 1000) { // Slow operations bottlenecks.push({ name, average: Math.round(avg), maximum: max, p95: Math.round(p95), samples: measurements.length }); } } return bottlenecks.sort((a, b) => b.average - a.average); } private percentile(values: number[], p: number): number { const sorted = values.slice().sort((a, b) => a - b); const index = Math.ceil((p / 100) * sorted.length) - 1; return sorted[index]; } } ``` ## Troubleshooting Tools ### Health Check System ```typescript // Comprehensive system health checking export class HealthChecker { private checks: Map<string, Function> = new Map(); constructor(private engine: MUDEngine) { this.registerDefaultChecks(); } private registerDefaultChecks(): void { // Database connectivity this.checks.set('database', async () => { try { await this.engine.database.query('SELECT 1'); return { status: 'healthy', message: 'Database connected' }; } catch (error) { return { status: 'unhealthy', message: `Database error: ${error.message}` }; } }); // Plugin health this.checks.set('plugins', async () => { const plugins = this.engine.pluginManager.getAll(); const unhealthy = plugins.filter(p => p.status === 'error'); if (unhealthy.length > 0) { return { status: 'unhealthy', message: `${unhealthy.length} plugins in error state`, details: unhealthy.map(p => p.name) }; } return { status: 'healthy', message: `${plugins.length} plugins loaded` }; }); // Memory usage this.checks.set('memory', async () => { const usage = process.memoryUsage(); const usedMB = Math.round(usage.heapUsed / 1024 / 1024); const totalMB = Math.round(usage.heapTotal / 1024 / 1024); if (usedMB > 512) { // 512MB threshold return { status: 'warning', message: `High memory usage: ${usedMB}/${totalMB}MB` }; } return { status: 'healthy', message: `Memory usage: ${usedMB}/${totalMB}MB` }; }); // Event loop lag this.checks.set('eventloop', async () => { const lag = await this.measureEventLoopLag(); if (lag > 100) { // 100ms threshold return { status: 'warning', message: `Event loop lag: ${lag}ms` }; } return { status: 'healthy', message: `Event loop lag: ${lag}ms` }; }); } async runAllChecks(): Promise<any> { const results: any = {}; for (const [name, check] of this.checks) { try { results[name] = await check(); } catch (error) { results[name] = { status: 'error', message: `Health check failed: ${error.message}` }; } } const overallStatus = this.determineOverallStatus(results); return { status: overallStatus, timestamp: new Date().toISOString(), checks: results }; } private determineOverallStatus(results: any): string { const statuses = Object.values(results).map((r: any) => r.status); if (statuses.includes('error') || statuses.includes('unhealthy')) { return 'unhealthy'; } if (statuses.includes('warning')) { return 'warning'; } return 'healthy'; } private async measureEventLoopLag(): Promise<number> { return new Promise(resolve => { const start = Date.now(); setImmediate(() => { resolve(Date.now() - start); }); }); } } ``` This comprehensive debugging and testing guide provides all the tools and techniques needed to maintain a robust, well-tested MUD engine. The examples progress from basic debugging techniques to advanced performance analysis and production monitoring.