import { expect, tap } from '@git.zone/tstest/tapbundle'; import * as smartipc from '../ts/index.js'; import * as path from 'path'; import * as fs from 'fs'; import * as os from 'os'; const testSocketPath = path.join(os.tmpdir(), `test-ipc-reliability-${Date.now()}.sock`); tap.test('Server Readiness API', async () => { const server = smartipc.SmartIpc.createServer({ id: 'test-server', socketPath: testSocketPath, autoCleanupSocketFile: true }); let readyEventFired = false; server.on('ready', () => { readyEventFired = true; }); // Start server with 'accepting' readiness mode await server.start({ readyWhen: 'accepting' }); // Check that ready event was fired expect(readyEventFired).toBeTrue(); expect(server.getIsReady()).toBeTrue(); await server.stop(); }); tap.test('Automatic Socket Cleanup', async () => { // Create a stale socket file fs.writeFileSync(testSocketPath, ''); expect(fs.existsSync(testSocketPath)).toBeTrue(); const server = smartipc.SmartIpc.createServer({ id: 'test-server', socketPath: testSocketPath, autoCleanupSocketFile: true, socketMode: 0o600 }); // Should clean up stale socket and start successfully await server.start(); expect(server.getIsReady()).toBeTrue(); await server.stop(); }); tap.test('Client Connection Retry', async () => { const server = smartipc.SmartIpc.createServer({ id: 'retry-server', socketPath: testSocketPath, autoCleanupSocketFile: true }); // Create client with retry configuration const client = smartipc.SmartIpc.createClient({ id: 'retry-client', socketPath: testSocketPath, connectRetry: { enabled: true, initialDelay: 50, maxDelay: 500, maxAttempts: 10, totalTimeout: 5000 }, registerTimeoutMs: 3000 }); // Start server first with accepting readiness mode await server.start({ readyWhen: 'accepting' }); // Give server a moment to be fully ready await new Promise(resolve => setTimeout(resolve, 100)); // Client should connect successfully with retry enabled await client.connect(); expect(client.getIsConnected()).toBeTrue(); await client.disconnect(); await server.stop(); }); tap.test('Graceful Heartbeat Handling', async () => { const server = smartipc.SmartIpc.createServer({ id: 'heartbeat-server', socketPath: testSocketPath, autoCleanupSocketFile: true, heartbeat: true, heartbeatInterval: 100, heartbeatTimeout: 500, heartbeatInitialGracePeriodMs: 1000, heartbeatThrowOnTimeout: false }); // Add error handler to prevent unhandled error server.on('error', (error) => { // Ignore heartbeat errors in this test }); await server.start({ readyWhen: 'accepting' }); // Give server a moment to be fully ready await new Promise(resolve => setTimeout(resolve, 100)); const client = smartipc.SmartIpc.createClient({ id: 'heartbeat-client', socketPath: testSocketPath, heartbeat: true, heartbeatInterval: 100, heartbeatTimeout: 500, heartbeatInitialGracePeriodMs: 1000, heartbeatThrowOnTimeout: false }); let heartbeatTimeoutFired = false; client.on('heartbeatTimeout', () => { heartbeatTimeoutFired = true; }); // Add error handler to prevent unhandled error client.on('error', (error) => { // Ignore errors in this test }); await client.connect(); expect(client.getIsConnected()).toBeTrue(); // Wait to ensure heartbeat is working await new Promise(resolve => setTimeout(resolve, 300)); // Heartbeat should not timeout during normal operation expect(heartbeatTimeoutFired).toBeFalse(); await client.disconnect(); await server.stop(); }); tap.test('Test Helper - waitForServer', async () => { const server = smartipc.SmartIpc.createServer({ id: 'wait-test-server', socketPath: testSocketPath, autoCleanupSocketFile: true }); // Start server after a delay setTimeout(() => { server.start(); }, 100); // Wait for server should succeed await smartipc.SmartIpc.waitForServer({ socketPath: testSocketPath, timeoutMs: 3000 }); // Server should be ready const client = smartipc.SmartIpc.createClient({ id: 'wait-test-client', socketPath: testSocketPath }); await client.connect(); expect(client.getIsConnected()).toBeTrue(); await client.disconnect(); await server.stop(); }); tap.test('Race Condition - Immediate Connect After Server Start', async () => { const server = smartipc.SmartIpc.createServer({ id: 'race-server', socketPath: testSocketPath, autoCleanupSocketFile: true }); // Start server and immediately try to connect const serverPromise = server.start({ readyWhen: 'accepting' }); const client = smartipc.SmartIpc.createClient({ id: 'race-client', socketPath: testSocketPath, connectRetry: { enabled: true, maxAttempts: 20, initialDelay: 10, maxDelay: 100 }, registerTimeoutMs: 5000 }); // Wait for server to be ready await serverPromise; // Client should be able to connect without race condition await client.connect(); expect(client.getIsConnected()).toBeTrue(); // Test request/response to ensure full functionality server.onMessage('test', async (data) => { return { echo: data }; }); const response = await client.request('test', { message: 'hello' }); expect(response.echo.message).toEqual('hello'); await client.disconnect(); await server.stop(); }); tap.test('Multiple Clients with Retry', async () => { const server = smartipc.SmartIpc.createServer({ id: 'multi-server', socketPath: testSocketPath, autoCleanupSocketFile: true, maxClients: 10 }); await server.start({ readyWhen: 'accepting' }); // Create multiple clients with retry const clients = []; for (let i = 0; i < 5; i++) { const client = smartipc.SmartIpc.createClient({ id: `client-${i}`, socketPath: testSocketPath, connectRetry: { enabled: true, maxAttempts: 5 } }); clients.push(client); } // Connect all clients concurrently await Promise.all(clients.map(c => c.connect())); // Verify all connected for (const client of clients) { expect(client.getIsConnected()).toBeTrue(); } // Disconnect all await Promise.all(clients.map(c => c.disconnect())); await server.stop(); }); tap.test('Server Restart with Socket Cleanup', async () => { const server = smartipc.SmartIpc.createServer({ id: 'restart-server', socketPath: testSocketPath, autoCleanupSocketFile: true }); // First start await server.start(); expect(server.getIsReady()).toBeTrue(); await server.stop(); // Second start - should cleanup and work await server.start(); expect(server.getIsReady()).toBeTrue(); const client = smartipc.SmartIpc.createClient({ id: 'restart-client', socketPath: testSocketPath }); await client.connect(); expect(client.getIsConnected()).toBeTrue(); await client.disconnect(); await server.stop(); }); // Clean up test socket file tap.test('Cleanup', async () => { try { fs.unlinkSync(testSocketPath); } catch (e) { // Ignore if doesn't exist } }); export default tap.start();