fix(daemon): Fix smartipc integration and add daemon/ipc integration tests
This commit is contained in:
		@@ -1,5 +1,13 @@
 | 
			
		||||
# 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)
 | 
			
		||||
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 = {
 | 
			
		||||
  name: '@git.zone/tspm',
 | 
			
		||||
  version: '1.6.0',
 | 
			
		||||
  version: '1.6.1',
 | 
			
		||||
  description: 'a no fuzz process manager'
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -40,9 +40,10 @@ export class TspmDaemon {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Initialize IPC server
 | 
			
		||||
    this.ipcServer = new plugins.smartipc.IpcServer({
 | 
			
		||||
    this.ipcServer = plugins.smartipc.SmartIpc.createServer({
 | 
			
		||||
      id: 'tspm-daemon',
 | 
			
		||||
      socketPath: this.socketPath,
 | 
			
		||||
      heartbeat: false,  // Disable heartbeat for now
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // Register message handlers
 | 
			
		||||
@@ -72,9 +73,9 @@ export class TspmDaemon {
 | 
			
		||||
   */
 | 
			
		||||
  private registerHandlers(): void {
 | 
			
		||||
    // Process management handlers
 | 
			
		||||
    this.ipcServer.on<RequestForMethod<'start'>>(
 | 
			
		||||
    this.ipcServer.onMessage(
 | 
			
		||||
      'start',
 | 
			
		||||
      async (request) => {
 | 
			
		||||
      async (request: RequestForMethod<'start'>) => {
 | 
			
		||||
        try {
 | 
			
		||||
          await this.tspmInstance.start(request.config);
 | 
			
		||||
          const processInfo = this.tspmInstance.processInfo.get(
 | 
			
		||||
@@ -91,9 +92,9 @@ export class TspmDaemon {
 | 
			
		||||
      },
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    this.ipcServer.on<RequestForMethod<'stop'>>(
 | 
			
		||||
    this.ipcServer.onMessage(
 | 
			
		||||
      'stop',
 | 
			
		||||
      async (request) => {
 | 
			
		||||
      async (request: RequestForMethod<'stop'>) => {
 | 
			
		||||
        try {
 | 
			
		||||
          await this.tspmInstance.stop(request.id);
 | 
			
		||||
          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 {
 | 
			
		||||
        await this.tspmInstance.restart(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',
 | 
			
		||||
      async (request) => {
 | 
			
		||||
      async (request: RequestForMethod<'delete'>) => {
 | 
			
		||||
        try {
 | 
			
		||||
          await this.tspmInstance.delete(request.id);
 | 
			
		||||
          return {
 | 
			
		||||
@@ -136,15 +137,15 @@ export class TspmDaemon {
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    // Query handlers
 | 
			
		||||
    this.ipcServer.on<RequestForMethod<'list'>>(
 | 
			
		||||
    this.ipcServer.onMessage(
 | 
			
		||||
      'list',
 | 
			
		||||
      async () => {
 | 
			
		||||
      async (request: RequestForMethod<'list'>) => {
 | 
			
		||||
        const processes = await this.tspmInstance.list();
 | 
			
		||||
        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 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);
 | 
			
		||||
      return { logs };
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // Batch operations handlers
 | 
			
		||||
    this.ipcServer.on<RequestForMethod<'startAll'>>('startAll', async () => {
 | 
			
		||||
    this.ipcServer.onMessage('startAll', async (request: RequestForMethod<'startAll'>) => {
 | 
			
		||||
      const started: string[] = [];
 | 
			
		||||
      const failed: Array<{ id: string; error: string }> = [];
 | 
			
		||||
 | 
			
		||||
@@ -182,7 +183,7 @@ export class TspmDaemon {
 | 
			
		||||
      return { started, failed };
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    this.ipcServer.on<RequestForMethod<'stopAll'>>('stopAll', async () => {
 | 
			
		||||
    this.ipcServer.onMessage('stopAll', async (request: RequestForMethod<'stopAll'>) => {
 | 
			
		||||
      const stopped: string[] = [];
 | 
			
		||||
      const failed: Array<{ id: string; error: string }> = [];
 | 
			
		||||
 | 
			
		||||
@@ -200,7 +201,7 @@ export class TspmDaemon {
 | 
			
		||||
      return { stopped, failed };
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    this.ipcServer.on<RequestForMethod<'restartAll'>>('restartAll', async () => {
 | 
			
		||||
    this.ipcServer.onMessage('restartAll', async (request: RequestForMethod<'restartAll'>) => {
 | 
			
		||||
      const restarted: string[] = [];
 | 
			
		||||
      const failed: Array<{ id: string; error: string }> = [];
 | 
			
		||||
 | 
			
		||||
@@ -219,7 +220,7 @@ export class TspmDaemon {
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // 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();
 | 
			
		||||
      return {
 | 
			
		||||
        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) {
 | 
			
		||||
        return {
 | 
			
		||||
          success: false,
 | 
			
		||||
@@ -256,7 +257,7 @@ export class TspmDaemon {
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // Heartbeat handler
 | 
			
		||||
    this.ipcServer.on<RequestForMethod<'heartbeat'>>('heartbeat', async () => {
 | 
			
		||||
    this.ipcServer.onMessage('heartbeat', async (request: RequestForMethod<'heartbeat'>) => {
 | 
			
		||||
      return {
 | 
			
		||||
        timestamp: Date.now(),
 | 
			
		||||
        status: this.isShuttingDown ? 'degraded' : 'healthy',
 | 
			
		||||
 
 | 
			
		||||
@@ -41,9 +41,10 @@ export class TspmIpcClient {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Create IPC client
 | 
			
		||||
    this.ipcClient = new plugins.smartipc.IpcClient({
 | 
			
		||||
    this.ipcClient = plugins.smartipc.SmartIpc.createClient({
 | 
			
		||||
      id: 'tspm-cli',
 | 
			
		||||
      socketPath: this.socketPath,
 | 
			
		||||
      heartbeat: false,  // Disable heartbeat for now
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // Connect to the daemon
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user