import { tap, expect } from '@git.zone/tstest/tapbundle'; import * as plugins from '../ts/plugins.js'; import * as paths from '../ts/paths.js'; import * as fs from 'fs/promises'; import { execSync } from 'child_process'; // Import tspm client import { tspmIpcClient } from '../ts/client/tspm.ipcclient.js'; // Test process that will crash const CRASH_SCRIPT = ` setInterval(() => { console.log('[test] Process is running...'); }, 1000); setTimeout(() => { console.error('[test] About to crash with non-zero exit code!'); process.exit(42); }, 3000); `; /** * Helper to run a CLI command and capture output */ function runCli(cmd: string): { stdout: string; stderr: string; exitCode: number } { try { const stdout = execSync(cmd, { encoding: 'utf-8', stdio: 'pipe' }); return { stdout, stderr: '', exitCode: 0 }; } catch (e: any) { return { stdout: e.stdout?.toString() || '', stderr: e.stderr?.toString() || '', exitCode: e.status ?? 1, }; } } tap.test('should create crash logs when process crashes', async (tools) => { const crashScriptPath = plugins.path.join(paths.tspmDir, 'test-crash-script.js'); const crashLogsDir = plugins.path.join(paths.tspmDir, 'crashlogs'); // Clean up any existing crash logs try { await fs.rm(crashLogsDir, { recursive: true, force: true }); } catch {} // Write the crash script await fs.writeFile(crashScriptPath, CRASH_SCRIPT); // Stop any existing daemon first runCli('tsx ts/cli.ts daemon stop'); await tools.delayFor(1000); // Start the daemon console.log('Starting daemon...'); const daemonResult = runCli('tsx ts/cli.ts daemon start'); console.log('Daemon start output:', daemonResult.stdout, daemonResult.stderr); // Wait for daemon to be ready await tools.delayFor(3000); // Add a process that will crash console.log('Adding crash test process...'); const addResult = runCli(`tsx ts/cli.ts add "node ${crashScriptPath}" --name crash-test`); console.log('Add output:', addResult.stdout, addResult.stderr); // Extract process ID from output const idMatch = addResult.stdout.match(/Assigned ID:\s*(\d+)/); if (!idMatch) { console.log('Could not extract process ID from output, skipping integration test'); runCli('tsx ts/cli.ts daemon stop'); await fs.unlink(crashScriptPath).catch(() => {}); return; } const processId = parseInt(idMatch[1]); console.log(`Process ID: ${processId}`); // Start the process console.log('Starting process that will crash...'); runCli(`tsx ts/cli.ts start ${processId}`); // Wait for the process to crash (it crashes after 3 seconds) console.log('Waiting for process to crash...'); await tools.delayFor(5000); // Check if crash log was created console.log('Checking for crash log...'); const crashLogFiles = await fs.readdir(crashLogsDir).catch(() => []); console.log(`Found ${crashLogFiles.length} crash log files:`, crashLogFiles); // Should have at least one crash log expect(crashLogFiles.length).toBeGreaterThan(0); // Find the crash log for our test process const testCrashLog = crashLogFiles.find(file => file.includes('crash-test')); expect(testCrashLog).toBeTruthy(); // Read and verify crash log content const crashLogPath = plugins.path.join(crashLogsDir, testCrashLog!); const crashLogContent = await fs.readFile(crashLogPath, 'utf-8'); console.log('Crash log content:'); console.log(crashLogContent); // Verify crash log contains expected information expect(crashLogContent).toInclude('CRASH REPORT'); expect(crashLogContent).toInclude('Exit Code'); expect(crashLogContent).toInclude('About to crash'); // Stop the process and daemon console.log('Cleaning up...'); runCli(`tsx ts/cli.ts delete ${processId}`); runCli('tsx ts/cli.ts daemon stop'); // Clean up test file await fs.unlink(crashScriptPath).catch(() => {}); }); tap.test('should create crash logs when process is killed', async (tools) => { const killScriptPath = plugins.path.join(paths.tspmDir, 'test-kill-script.js'); const crashLogsDir = plugins.path.join(paths.tspmDir, 'crashlogs'); // Write a script that runs indefinitely const KILL_SCRIPT = ` setInterval(() => { console.log('[test] Process is running and will be killed...'); }, 500); `; await fs.writeFile(killScriptPath, KILL_SCRIPT); // Stop any existing daemon runCli('tsx ts/cli.ts daemon stop'); await tools.delayFor(1000); // Start the daemon console.log('Starting daemon...'); runCli('tsx ts/cli.ts daemon start'); // Wait for daemon to be ready await tools.delayFor(3000); // Add a process that we'll kill console.log('Adding kill test process...'); const addResult = runCli(`tsx ts/cli.ts add "node ${killScriptPath}" --name kill-test`); console.log('Add output:', addResult.stdout, addResult.stderr); // Extract process ID const idMatch = addResult.stdout.match(/Assigned ID:\s*(\d+)/); if (!idMatch) { console.log('Could not extract process ID from output, skipping integration test'); runCli('tsx ts/cli.ts daemon stop'); await fs.unlink(killScriptPath).catch(() => {}); return; } const processId = parseInt(idMatch[1]); // Start the process console.log('Starting process to be killed...'); runCli(`tsx ts/cli.ts start ${processId}`); // Wait for process to run a bit await tools.delayFor(2000); // Get the actual PID of the running process const statusResult = runCli(`tsx ts/cli.ts describe ${processId}`); const pidMatch = statusResult.stdout.match(/pid:\s+(\d+)/); if (pidMatch) { const pid = parseInt(pidMatch[1]); console.log(`Killing process with PID ${pid}...`); // Kill the process with SIGTERM runCli(`kill -TERM ${pid}`); // Wait for crash log to be created await tools.delayFor(3000); // Check for crash log console.log('Checking for crash log from killed process...'); const crashLogFiles = await fs.readdir(crashLogsDir).catch(() => []); const killCrashLog = crashLogFiles.find(file => file.includes('kill-test')); if (killCrashLog) { const crashLogPath = plugins.path.join(crashLogsDir, killCrashLog); const crashLogContent = await fs.readFile(crashLogPath, 'utf-8'); console.log('Kill crash log content:'); console.log(crashLogContent); expect(crashLogContent).toInclude('SIGTERM'); } } // Clean up console.log('Cleaning up...'); runCli(`tsx ts/cli.ts delete ${processId}`); runCli('tsx ts/cli.ts daemon stop'); await fs.unlink(killScriptPath).catch(() => {}); }); export default tap.start();