fix(client): Improve IPC client robustness and daemon debug logging; update tests and package metadata

This commit is contained in:
2025-08-29 09:29:53 +00:00
parent ebf06d6153
commit 6a8e723c03
10 changed files with 305 additions and 177 deletions

View File

@@ -4,7 +4,7 @@ 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';
import { tspmIpcClient, TspmIpcClient } from '../ts/client/tspm.ipcclient.js';
// Helper to ensure daemon is stopped before tests
async function ensureDaemonStopped() {
@@ -26,6 +26,67 @@ async function cleanupTestFiles() {
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();
@@ -40,7 +101,8 @@ tap.test('Full daemon lifecycle test', async (tools) => {
// Test 2: Start daemon
console.log('Starting daemon...');
await tspmIpcClient.connect();
await startDaemonForTest();
await connectWithRetry();
// Give daemon time to fully initialize
await new Promise((resolve) => setTimeout(resolve, 2000));
@@ -63,6 +125,9 @@ tap.test('Full daemon lifecycle test', async (tools) => {
status = await tspmIpcClient.getDaemonStatus();
expect(status).toEqual(null);
// Ensure client disconnects cleanly
await tspmIpcClient.disconnect();
done.resolve();
});
@@ -70,13 +135,28 @@ tap.test('Process management through daemon', async (tools) => {
const done = tools.defer();
// Ensure daemon is running
await tspmIpcClient.connect();
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).toEqual(0);
expect(listResponse.processes.length).toBeGreaterThanOrEqual(0);
// Test 2: Start a test process
const testConfig: tspm.IProcessConfig = {
@@ -91,38 +171,43 @@ tap.test('Process management through daemon', async (tools) => {
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 process = listResponse.processes.find((p) => p.id === 'test-echo');
expect(process).toBeDefined();
expect(process?.id).toEqual('test-echo');
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);
expect(stopResponse.message).toInclude('stopped successfully');
// 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',
);
@@ -130,6 +215,7 @@ tap.test('Process management through daemon', async (tools) => {
// Cleanup: stop daemon
await tspmIpcClient.stopDaemon(true);
await tspmIpcClient.disconnect();
done.resolve();
});
@@ -138,7 +224,18 @@ tap.test('Batch operations through daemon', async (tools) => {
const done = tools.defer();
// Ensure daemon is running
await tspmIpcClient.connect();
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
@@ -186,6 +283,7 @@ tap.test('Batch operations through daemon', async (tools) => {
// Stop daemon
await tspmIpcClient.stopDaemon(true);
await tspmIpcClient.disconnect();
done.resolve();
});
@@ -194,7 +292,18 @@ tap.test('Daemon error handling', async (tools) => {
const done = tools.defer();
// Ensure daemon is running
await tspmIpcClient.connect();
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
@@ -223,6 +332,7 @@ tap.test('Daemon error handling', async (tools) => {
// Stop daemon
await tspmIpcClient.stopDaemon(true);
await tspmIpcClient.disconnect();
done.resolve();
});
@@ -231,7 +341,18 @@ tap.test('Daemon heartbeat functionality', async (tools) => {
const done = tools.defer();
// Ensure daemon is running
await tspmIpcClient.connect();
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
@@ -241,6 +362,7 @@ tap.test('Daemon heartbeat functionality', async (tools) => {
// Stop daemon
await tspmIpcClient.stopDaemon(true);
await tspmIpcClient.disconnect();
done.resolve();
});
@@ -249,7 +371,18 @@ tap.test('Daemon memory and CPU reporting', async (tools) => {
const done = tools.defer();
// Ensure daemon is running
await tspmIpcClient.connect();
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
@@ -261,6 +394,7 @@ tap.test('Daemon memory and CPU reporting', async (tools) => {
// Stop daemon
await tspmIpcClient.stopDaemon(true);
await tspmIpcClient.disconnect();
done.resolve();
});