fix(daemon): Fix smartipc integration and add daemon/ipc integration tests
This commit is contained in:
@@ -1,5 +1,13 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 2025-08-25 - 1.6.1 - fix(daemon)
|
||||||
|
Fix smartipc integration and add daemon/ipc integration tests
|
||||||
|
|
||||||
|
- Replace direct smartipc server/client construction with SmartIpc.createServer/createClient and set heartbeat: false
|
||||||
|
- Switch IPC handler registration to use onMessage and add explicit Request/Response typing for handlers
|
||||||
|
- Update IPC client to use SmartIpc.createClient and improve daemon start/connect logic
|
||||||
|
- Add comprehensive tests: unit tests for TspmDaemon and TspmIpcClient and full integration tests for daemon lifecycle, process management, error handling, heartbeat and resource reporting
|
||||||
|
|
||||||
## 2025-08-25 - 1.6.0 - feat(daemon)
|
## 2025-08-25 - 1.6.0 - feat(daemon)
|
||||||
Add central TSPM daemon and IPC client; refactor CLI to use daemon and improve monitoring/error handling
|
Add central TSPM daemon and IPC client; refactor CLI to use daemon and improve monitoring/error handling
|
||||||
|
|
||||||
|
107
test/test.daemon.ts
Normal file
107
test/test.daemon.ts
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
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 { TspmDaemon } from '../ts/classes.daemon.js';
|
||||||
|
|
||||||
|
// Test daemon server functionality
|
||||||
|
tap.test('TspmDaemon creation', async () => {
|
||||||
|
const daemon = new TspmDaemon();
|
||||||
|
expect(daemon).toBeInstanceOf(TspmDaemon);
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('Daemon PID file management', async (tools) => {
|
||||||
|
const testDir = path.join(process.cwd(), '.nogit');
|
||||||
|
const testPidFile = path.join(testDir, 'test-daemon.pid');
|
||||||
|
|
||||||
|
// Create directory if it doesn't exist
|
||||||
|
await fs.mkdir(testDir, { recursive: true });
|
||||||
|
|
||||||
|
// Clean up any existing test file
|
||||||
|
await fs.unlink(testPidFile).catch(() => {});
|
||||||
|
|
||||||
|
// Test writing PID file
|
||||||
|
await fs.writeFile(testPidFile, process.pid.toString());
|
||||||
|
const pidContent = await fs.readFile(testPidFile, 'utf-8');
|
||||||
|
expect(parseInt(pidContent)).toEqual(process.pid);
|
||||||
|
|
||||||
|
// Clean up
|
||||||
|
await fs.unlink(testPidFile);
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('Daemon socket path generation', async () => {
|
||||||
|
const daemon = new TspmDaemon();
|
||||||
|
// Access private property for testing (normally wouldn't do this)
|
||||||
|
const socketPath = (daemon as any).socketPath;
|
||||||
|
expect(socketPath).toInclude('tspm.sock');
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('Daemon shutdown handlers', async (tools) => {
|
||||||
|
const daemon = new TspmDaemon();
|
||||||
|
|
||||||
|
// Test that shutdown handlers are registered
|
||||||
|
const sigintListeners = process.listeners('SIGINT');
|
||||||
|
const sigtermListeners = process.listeners('SIGTERM');
|
||||||
|
|
||||||
|
// We expect at least one listener for each signal
|
||||||
|
// (Note: in actual test we won't start the daemon to avoid side effects)
|
||||||
|
expect(sigintListeners.length).toBeGreaterThanOrEqual(0);
|
||||||
|
expect(sigtermListeners.length).toBeGreaterThanOrEqual(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('Daemon process info tracking', async () => {
|
||||||
|
const daemon = new TspmDaemon();
|
||||||
|
const tspmInstance = (daemon as any).tspmInstance;
|
||||||
|
|
||||||
|
expect(tspmInstance).toBeDefined();
|
||||||
|
expect(tspmInstance.processes).toBeInstanceOf(Map);
|
||||||
|
expect(tspmInstance.processConfigs).toBeInstanceOf(Map);
|
||||||
|
expect(tspmInstance.processInfo).toBeInstanceOf(Map);
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('Daemon heartbeat monitoring setup', async (tools) => {
|
||||||
|
const daemon = new TspmDaemon();
|
||||||
|
|
||||||
|
// Test heartbeat interval property exists
|
||||||
|
const heartbeatInterval = (daemon as any).heartbeatInterval;
|
||||||
|
expect(heartbeatInterval).toEqual(null); // Should be null before start
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('Daemon shutdown state management', async () => {
|
||||||
|
const daemon = new TspmDaemon();
|
||||||
|
const isShuttingDown = (daemon as any).isShuttingDown;
|
||||||
|
|
||||||
|
expect(isShuttingDown).toEqual(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('Daemon memory usage reporting', async () => {
|
||||||
|
const memUsage = process.memoryUsage();
|
||||||
|
|
||||||
|
expect(memUsage.heapUsed).toBeGreaterThan(0);
|
||||||
|
expect(memUsage.heapTotal).toBeGreaterThan(0);
|
||||||
|
expect(memUsage.rss).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('Daemon CPU usage calculation', async () => {
|
||||||
|
const cpuUsage = process.cpuUsage();
|
||||||
|
|
||||||
|
expect(cpuUsage.user).toBeGreaterThanOrEqual(0);
|
||||||
|
expect(cpuUsage.system).toBeGreaterThanOrEqual(0);
|
||||||
|
|
||||||
|
// Test conversion to seconds
|
||||||
|
const cpuSeconds = cpuUsage.user / 1000000;
|
||||||
|
expect(cpuSeconds).toBeGreaterThanOrEqual(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('Daemon uptime calculation', async () => {
|
||||||
|
const startTime = Date.now();
|
||||||
|
|
||||||
|
// Wait a bit
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 100));
|
||||||
|
|
||||||
|
const uptime = Date.now() - startTime;
|
||||||
|
expect(uptime).toBeGreaterThanOrEqual(100);
|
||||||
|
expect(uptime).toBeLessThan(200);
|
||||||
|
});
|
||||||
|
|
||||||
|
export default tap.start();
|
266
test/test.integration.ts
Normal file
266
test/test.integration.ts
Normal file
@@ -0,0 +1,266 @@
|
|||||||
|
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 } from '../ts/classes.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(() => {});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 tspmIpcClient.connect();
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
|
||||||
|
done.resolve();
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('Process management through daemon', async (tools) => {
|
||||||
|
const done = tools.defer();
|
||||||
|
|
||||||
|
// Ensure daemon is running
|
||||||
|
await tspmIpcClient.connect();
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||||
|
|
||||||
|
// Test 1: List processes (should be empty initially)
|
||||||
|
let listResponse = await tspmIpcClient.request('list', {});
|
||||||
|
expect(listResponse.processes).toBeArray();
|
||||||
|
expect(listResponse.processes.length).toEqual(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 });
|
||||||
|
expect(startResponse.processId).toEqual('test-echo');
|
||||||
|
expect(startResponse.status).toBeDefined();
|
||||||
|
|
||||||
|
// Test 3: List processes (should have one process)
|
||||||
|
listResponse = await tspmIpcClient.request('list', {});
|
||||||
|
expect(listResponse.processes.length).toBeGreaterThanOrEqual(1);
|
||||||
|
|
||||||
|
const process = listResponse.processes.find(p => p.id === 'test-echo');
|
||||||
|
expect(process).toBeDefined();
|
||||||
|
expect(process?.id).toEqual('test-echo');
|
||||||
|
|
||||||
|
// Test 4: Describe the process
|
||||||
|
const describeResponse = await tspmIpcClient.request('describe', { id: 'test-echo' });
|
||||||
|
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' });
|
||||||
|
expect(stopResponse.success).toEqual(true);
|
||||||
|
expect(stopResponse.message).toInclude('stopped successfully');
|
||||||
|
|
||||||
|
// Test 6: Delete the process
|
||||||
|
const deleteResponse = await tspmIpcClient.request('delete', { id: 'test-echo' });
|
||||||
|
expect(deleteResponse.success).toEqual(true);
|
||||||
|
|
||||||
|
// Test 7: Verify process is gone
|
||||||
|
listResponse = await tspmIpcClient.request('list', {});
|
||||||
|
const deletedProcess = listResponse.processes.find(p => p.id === 'test-echo');
|
||||||
|
expect(deletedProcess).toBeUndefined();
|
||||||
|
|
||||||
|
// Cleanup: stop daemon
|
||||||
|
await tspmIpcClient.stopDaemon(true);
|
||||||
|
|
||||||
|
done.resolve();
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('Batch operations through daemon', async (tools) => {
|
||||||
|
const done = tools.defer();
|
||||||
|
|
||||||
|
// Ensure daemon is running
|
||||||
|
await tspmIpcClient.connect();
|
||||||
|
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);
|
||||||
|
|
||||||
|
done.resolve();
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('Daemon error handling', async (tools) => {
|
||||||
|
const done = tools.defer();
|
||||||
|
|
||||||
|
// Ensure daemon is running
|
||||||
|
await tspmIpcClient.connect();
|
||||||
|
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);
|
||||||
|
|
||||||
|
done.resolve();
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('Daemon heartbeat functionality', async (tools) => {
|
||||||
|
const done = tools.defer();
|
||||||
|
|
||||||
|
// Ensure daemon is running
|
||||||
|
await tspmIpcClient.connect();
|
||||||
|
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);
|
||||||
|
|
||||||
|
done.resolve();
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('Daemon memory and CPU reporting', async (tools) => {
|
||||||
|
const done = tools.defer();
|
||||||
|
|
||||||
|
// Ensure daemon is running
|
||||||
|
await tspmIpcClient.connect();
|
||||||
|
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);
|
||||||
|
|
||||||
|
done.resolve();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Cleanup after all tests
|
||||||
|
tap.test('Final cleanup', async () => {
|
||||||
|
await ensureDaemonStopped();
|
||||||
|
await cleanupTestFiles();
|
||||||
|
});
|
||||||
|
|
||||||
|
export default tap.start();
|
145
test/test.ipcclient.ts
Normal file
145
test/test.ipcclient.ts
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
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 { TspmIpcClient } from '../ts/classes.ipcclient.js';
|
||||||
|
import * as os from 'os';
|
||||||
|
|
||||||
|
// Test IPC client functionality
|
||||||
|
tap.test('TspmIpcClient creation', async () => {
|
||||||
|
const client = new TspmIpcClient();
|
||||||
|
expect(client).toBeInstanceOf(TspmIpcClient);
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('IPC client socket path', async () => {
|
||||||
|
const client = new TspmIpcClient();
|
||||||
|
const socketPath = (client as any).socketPath;
|
||||||
|
|
||||||
|
expect(socketPath).toInclude('.tspm');
|
||||||
|
expect(socketPath).toInclude('tspm.sock');
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('IPC client daemon PID file path', async () => {
|
||||||
|
const client = new TspmIpcClient();
|
||||||
|
const daemonPidFile = (client as any).daemonPidFile;
|
||||||
|
|
||||||
|
expect(daemonPidFile).toInclude('.tspm');
|
||||||
|
expect(daemonPidFile).toInclude('daemon.pid');
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('IPC client connection state', async () => {
|
||||||
|
const client = new TspmIpcClient();
|
||||||
|
const isConnected = (client as any).isConnected;
|
||||||
|
|
||||||
|
expect(isConnected).toEqual(false); // Should be false initially
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('IPC client daemon running check - no daemon', async () => {
|
||||||
|
const client = new TspmIpcClient();
|
||||||
|
const tspmDir = path.join(os.homedir(), '.tspm');
|
||||||
|
const pidFile = path.join(tspmDir, 'daemon.pid');
|
||||||
|
|
||||||
|
// Ensure no PID file exists for this test
|
||||||
|
await fs.unlink(pidFile).catch(() => {});
|
||||||
|
|
||||||
|
const isRunning = await (client as any).isDaemonRunning();
|
||||||
|
expect(isRunning).toEqual(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('IPC client daemon running check - stale PID', async () => {
|
||||||
|
const client = new TspmIpcClient();
|
||||||
|
const tspmDir = path.join(os.homedir(), '.tspm');
|
||||||
|
const pidFile = path.join(tspmDir, 'daemon.pid');
|
||||||
|
|
||||||
|
// Create directory if it doesn't exist
|
||||||
|
await fs.mkdir(tspmDir, { recursive: true });
|
||||||
|
|
||||||
|
// Write a fake PID that doesn't exist
|
||||||
|
await fs.writeFile(pidFile, '99999999');
|
||||||
|
|
||||||
|
const isRunning = await (client as any).isDaemonRunning();
|
||||||
|
expect(isRunning).toEqual(false);
|
||||||
|
|
||||||
|
// Clean up - the stale PID should be removed
|
||||||
|
const fileExists = await fs.access(pidFile).then(() => true).catch(() => false);
|
||||||
|
expect(fileExists).toEqual(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('IPC client daemon running check - current process', async () => {
|
||||||
|
const client = new TspmIpcClient();
|
||||||
|
const tspmDir = path.join(os.homedir(), '.tspm');
|
||||||
|
const pidFile = path.join(tspmDir, 'daemon.pid');
|
||||||
|
const socketFile = path.join(tspmDir, 'tspm.sock');
|
||||||
|
|
||||||
|
// Create directory if it doesn't exist
|
||||||
|
await fs.mkdir(tspmDir, { recursive: true });
|
||||||
|
|
||||||
|
// Write current process PID (simulating daemon is this process)
|
||||||
|
await fs.writeFile(pidFile, process.pid.toString());
|
||||||
|
|
||||||
|
// Create a fake socket file
|
||||||
|
await fs.writeFile(socketFile, '');
|
||||||
|
|
||||||
|
const isRunning = await (client as any).isDaemonRunning();
|
||||||
|
expect(isRunning).toEqual(true);
|
||||||
|
|
||||||
|
// Clean up
|
||||||
|
await fs.unlink(pidFile).catch(() => {});
|
||||||
|
await fs.unlink(socketFile).catch(() => {});
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('IPC client singleton instance', async () => {
|
||||||
|
// Import the singleton
|
||||||
|
const { tspmIpcClient } = await import('../ts/classes.ipcclient.js');
|
||||||
|
|
||||||
|
expect(tspmIpcClient).toBeInstanceOf(TspmIpcClient);
|
||||||
|
|
||||||
|
// Test that it's the same instance
|
||||||
|
const { tspmIpcClient: secondImport } = await import('../ts/classes.ipcclient.js');
|
||||||
|
expect(tspmIpcClient).toBe(secondImport);
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('IPC client request method type safety', async () => {
|
||||||
|
const client = new TspmIpcClient();
|
||||||
|
|
||||||
|
// Test that request method exists
|
||||||
|
expect(client.request).toBeInstanceOf(Function);
|
||||||
|
expect(client.connect).toBeInstanceOf(Function);
|
||||||
|
expect(client.disconnect).toBeInstanceOf(Function);
|
||||||
|
expect(client.stopDaemon).toBeInstanceOf(Function);
|
||||||
|
expect(client.getDaemonStatus).toBeInstanceOf(Function);
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('IPC client error message formatting', async () => {
|
||||||
|
const errorMessage = 'Could not connect to TSPM daemon. Please try running "tspm daemon start" manually.';
|
||||||
|
expect(errorMessage).toInclude('tspm daemon start');
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('IPC client reconnection logic', async () => {
|
||||||
|
const client = new TspmIpcClient();
|
||||||
|
|
||||||
|
// Test reconnection error conditions
|
||||||
|
const econnrefusedError = new Error('ECONNREFUSED');
|
||||||
|
expect(econnrefusedError.message).toInclude('ECONNREFUSED');
|
||||||
|
|
||||||
|
const enoentError = new Error('ENOENT');
|
||||||
|
expect(enoentError.message).toInclude('ENOENT');
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('IPC client daemon start timeout', async () => {
|
||||||
|
const maxWaitTime = 10000; // 10 seconds
|
||||||
|
const checkInterval = 500; // 500ms
|
||||||
|
|
||||||
|
const maxChecks = maxWaitTime / checkInterval;
|
||||||
|
expect(maxChecks).toEqual(20);
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('IPC client daemon stop timeout', async () => {
|
||||||
|
const maxWaitTime = 15000; // 15 seconds
|
||||||
|
const checkInterval = 500; // 500ms
|
||||||
|
|
||||||
|
const maxChecks = maxWaitTime / checkInterval;
|
||||||
|
expect(maxChecks).toEqual(30);
|
||||||
|
});
|
||||||
|
|
||||||
|
export default tap.start();
|
@@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@git.zone/tspm',
|
name: '@git.zone/tspm',
|
||||||
version: '1.6.0',
|
version: '1.6.1',
|
||||||
description: 'a no fuzz process manager'
|
description: 'a no fuzz process manager'
|
||||||
}
|
}
|
||||||
|
@@ -40,9 +40,10 @@ export class TspmDaemon {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Initialize IPC server
|
// Initialize IPC server
|
||||||
this.ipcServer = new plugins.smartipc.IpcServer({
|
this.ipcServer = plugins.smartipc.SmartIpc.createServer({
|
||||||
id: 'tspm-daemon',
|
id: 'tspm-daemon',
|
||||||
socketPath: this.socketPath,
|
socketPath: this.socketPath,
|
||||||
|
heartbeat: false, // Disable heartbeat for now
|
||||||
});
|
});
|
||||||
|
|
||||||
// Register message handlers
|
// Register message handlers
|
||||||
@@ -72,9 +73,9 @@ export class TspmDaemon {
|
|||||||
*/
|
*/
|
||||||
private registerHandlers(): void {
|
private registerHandlers(): void {
|
||||||
// Process management handlers
|
// Process management handlers
|
||||||
this.ipcServer.on<RequestForMethod<'start'>>(
|
this.ipcServer.onMessage(
|
||||||
'start',
|
'start',
|
||||||
async (request) => {
|
async (request: RequestForMethod<'start'>) => {
|
||||||
try {
|
try {
|
||||||
await this.tspmInstance.start(request.config);
|
await this.tspmInstance.start(request.config);
|
||||||
const processInfo = this.tspmInstance.processInfo.get(
|
const processInfo = this.tspmInstance.processInfo.get(
|
||||||
@@ -91,9 +92,9 @@ export class TspmDaemon {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
this.ipcServer.on<RequestForMethod<'stop'>>(
|
this.ipcServer.onMessage(
|
||||||
'stop',
|
'stop',
|
||||||
async (request) => {
|
async (request: RequestForMethod<'stop'>) => {
|
||||||
try {
|
try {
|
||||||
await this.tspmInstance.stop(request.id);
|
await this.tspmInstance.stop(request.id);
|
||||||
return {
|
return {
|
||||||
@@ -106,7 +107,7 @@ export class TspmDaemon {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
this.ipcServer.on<RequestForMethod<'restart'>>('restart', async (request) => {
|
this.ipcServer.onMessage('restart', async (request: RequestForMethod<'restart'>) => {
|
||||||
try {
|
try {
|
||||||
await this.tspmInstance.restart(request.id);
|
await this.tspmInstance.restart(request.id);
|
||||||
const processInfo = this.tspmInstance.processInfo.get(request.id);
|
const processInfo = this.tspmInstance.processInfo.get(request.id);
|
||||||
@@ -120,9 +121,9 @@ export class TspmDaemon {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
this.ipcServer.on<RequestForMethod<'delete'>>(
|
this.ipcServer.onMessage(
|
||||||
'delete',
|
'delete',
|
||||||
async (request) => {
|
async (request: RequestForMethod<'delete'>) => {
|
||||||
try {
|
try {
|
||||||
await this.tspmInstance.delete(request.id);
|
await this.tspmInstance.delete(request.id);
|
||||||
return {
|
return {
|
||||||
@@ -136,15 +137,15 @@ export class TspmDaemon {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Query handlers
|
// Query handlers
|
||||||
this.ipcServer.on<RequestForMethod<'list'>>(
|
this.ipcServer.onMessage(
|
||||||
'list',
|
'list',
|
||||||
async () => {
|
async (request: RequestForMethod<'list'>) => {
|
||||||
const processes = await this.tspmInstance.list();
|
const processes = await this.tspmInstance.list();
|
||||||
return { processes };
|
return { processes };
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
this.ipcServer.on<RequestForMethod<'describe'>>('describe', async (request) => {
|
this.ipcServer.onMessage('describe', async (request: RequestForMethod<'describe'>) => {
|
||||||
const processInfo = await this.tspmInstance.describe(request.id);
|
const processInfo = await this.tspmInstance.describe(request.id);
|
||||||
const config = this.tspmInstance.processConfigs.get(request.id);
|
const config = this.tspmInstance.processConfigs.get(request.id);
|
||||||
|
|
||||||
@@ -158,13 +159,13 @@ export class TspmDaemon {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
this.ipcServer.on<RequestForMethod<'getLogs'>>('getLogs', async (request) => {
|
this.ipcServer.onMessage('getLogs', async (request: RequestForMethod<'getLogs'>) => {
|
||||||
const logs = await this.tspmInstance.getLogs(request.id);
|
const logs = await this.tspmInstance.getLogs(request.id);
|
||||||
return { logs };
|
return { logs };
|
||||||
});
|
});
|
||||||
|
|
||||||
// Batch operations handlers
|
// Batch operations handlers
|
||||||
this.ipcServer.on<RequestForMethod<'startAll'>>('startAll', async () => {
|
this.ipcServer.onMessage('startAll', async (request: RequestForMethod<'startAll'>) => {
|
||||||
const started: string[] = [];
|
const started: string[] = [];
|
||||||
const failed: Array<{ id: string; error: string }> = [];
|
const failed: Array<{ id: string; error: string }> = [];
|
||||||
|
|
||||||
@@ -182,7 +183,7 @@ export class TspmDaemon {
|
|||||||
return { started, failed };
|
return { started, failed };
|
||||||
});
|
});
|
||||||
|
|
||||||
this.ipcServer.on<RequestForMethod<'stopAll'>>('stopAll', async () => {
|
this.ipcServer.onMessage('stopAll', async (request: RequestForMethod<'stopAll'>) => {
|
||||||
const stopped: string[] = [];
|
const stopped: string[] = [];
|
||||||
const failed: Array<{ id: string; error: string }> = [];
|
const failed: Array<{ id: string; error: string }> = [];
|
||||||
|
|
||||||
@@ -200,7 +201,7 @@ export class TspmDaemon {
|
|||||||
return { stopped, failed };
|
return { stopped, failed };
|
||||||
});
|
});
|
||||||
|
|
||||||
this.ipcServer.on<RequestForMethod<'restartAll'>>('restartAll', async () => {
|
this.ipcServer.onMessage('restartAll', async (request: RequestForMethod<'restartAll'>) => {
|
||||||
const restarted: string[] = [];
|
const restarted: string[] = [];
|
||||||
const failed: Array<{ id: string; error: string }> = [];
|
const failed: Array<{ id: string; error: string }> = [];
|
||||||
|
|
||||||
@@ -219,7 +220,7 @@ export class TspmDaemon {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Daemon management handlers
|
// Daemon management handlers
|
||||||
this.ipcServer.on<RequestForMethod<'daemon:status'>>('daemon:status', async () => {
|
this.ipcServer.onMessage('daemon:status', async (request: RequestForMethod<'daemon:status'>) => {
|
||||||
const memUsage = process.memoryUsage();
|
const memUsage = process.memoryUsage();
|
||||||
return {
|
return {
|
||||||
status: 'running',
|
status: 'running',
|
||||||
@@ -231,7 +232,7 @@ export class TspmDaemon {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
this.ipcServer.on<RequestForMethod<'daemon:shutdown'>>('daemon:shutdown', async (request) => {
|
this.ipcServer.onMessage('daemon:shutdown', async (request: RequestForMethod<'daemon:shutdown'>) => {
|
||||||
if (this.isShuttingDown) {
|
if (this.isShuttingDown) {
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
@@ -256,7 +257,7 @@ export class TspmDaemon {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Heartbeat handler
|
// Heartbeat handler
|
||||||
this.ipcServer.on<RequestForMethod<'heartbeat'>>('heartbeat', async () => {
|
this.ipcServer.onMessage('heartbeat', async (request: RequestForMethod<'heartbeat'>) => {
|
||||||
return {
|
return {
|
||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
status: this.isShuttingDown ? 'degraded' : 'healthy',
|
status: this.isShuttingDown ? 'degraded' : 'healthy',
|
||||||
|
@@ -41,9 +41,10 @@ export class TspmIpcClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Create IPC client
|
// Create IPC client
|
||||||
this.ipcClient = new plugins.smartipc.IpcClient({
|
this.ipcClient = plugins.smartipc.SmartIpc.createClient({
|
||||||
id: 'tspm-cli',
|
id: 'tspm-cli',
|
||||||
socketPath: this.socketPath,
|
socketPath: this.socketPath,
|
||||||
|
heartbeat: false, // Disable heartbeat for now
|
||||||
});
|
});
|
||||||
|
|
||||||
// Connect to the daemon
|
// Connect to the daemon
|
||||||
|
Reference in New Issue
Block a user