409 lines
12 KiB
TypeScript
409 lines
12 KiB
TypeScript
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';
|
|
|
|
// 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: 'test-echo',
|
|
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('test-echo');
|
|
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 === 'test-echo');
|
|
expect(procInfo).toBeDefined();
|
|
expect(procInfo?.id).toEqual('test-echo');
|
|
|
|
// Test 4: Describe the process
|
|
const describeResponse = await tspmIpcClient.request('describe', {
|
|
id: 'test-echo',
|
|
});
|
|
console.log('Describe:', describeResponse);
|
|
expect(describeResponse.processInfo).toBeDefined();
|
|
expect(describeResponse.config).toBeDefined();
|
|
expect(describeResponse.config.id).toEqual('test-echo');
|
|
|
|
// Test 5: Stop the process
|
|
const stopResponse = await tspmIpcClient.request('stop', { id: 'test-echo' });
|
|
console.log('Stop response:', stopResponse);
|
|
expect(stopResponse.success).toEqual(true);
|
|
|
|
// Test 6: Delete the process
|
|
const deleteResponse = await tspmIpcClient.request('delete', {
|
|
id: 'test-echo',
|
|
});
|
|
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 === 'test-echo',
|
|
);
|
|
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: 'batch-test-1',
|
|
name: 'Batch Test 1',
|
|
command: 'echo "Process 1"',
|
|
projectDir: process.cwd(),
|
|
memoryLimitBytes: 50 * 1024 * 1024,
|
|
autorestart: false,
|
|
},
|
|
{
|
|
id: 'batch-test-2',
|
|
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: 'non-existent-process' });
|
|
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: 'non-existent-process' });
|
|
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: 'non-existent-process' });
|
|
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();
|