/**
* Tests for DaemonRegistry service
*/
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { existsSync, unlinkSync } from 'fs';
import { DaemonRegistry, DaemonInfo } from '../../../src/daemon/registry/daemon-registry.js';
describe('DaemonRegistry', () => {
const testDaemonInfo: DaemonInfo = {
pid: process.pid,
httpPort: 8765,
wsPort: 8766,
startTime: new Date().toISOString(),
version: '1.0.0'
};
beforeEach(async () => {
// CRITICAL: Kill any leftover daemon processes from other tests
const { spawn } = await import('child_process');
try {
const isWindows = process.platform === 'win32';
if (isWindows) {
// Windows: Use wmic to get command line details (only kill daemon processes, not Claude Code!)
const wmic = spawn('wmic', ['process', 'where', 'name="node.exe"', 'get', 'ProcessId,CommandLine', '/FORMAT:CSV'], { stdio: 'pipe' });
let output = '';
wmic.stdout.on('data', (data) => {
output += data.toString();
});
await new Promise<void>((resolve) => {
wmic.on('close', async () => {
const lines = output.split('\n');
for (const line of lines) {
// Look specifically for daemon processes (both path separators)
if ((line.includes('dist\\src\\daemon\\index.js') || line.includes('dist/src/daemon/index.js')) && !line.includes('grep')) {
// Extract PID from CSV format - last numeric field
const match = line.match(/,(\d+)/);
const pid = match && match[1] ? parseInt(match[1], 10) : NaN;
if (!isNaN(pid) && pid !== process.pid) {
try {
// Use taskkill on Windows - await completion to prevent race conditions
await new Promise<void>((resolveKill, rejectKill) => {
const killProcess = spawn('taskkill', ['/PID', pid.toString(), '/F'], { stdio: 'ignore' });
killProcess.on('close', (code) => {
if (code === 0 || code === 128) {
// Success or process already dead
console.log(`[TEST-CLEANUP] Killed leftover daemon process ${pid}`);
resolveKill();
} else {
rejectKill(new Error(`taskkill exited with code ${code}`));
}
});
killProcess.on('error', (err) => {
rejectKill(err);
});
});
} catch (e) {
// Process might already be dead or kill failed
console.log(`[TEST-CLEANUP] Failed to kill process ${pid}: ${e}`);
}
}
}
}
resolve();
});
});
} else {
// Unix: Use ps aux
const ps = spawn('ps', ['aux'], { stdio: 'pipe' });
let output = '';
ps.stdout.on('data', (data) => {
output += data.toString();
});
await new Promise<void>((resolve) => {
ps.on('close', () => {
const lines = output.split('\n');
for (const line of lines) {
if (line.includes('dist/src/daemon/index.js') && !line.includes('grep')) {
const parts = line.trim().split(/\s+/);
const pid = parts[1] ? parseInt(parts[1], 10) : NaN;
if (!isNaN(pid) && pid !== process.pid) {
try {
process.kill(pid, 'SIGKILL');
console.log(`[TEST-CLEANUP] Killed leftover daemon process ${pid}`);
} catch (e) {
// Process might already be dead
}
}
}
}
resolve();
});
});
}
} catch (e) {
// Ignore cleanup errors
}
// Clean up any existing registry before each test
await DaemonRegistry.cleanup();
}, 30000); // Increase timeout for Windows - process enumeration can be slower
afterEach(async () => {
// Clean up registry after each test
await DaemonRegistry.cleanup();
});
describe('register', () => {
it('should register daemon info successfully', async () => {
await DaemonRegistry.register(testDaemonInfo);
expect(DaemonRegistry.registryExists()).toBe(true);
const discovered = await DaemonRegistry.discover();
expect(discovered).toEqual(testDaemonInfo);
});
it('should allow re-registration with same PID but different ports', async () => {
// Register first daemon with current PID
await DaemonRegistry.register(testDaemonInfo);
// Try to register different daemon with same PID but different port
const conflictingDaemon: DaemonInfo = {
...testDaemonInfo,
httpPort: 9001, // Different port, same PID
wsPort: 9002
};
// This should succeed since it's the same PID (re-registration allowed)
await expect(DaemonRegistry.register(conflictingDaemon)).resolves.not.toThrow();
// Verify the ports were updated
const discovered = await DaemonRegistry.discover();
expect(discovered?.httpPort).toBe(9001);
});
it('should clean up stale registry when registering new daemon', async () => {
// Create a stale registry entry with fake PID
const staleInfo: DaemonInfo = {
pid: 99999, // Very unlikely to be running
httpPort: 8765,
wsPort: 8766,
startTime: new Date().toISOString()
};
// Manually write stale registry (bypass the registration validation)
const registryPath = DaemonRegistry.getRegistryPath();
require('fs').writeFileSync(registryPath, JSON.stringify(staleInfo, null, 2));
// Try to register new daemon - should clean up stale entry and succeed
await expect(DaemonRegistry.register(testDaemonInfo)).resolves.not.toThrow();
// Verify the new daemon is registered
const discovered = await DaemonRegistry.discover();
expect(discovered?.pid).toBe(testDaemonInfo.pid);
});
it('should allow re-registration of same daemon PID', async () => {
await DaemonRegistry.register(testDaemonInfo);
const updatedInfo: DaemonInfo = {
...testDaemonInfo,
httpPort: 9002, // Different port, same PID
wsPort: 9003
};
await expect(DaemonRegistry.register(updatedInfo)).resolves.not.toThrow();
const discovered = await DaemonRegistry.discover();
expect(discovered?.httpPort).toBe(9002);
});
});
describe('discover', () => {
it('should return null when no registry exists', async () => {
const discovered = await DaemonRegistry.discover();
expect(discovered).toBeNull();
});
it('should return daemon info when registry exists and process running', async () => {
await DaemonRegistry.register(testDaemonInfo);
const discovered = await DaemonRegistry.discover();
expect(discovered).toEqual(testDaemonInfo);
});
it('should clean up stale registry when process not running', async () => {
const staleInfo: DaemonInfo = {
pid: 99999, // Very unlikely to be a running process
httpPort: 8765,
wsPort: 8766,
startTime: new Date().toISOString()
};
await DaemonRegistry.register(staleInfo);
expect(DaemonRegistry.registryExists()).toBe(true);
// Discovery should clean up stale entry
const discovered = await DaemonRegistry.discover();
expect(discovered).toBeNull();
expect(DaemonRegistry.registryExists()).toBe(false);
});
it('should handle corrupted registry file', async () => {
// Create corrupted registry file
const registryPath = DaemonRegistry.getRegistryPath();
require('fs').writeFileSync(registryPath, 'invalid json content');
const discovered = await DaemonRegistry.discover();
expect(discovered).toBeNull();
expect(DaemonRegistry.registryExists()).toBe(false);
});
it('should handle incomplete daemon info', async () => {
const registryPath = DaemonRegistry.getRegistryPath();
require('fs').writeFileSync(registryPath, JSON.stringify({ pid: 123 })); // Missing required fields
const discovered = await DaemonRegistry.discover();
expect(discovered).toBeNull();
expect(DaemonRegistry.registryExists()).toBe(false);
});
});
describe('cleanup', () => {
it('should remove registry file if it exists', async () => {
await DaemonRegistry.register(testDaemonInfo);
expect(DaemonRegistry.registryExists()).toBe(true);
await DaemonRegistry.cleanup();
expect(DaemonRegistry.registryExists()).toBe(false);
});
it('should not throw error if registry file does not exist', async () => {
expect(DaemonRegistry.registryExists()).toBe(false);
await expect(DaemonRegistry.cleanup()).resolves.not.toThrow();
});
});
describe('isProcessRunning', () => {
it('should return true for current process', async () => {
const isRunning = await DaemonRegistry.isProcessRunning(process.pid);
expect(isRunning).toBe(true);
});
it('should return false for non-existent process', async () => {
const isRunning = await DaemonRegistry.isProcessRunning(99999);
expect(isRunning).toBe(false);
});
});
describe('utility methods', () => {
it('should return correct registry path', () => {
const path = DaemonRegistry.getRegistryPath();
expect(path).toContain('.folder-mcp');
expect(path).toContain('daemon.pid');
});
it('should correctly check registry existence', async () => {
expect(DaemonRegistry.registryExists()).toBe(false);
await DaemonRegistry.register(testDaemonInfo);
expect(DaemonRegistry.registryExists()).toBe(true);
await DaemonRegistry.cleanup();
expect(DaemonRegistry.registryExists()).toBe(false);
});
});
});