import { afterEach, beforeEach, describe, expect, it } from 'bun:test'; import * as http from 'http'; import * as fs from 'fs'; import * as path from 'path'; import * as os from 'os'; import { spawn } from 'child_process'; import { isDaemonRunning, startDaemon, stopDaemon } from '../../../src/copilot/copilot-daemon'; import { DEFAULT_COPILOT_CONFIG } from '../../../src/config/unified-config-types'; import { getCcsDir } from '../../../src/utils/config-manager'; const activeServers: http.Server[] = []; let originalCcsHome: string | undefined; let tempDir: string; beforeEach(() => { originalCcsHome = process.env.CCS_HOME; tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ccs-copilot-daemon-test-')); process.env.CCS_HOME = tempDir; }); afterEach(async () => { await Promise.all( activeServers.splice(0).map( (server) => new Promise((resolve) => { server.close(() => resolve()); }) ) ); if (originalCcsHome !== undefined) { process.env.CCS_HOME = originalCcsHome; } else { delete process.env.CCS_HOME; } try { fs.rmSync(tempDir, { recursive: true, force: true }); } catch { // Ignore cleanup errors } }); async function createServer( handler: (req: http.IncomingMessage, res: http.ServerResponse) => void ): Promise { const server = http.createServer(handler); activeServers.push(server); await new Promise((resolve) => { server.listen(0, '127.0.0.1', () => resolve()); }); const address = server.address(); if (!address || typeof address === 'string') { throw new Error('Unable to resolve server port'); } return address.port; } function writeCopilotPortConfig(port: number): void { const configPath = path.join(getCcsDir(), 'config.yaml'); fs.mkdirSync(path.dirname(configPath), { recursive: true }); fs.writeFileSync(configPath, `version: 8\ncopilot:\n port: ${port}\n`); } describe('copilot daemon health detection', () => { it('returns false for invalid port inputs', async () => { expect(await isDaemonRunning(0)).toBe(false); expect(await isDaemonRunning(65536)).toBe(false); }); it('returns false when no daemon is running on port', async () => { const running = await isDaemonRunning(19998); expect(running).toBe(false); }); it('returns true when daemon root endpoint confirms server is running', async () => { const port = await createServer((_req, res) => { res.writeHead(200, { 'Content-Type': 'text/plain' }); res.end('Server running'); }); const running = await isDaemonRunning(port); expect(running).toBe(true); }); it('returns false when root endpoint returns 200 but unexpected body', async () => { const port = await createServer((_req, res) => { res.writeHead(200, { 'Content-Type': 'text/plain' }); res.end('ok'); }); const running = await isDaemonRunning(port); expect(running).toBe(false); }); it('returns false when root endpoint returns 200 with empty body', async () => { const port = await createServer((_req, res) => { res.writeHead(200, { 'Content-Type': 'text/plain' }); res.end(''); }); const running = await isDaemonRunning(port); expect(running).toBe(false); }); it('returns false when root endpoint is non-200', async () => { const port = await createServer((_req, res) => { res.writeHead(503, { 'Content-Type': 'text/plain' }); res.end('unavailable'); }); const running = await isDaemonRunning(port); expect(running).toBe(false); }); it('fails fast when startDaemon is called with invalid port', async () => { const result = await startDaemon({ ...DEFAULT_COPILOT_CONFIG, port: 70000, }); expect(result.success).toBe(false); expect(result.error).toContain('Invalid Copilot daemon port'); }); }); describe('copilot daemon stop safety', () => { it('returns failure when stale PID points to unrelated process but daemon is still live', async () => { const port = await createServer((_req, res) => { res.writeHead(200, { 'Content-Type': 'text/plain' }); res.end('Server running'); }); writeCopilotPortConfig(port); const unrelatedProcess = spawn(process.execPath, ['-e', 'setInterval(() => {}, 1000);'], { detached: true, stdio: 'ignore', }); unrelatedProcess.unref(); const unrelatedPid = unrelatedProcess.pid; expect(unrelatedPid).toBeDefined(); if (!unrelatedPid) { throw new Error('Failed to spawn unrelated process'); } const pidFile = path.join(getCcsDir(), 'copilot', 'daemon.pid'); fs.mkdirSync(path.dirname(pidFile), { recursive: true }); fs.writeFileSync(pidFile, String(unrelatedPid)); try { const result = await stopDaemon(); expect(result.success).toBe(false); expect(result.error).toContain('daemon is still responding on port'); // Unrelated process should still be alive. expect(() => process.kill(unrelatedPid, 0)).not.toThrow(); } finally { try { process.kill(unrelatedPid, 'SIGTERM'); } catch { // Process already exited. } } }); it('does not terminate unrelated process from stale PID file', async () => { writeCopilotPortConfig(65534); const unrelatedProcess = spawn(process.execPath, ['-e', 'setInterval(() => {}, 1000);'], { detached: true, stdio: 'ignore', }); unrelatedProcess.unref(); const unrelatedPid = unrelatedProcess.pid; expect(unrelatedPid).toBeDefined(); if (!unrelatedPid) { throw new Error('Failed to spawn unrelated process'); } const pidFile = path.join(getCcsDir(), 'copilot', 'daemon.pid'); fs.mkdirSync(path.dirname(pidFile), { recursive: true }); fs.writeFileSync(pidFile, String(unrelatedPid)); try { const result = await stopDaemon(); expect(result.success).toBe(true); // Unrelated process should still be alive. expect(() => process.kill(unrelatedPid, 0)).not.toThrow(); } finally { try { process.kill(unrelatedPid, 'SIGTERM'); } catch { // Process already exited. } } }); });