import { expect, tap } from '@git.zone/tstest/tapbundle'; import * as tspm from '../ts/index.js'; import * as path from 'path'; import * as fs from 'fs/promises'; import * as os from 'os'; import { spawn } from 'child_process'; import { tspmIpcClient, TspmIpcClient } from '../ts/client/tspm.ipcclient.js'; import { toProcessId } from '../ts/shared/protocol/id.js'; // Helper to ensure daemon is stopped before tests async function ensureDaemonStopped() { try { await tspmIpcClient.stopDaemon(false); await new Promise((resolve) => setTimeout(resolve, 1000)); } catch (error) { // Ignore errors if daemon is not running } } // Helper to clean up test files async function cleanupTestFiles() { const tspmDir = path.join(os.homedir(), '.tspm'); const pidFile = path.join(tspmDir, 'daemon.pid'); const socketFile = path.join(tspmDir, 'tspm.sock'); await fs.unlink(pidFile).catch(() => {}); await fs.unlink(socketFile).catch(() => {}); } // Helper to start the daemon for tests async function startDaemonForTest() { const daemonEntry = path.join(process.cwd(), 'dist_ts', 'daemon', 'index.js'); // Spawn daemon as detached background process to avoid interfering with TAP output const child = spawn(process.execPath, [daemonEntry], { detached: true, stdio: 'ignore', env: { ...process.env, TSPM_DAEMON_MODE: 'true', SMARTIPC_CLIENT_ONLY: '0', }, }); child.unref(); // Wait for PID file and alive process (avoid early IPC connects) const tspmDir = path.join(os.homedir(), '.tspm'); const pidFile = path.join(tspmDir, 'daemon.pid'); const socketFile = path.join(tspmDir, 'tspm.sock'); const timeoutMs = 10000; const stepMs = 200; const start = Date.now(); while (Date.now() - start < timeoutMs) { try { const pidContent = await fs.readFile(pidFile, 'utf-8').catch(() => null); if (pidContent) { const pid = parseInt(pidContent.trim(), 10); try { process.kill(pid, 0); // PID alive, also ensure socket path exists await fs.access(socketFile).catch(() => {}); // small grace period to ensure server readiness await new Promise((r) => setTimeout(r, 500)); return; } catch { // process not yet alive } } } catch { // ignore } await new Promise((r) => setTimeout(r, stepMs)); } throw new Error('Daemon did not become ready in time'); } // Helper to connect with simple retry logic to avoid race conditions async function connectWithRetry(retries: number = 5, delayMs: number = 1000) { for (let attempt = 0; attempt < retries; attempt++) { try { await tspmIpcClient.connect(); return; } catch (e) { if (attempt === retries - 1) throw e; await new Promise((r) => setTimeout(r, delayMs)); } } } // Integration tests for daemon-client communication tap.test('Full daemon lifecycle test', async (tools) => { const done = tools.defer(); // Ensure clean state await ensureDaemonStopped(); await cleanupTestFiles(); // Test 1: Check daemon is not running let status = await tspmIpcClient.getDaemonStatus(); expect(status).toEqual(null); // Test 2: Start daemon console.log('Starting daemon...'); await startDaemonForTest(); await connectWithRetry(); // Give daemon time to fully initialize await new Promise((resolve) => setTimeout(resolve, 2000)); // Test 3: Check daemon is running status = await tspmIpcClient.getDaemonStatus(); expect(status).toBeDefined(); expect(status?.status).toEqual('running'); expect(status?.pid).toBeGreaterThan(0); expect(status?.processCount).toBeGreaterThanOrEqual(0); // Test 4: Stop daemon console.log('Stopping daemon...'); await tspmIpcClient.stopDaemon(true); // Give daemon time to shutdown await new Promise((resolve) => setTimeout(resolve, 2000)); // Test 5: Check daemon is stopped status = await tspmIpcClient.getDaemonStatus(); expect(status).toEqual(null); // Ensure client disconnects cleanly await tspmIpcClient.disconnect(); done.resolve(); }); tap.test('Process management through daemon', async (tools) => { const done = tools.defer(); // Ensure daemon is running if (!(await tspmIpcClient.getDaemonStatus())) { await startDaemonForTest(); } const beforeStatus = await tspmIpcClient.getDaemonStatus(); console.log('Status before connect:', beforeStatus); for (let i = 0; i < 5; i++) { try { await tspmIpcClient.connect(); break; } catch (e) { if (i === 4) throw e; await new Promise((r) => setTimeout(r, 1000)); } } await new Promise((resolve) => setTimeout(resolve, 1000)); console.log('Connected for process management test'); // Test 1: List processes (should be empty initially) let listResponse = await tspmIpcClient.request('list', {}); console.log('Initial list:', listResponse); expect(listResponse.processes).toBeArray(); expect(listResponse.processes.length).toBeGreaterThanOrEqual(0); // Test 2: Start a test process const testConfig: tspm.IProcessConfig = { id: toProcessId(1001), name: 'Test Echo Process', command: 'echo "Test process"', projectDir: process.cwd(), memoryLimitBytes: 50 * 1024 * 1024, autorestart: false, }; const startResponse = await tspmIpcClient.request('start', { config: testConfig, }); console.log('Start response:', startResponse); expect(startResponse.processId).toEqual(1001); expect(startResponse.status).toBeDefined(); // Test 3: List processes (should have one process) listResponse = await tspmIpcClient.request('list', {}); console.log('List after start:', listResponse); expect(listResponse.processes.length).toBeGreaterThanOrEqual(1); const procInfo = listResponse.processes.find((p) => p.id === toProcessId(1001)); expect(procInfo).toBeDefined(); expect(procInfo?.id).toEqual(1001); // Test 4: Describe the process const describeResponse = await tspmIpcClient.request('describe', { id: toProcessId(1001), }); console.log('Describe:', describeResponse); expect(describeResponse.processInfo).toBeDefined(); expect(describeResponse.config).toBeDefined(); expect(describeResponse.config.id).toEqual(1001); // Test 5: Stop the process const stopResponse = await tspmIpcClient.request('stop', { id: toProcessId(1001) }); console.log('Stop response:', stopResponse); expect(stopResponse.success).toEqual(true); // Test 6: Delete the process const deleteResponse = await tspmIpcClient.request('delete', { id: toProcessId(1001), }); console.log('Delete response:', deleteResponse); expect(deleteResponse.success).toEqual(true); // Test 7: Verify process is gone listResponse = await tspmIpcClient.request('list', {}); console.log('List after delete:', listResponse); const deletedProcess = listResponse.processes.find((p) => p.id === toProcessId(1001)); expect(deletedProcess).toBeUndefined(); // Cleanup: stop daemon await tspmIpcClient.stopDaemon(true); await tspmIpcClient.disconnect(); done.resolve(); }); tap.test('Batch operations through daemon', async (tools) => { const done = tools.defer(); // Ensure daemon is running if (!(await tspmIpcClient.getDaemonStatus())) { await startDaemonForTest(); } for (let i = 0; i < 5; i++) { try { await tspmIpcClient.connect(); break; } catch (e) { if (i === 4) throw e; await new Promise((r) => setTimeout(r, 1000)); } } await new Promise((resolve) => setTimeout(resolve, 1000)); // Add multiple test processes const testConfigs: tspm.IProcessConfig[] = [ { id: toProcessId(1101), name: 'Batch Test 1', command: 'echo "Process 1"', projectDir: process.cwd(), memoryLimitBytes: 50 * 1024 * 1024, autorestart: false, }, { id: toProcessId(1102), name: 'Batch Test 2', command: 'echo "Process 2"', projectDir: process.cwd(), memoryLimitBytes: 50 * 1024 * 1024, autorestart: false, }, ]; // Start processes for (const config of testConfigs) { await tspmIpcClient.request('start', { config }); } // Test 1: Stop all processes const stopAllResponse = await tspmIpcClient.request('stopAll', {}); expect(stopAllResponse.stopped).toBeArray(); expect(stopAllResponse.stopped.length).toBeGreaterThanOrEqual(2); // Test 2: Start all processes const startAllResponse = await tspmIpcClient.request('startAll', {}); expect(startAllResponse.started).toBeArray(); // Test 3: Restart all processes const restartAllResponse = await tspmIpcClient.request('restartAll', {}); expect(restartAllResponse.restarted).toBeArray(); // Cleanup: delete all test processes for (const config of testConfigs) { await tspmIpcClient.request('delete', { id: config.id }).catch(() => {}); } // Stop daemon await tspmIpcClient.stopDaemon(true); await tspmIpcClient.disconnect(); done.resolve(); }); tap.test('Daemon error handling', async (tools) => { const done = tools.defer(); // Ensure daemon is running if (!(await tspmIpcClient.getDaemonStatus())) { await startDaemonForTest(); } for (let i = 0; i < 5; i++) { try { await tspmIpcClient.connect(); break; } catch (e) { if (i === 4) throw e; await new Promise((r) => setTimeout(r, 1000)); } } await new Promise((resolve) => setTimeout(resolve, 1000)); // Test 1: Try to stop non-existent process try { await tspmIpcClient.request('stop', { id: toProcessId(99999) }); expect(false).toEqual(true); // Should not reach here } catch (error) { expect(error.message).toInclude('Failed to stop process'); } // Test 2: Try to describe non-existent process try { await tspmIpcClient.request('describe', { id: toProcessId(99999) }); expect(false).toEqual(true); // Should not reach here } catch (error) { expect(error.message).toInclude('not found'); } // Test 3: Try to restart non-existent process try { await tspmIpcClient.request('restart', { id: toProcessId(99999) }); expect(false).toEqual(true); // Should not reach here } catch (error) { expect(error.message).toInclude('Failed to restart process'); } // Stop daemon await tspmIpcClient.stopDaemon(true); await tspmIpcClient.disconnect(); done.resolve(); }); tap.test('Daemon heartbeat functionality', async (tools) => { const done = tools.defer(); // Ensure daemon is running if (!(await tspmIpcClient.getDaemonStatus())) { await startDaemonForTest(); } for (let i = 0; i < 5; i++) { try { await tspmIpcClient.connect(); break; } catch (e) { if (i === 4) throw e; await new Promise((r) => setTimeout(r, 1000)); } } await new Promise((resolve) => setTimeout(resolve, 1000)); // Test heartbeat const heartbeatResponse = await tspmIpcClient.request('heartbeat', {}); expect(heartbeatResponse.timestamp).toBeGreaterThan(0); expect(heartbeatResponse.status).toEqual('healthy'); // Stop daemon await tspmIpcClient.stopDaemon(true); await tspmIpcClient.disconnect(); done.resolve(); }); tap.test('Daemon memory and CPU reporting', async (tools) => { const done = tools.defer(); // Ensure daemon is running if (!(await tspmIpcClient.getDaemonStatus())) { await startDaemonForTest(); } for (let i = 0; i < 5; i++) { try { await tspmIpcClient.connect(); break; } catch (e) { if (i === 4) throw e; await new Promise((r) => setTimeout(r, 1000)); } } await new Promise((resolve) => setTimeout(resolve, 1000)); // Get daemon status const status = await tspmIpcClient.getDaemonStatus(); expect(status).toBeDefined(); expect(status?.memoryUsage).toBeGreaterThan(0); expect(status?.cpuUsage).toBeGreaterThanOrEqual(0); expect(status?.uptime).toBeGreaterThan(0); // Stop daemon await tspmIpcClient.stopDaemon(true); await tspmIpcClient.disconnect(); done.resolve(); }); // Cleanup after all tests tap.test('Final cleanup', async () => { await ensureDaemonStopped(); await cleanupTestFiles(); }); export default tap.start();