2025-08-25 13:37:31 +00:00
|
|
|
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-improvements-${Date.now()}.sock`);
|
|
|
|
|
|
|
|
// Test 1: Server Readiness API
|
|
|
|
tap.test('Server readiness API should emit ready event', async () => {
|
|
|
|
const server = smartipc.SmartIpc.createServer({
|
|
|
|
id: 'test-server',
|
|
|
|
socketPath: testSocketPath,
|
|
|
|
autoCleanupSocketFile: true,
|
|
|
|
heartbeat: false // Disable heartbeat for this test
|
|
|
|
});
|
|
|
|
|
|
|
|
let readyEventFired = false;
|
|
|
|
server.on('ready', () => {
|
|
|
|
readyEventFired = true;
|
|
|
|
});
|
|
|
|
|
|
|
|
await server.start({ readyWhen: 'accepting' });
|
|
|
|
|
|
|
|
expect(readyEventFired).toBeTrue();
|
|
|
|
expect(server.getIsReady()).toBeTrue();
|
|
|
|
|
|
|
|
await server.stop();
|
|
|
|
});
|
|
|
|
|
|
|
|
// Test 2: Automatic Socket Cleanup
|
|
|
|
tap.test('Should cleanup stale socket file automatically', 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,
|
|
|
|
heartbeat: false // Disable heartbeat for this test
|
|
|
|
});
|
|
|
|
|
|
|
|
// Should clean up and start successfully
|
|
|
|
await server.start();
|
|
|
|
expect(server.getIsReady()).toBeTrue();
|
|
|
|
|
|
|
|
await server.stop();
|
|
|
|
});
|
|
|
|
|
|
|
|
// Test 3: Basic Connection with New Options
|
|
|
|
tap.test('Client should connect with basic configuration', async () => {
|
|
|
|
const server = smartipc.SmartIpc.createServer({
|
|
|
|
id: 'test-server',
|
|
|
|
socketPath: testSocketPath,
|
|
|
|
autoCleanupSocketFile: true,
|
|
|
|
heartbeat: false // Disable heartbeat for this test
|
|
|
|
});
|
|
|
|
|
|
|
|
await server.start({ readyWhen: 'accepting' });
|
|
|
|
|
|
|
|
// Wait for server to be fully ready
|
|
|
|
await new Promise(resolve => setTimeout(resolve, 200));
|
|
|
|
|
|
|
|
const client = smartipc.SmartIpc.createClient({
|
|
|
|
id: 'test-server',
|
|
|
|
socketPath: testSocketPath,
|
|
|
|
clientId: 'test-client',
|
|
|
|
registerTimeoutMs: 10000 // Longer timeout
|
|
|
|
});
|
|
|
|
|
|
|
|
await client.connect();
|
|
|
|
expect(client.getIsConnected()).toBeTrue();
|
|
|
|
|
|
|
|
await client.disconnect();
|
|
|
|
await server.stop();
|
|
|
|
});
|
|
|
|
|
|
|
|
// Test 4: Heartbeat Configuration Without Throwing
|
|
|
|
tap.test('Heartbeat should use event mode instead of throwing', async () => {
|
|
|
|
const server = smartipc.SmartIpc.createServer({
|
|
|
|
id: 'test-server',
|
|
|
|
socketPath: testSocketPath,
|
|
|
|
autoCleanupSocketFile: true,
|
|
|
|
heartbeat: false // Disable server heartbeat for this test
|
|
|
|
});
|
|
|
|
|
|
|
|
// Add error handler to prevent unhandled errors
|
|
|
|
server.on('error', () => {});
|
|
|
|
|
|
|
|
await server.start({ readyWhen: 'accepting' });
|
|
|
|
await new Promise(resolve => setTimeout(resolve, 200));
|
|
|
|
|
|
|
|
const client = smartipc.SmartIpc.createClient({
|
|
|
|
id: 'test-server',
|
|
|
|
socketPath: testSocketPath,
|
|
|
|
clientId: 'heartbeat-client',
|
|
|
|
heartbeat: true,
|
|
|
|
heartbeatInterval: 100,
|
|
|
|
heartbeatTimeout: 300,
|
|
|
|
heartbeatInitialGracePeriodMs: 1000,
|
|
|
|
heartbeatThrowOnTimeout: false // Don't throw, emit event
|
|
|
|
});
|
|
|
|
|
|
|
|
let heartbeatTimeoutFired = false;
|
|
|
|
client.on('heartbeatTimeout', () => {
|
|
|
|
heartbeatTimeoutFired = true;
|
|
|
|
});
|
|
|
|
|
|
|
|
client.on('error', () => {});
|
|
|
|
|
|
|
|
await client.connect();
|
|
|
|
expect(client.getIsConnected()).toBeTrue();
|
|
|
|
|
|
|
|
// Wait a bit but within grace period
|
|
|
|
await new Promise(resolve => setTimeout(resolve, 500));
|
|
|
|
|
|
|
|
// Should still be connected, no timeout during grace period
|
|
|
|
expect(heartbeatTimeoutFired).toBeFalse();
|
|
|
|
expect(client.getIsConnected()).toBeTrue();
|
|
|
|
|
|
|
|
await client.disconnect();
|
|
|
|
await server.stop();
|
|
|
|
});
|
|
|
|
|
|
|
|
// Test 5: Wait for Server Helper
|
|
|
|
tap.test('waitForServer should detect when server becomes ready', async () => {
|
|
|
|
const server = smartipc.SmartIpc.createServer({
|
|
|
|
id: 'test-server',
|
|
|
|
socketPath: testSocketPath,
|
|
|
|
autoCleanupSocketFile: true,
|
|
|
|
heartbeat: false // Disable heartbeat for this test
|
|
|
|
});
|
|
|
|
|
|
|
|
// Start server after delay
|
|
|
|
setTimeout(async () => {
|
|
|
|
await server.start();
|
|
|
|
}, 200);
|
|
|
|
|
|
|
|
// Wait for server should succeed
|
|
|
|
await smartipc.SmartIpc.waitForServer({
|
|
|
|
socketPath: testSocketPath,
|
|
|
|
timeoutMs: 3000
|
|
|
|
});
|
|
|
|
|
|
|
|
// Server should be ready now
|
|
|
|
const client = smartipc.SmartIpc.createClient({
|
|
|
|
id: 'test-server',
|
|
|
|
socketPath: testSocketPath,
|
|
|
|
clientId: 'wait-test-client'
|
|
|
|
});
|
|
|
|
|
|
|
|
await client.connect();
|
|
|
|
expect(client.getIsConnected()).toBeTrue();
|
|
|
|
|
|
|
|
await client.disconnect();
|
|
|
|
await server.stop();
|
|
|
|
});
|
|
|
|
|
|
|
|
// Test 6: Connect Retry Configuration
|
|
|
|
tap.test('Client retry should work with delayed server', async () => {
|
|
|
|
const server = smartipc.SmartIpc.createServer({
|
|
|
|
id: 'test-server',
|
|
|
|
socketPath: testSocketPath,
|
|
|
|
autoCleanupSocketFile: true,
|
|
|
|
heartbeat: false // Disable heartbeat for this test
|
|
|
|
});
|
|
|
|
|
|
|
|
const client = smartipc.SmartIpc.createClient({
|
|
|
|
id: 'test-server',
|
|
|
|
socketPath: testSocketPath,
|
|
|
|
clientId: 'retry-client',
|
|
|
|
connectRetry: {
|
|
|
|
enabled: true,
|
|
|
|
initialDelay: 100,
|
|
|
|
maxDelay: 500,
|
|
|
|
maxAttempts: 10,
|
|
|
|
totalTimeout: 5000
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
// Start server after a delay
|
|
|
|
setTimeout(async () => {
|
|
|
|
await server.start({ readyWhen: 'accepting' });
|
|
|
|
}, 300);
|
|
|
|
|
|
|
|
// Client should retry and eventually connect
|
|
|
|
await client.connect({ waitForReady: true, waitTimeout: 5000 });
|
|
|
|
expect(client.getIsConnected()).toBeTrue();
|
|
|
|
|
|
|
|
await client.disconnect();
|
|
|
|
await server.stop();
|
|
|
|
});
|
|
|
|
|
2025-08-29 08:48:38 +00:00
|
|
|
// Test 7: clientOnly prevents client from auto-starting a server
|
|
|
|
tap.test('clientOnly should prevent auto-start and fail fast', async () => {
|
|
|
|
const uniqueSocketPath = path.join(os.tmpdir(), `smartipc-clientonly-${Date.now()}.sock`);
|
|
|
|
|
|
|
|
const client = smartipc.SmartIpc.createClient({
|
|
|
|
id: 'clientonly-test',
|
|
|
|
socketPath: uniqueSocketPath,
|
|
|
|
clientId: 'co-client-1',
|
|
|
|
clientOnly: true,
|
|
|
|
connectRetry: { enabled: false }
|
|
|
|
});
|
|
|
|
|
|
|
|
let failed = false;
|
|
|
|
try {
|
|
|
|
await client.connect();
|
|
|
|
} catch (err: any) {
|
|
|
|
failed = true;
|
|
|
|
expect(err.message).toContain('clientOnly prevents auto-start');
|
|
|
|
}
|
|
|
|
expect(failed).toBeTrue();
|
|
|
|
// Ensure no server-side socket was created
|
|
|
|
expect(fs.existsSync(uniqueSocketPath)).toBeFalse();
|
|
|
|
});
|
|
|
|
|
|
|
|
// Test 8: env SMARTIPC_CLIENT_ONLY enforces clientOnly behavior
|
|
|
|
tap.test('SMARTIPC_CLIENT_ONLY=1 should enforce clientOnly', async () => {
|
|
|
|
const uniqueSocketPath = path.join(os.tmpdir(), `smartipc-clientonly-env-${Date.now()}.sock`);
|
|
|
|
const prev = process.env.SMARTIPC_CLIENT_ONLY;
|
|
|
|
process.env.SMARTIPC_CLIENT_ONLY = '1';
|
|
|
|
|
|
|
|
const client = smartipc.SmartIpc.createClient({
|
|
|
|
id: 'clientonly-test-env',
|
|
|
|
socketPath: uniqueSocketPath,
|
|
|
|
clientId: 'co-client-2',
|
|
|
|
connectRetry: { enabled: false }
|
|
|
|
});
|
|
|
|
|
|
|
|
let failed = false;
|
|
|
|
try {
|
|
|
|
await client.connect();
|
|
|
|
} catch (err: any) {
|
|
|
|
failed = true;
|
|
|
|
expect(err.message).toContain('clientOnly prevents auto-start');
|
|
|
|
}
|
|
|
|
expect(failed).toBeTrue();
|
|
|
|
expect(fs.existsSync(uniqueSocketPath)).toBeFalse();
|
|
|
|
|
|
|
|
// restore env
|
|
|
|
if (prev === undefined) {
|
|
|
|
delete process.env.SMARTIPC_CLIENT_ONLY;
|
|
|
|
} else {
|
|
|
|
process.env.SMARTIPC_CLIENT_ONLY = prev;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2025-08-25 13:37:31 +00:00
|
|
|
// Cleanup
|
|
|
|
tap.test('Cleanup test socket', async () => {
|
|
|
|
try {
|
|
|
|
fs.unlinkSync(testSocketPath);
|
|
|
|
} catch (e) {
|
|
|
|
// Ignore if doesn't exist
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2025-08-29 08:48:38 +00:00
|
|
|
export default tap.start();
|