import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { EventEmitter } from 'events'; import path from 'path'; import os from 'os'; import fs from 'fs/promises'; // Mock child_process vi.mock('child_process', () => ({ spawn: vi.fn(), execSync: vi.fn(), })); // Mock secure-fs vi.mock('@/lib/secure-fs.js', () => ({ access: vi.fn(), })); // Mock net vi.mock('net', () => ({ default: { createServer: vi.fn(), }, createServer: vi.fn(), })); import { spawn, execSync } from 'child_process'; import * as secureFs from '@/lib/secure-fs.js'; import net from 'net'; describe('dev-server-service.ts', () => { let testDir: string; beforeEach(async () => { vi.clearAllMocks(); vi.resetModules(); testDir = path.join(os.tmpdir(), `dev-server-test-${Date.now()}`); await fs.mkdir(testDir, { recursive: true }); // Default mock for secureFs.access - return resolved (file exists) vi.mocked(secureFs.access).mockResolvedValue(undefined); // Default mock for net.createServer - port available const mockServer = new EventEmitter() as any; mockServer.listen = vi.fn().mockImplementation((port: number, host: string) => { process.nextTick(() => mockServer.emit('listening')); }); mockServer.close = vi.fn(); vi.mocked(net.createServer).mockReturnValue(mockServer); // Default mock for execSync - no process on port vi.mocked(execSync).mockImplementation(() => { throw new Error('No process found'); }); }); afterEach(async () => { try { await fs.rm(testDir, { recursive: true, force: true }); } catch { // Ignore cleanup errors } }); describe('getDevServerService', () => { it('should return a singleton instance', async () => { const { getDevServerService } = await import('@/services/dev-server-service.js'); const instance1 = getDevServerService(); const instance2 = getDevServerService(); expect(instance1).toBe(instance2); }); }); describe('startDevServer', () => { it('should return error if worktree path does not exist', async () => { vi.mocked(secureFs.access).mockRejectedValueOnce(new Error('File not found')); const { getDevServerService } = await import('@/services/dev-server-service.js'); const service = getDevServerService(); const result = await service.startDevServer('/project', '/nonexistent/worktree'); expect(result.success).toBe(false); expect(result.error).toContain('does not exist'); }); it('should return error if no package.json found', async () => { vi.mocked(secureFs.access).mockImplementation(async (p: any) => { if (typeof p === 'string' && p.includes('package.json')) { throw new Error('File not found'); } return undefined; }); const { getDevServerService } = await import('@/services/dev-server-service.js'); const service = getDevServerService(); const result = await service.startDevServer(testDir, testDir); expect(result.success).toBe(false); expect(result.error).toContain('No package.json found'); }); it('should detect npm as package manager with package-lock.json', async () => { vi.mocked(secureFs.access).mockImplementation(async (p: any) => { const pathStr = typeof p === 'string' ? p : ''; if (pathStr.includes('bun.lockb')) throw new Error('Not found'); if (pathStr.includes('pnpm-lock.yaml')) throw new Error('Not found'); if (pathStr.includes('yarn.lock')) throw new Error('Not found'); if (pathStr.includes('package-lock.json')) return undefined; if (pathStr.includes('package.json')) return undefined; return undefined; }); const mockProcess = createMockProcess(); vi.mocked(spawn).mockReturnValue(mockProcess as any); const { getDevServerService } = await import('@/services/dev-server-service.js'); const service = getDevServerService(); await service.startDevServer(testDir, testDir); expect(spawn).toHaveBeenCalledWith('npm', ['run', 'dev'], expect.any(Object)); }); it('should detect yarn as package manager with yarn.lock', async () => { vi.mocked(secureFs.access).mockImplementation(async (p: any) => { const pathStr = typeof p === 'string' ? p : ''; if (pathStr.includes('bun.lockb')) throw new Error('Not found'); if (pathStr.includes('pnpm-lock.yaml')) throw new Error('Not found'); if (pathStr.includes('yarn.lock')) return undefined; if (pathStr.includes('package.json')) return undefined; return undefined; }); const mockProcess = createMockProcess(); vi.mocked(spawn).mockReturnValue(mockProcess as any); const { getDevServerService } = await import('@/services/dev-server-service.js'); const service = getDevServerService(); await service.startDevServer(testDir, testDir); expect(spawn).toHaveBeenCalledWith('yarn', ['dev'], expect.any(Object)); }); it('should detect pnpm as package manager with pnpm-lock.yaml', async () => { vi.mocked(secureFs.access).mockImplementation(async (p: any) => { const pathStr = typeof p === 'string' ? p : ''; if (pathStr.includes('bun.lockb')) throw new Error('Not found'); if (pathStr.includes('pnpm-lock.yaml')) return undefined; if (pathStr.includes('package.json')) return undefined; return undefined; }); const mockProcess = createMockProcess(); vi.mocked(spawn).mockReturnValue(mockProcess as any); const { getDevServerService } = await import('@/services/dev-server-service.js'); const service = getDevServerService(); await service.startDevServer(testDir, testDir); expect(spawn).toHaveBeenCalledWith('pnpm', ['run', 'dev'], expect.any(Object)); }); it('should detect bun as package manager with bun.lockb', async () => { vi.mocked(secureFs.access).mockImplementation(async (p: any) => { const pathStr = typeof p === 'string' ? p : ''; if (pathStr.includes('bun.lockb')) return undefined; if (pathStr.includes('package.json')) return undefined; return undefined; }); const mockProcess = createMockProcess(); vi.mocked(spawn).mockReturnValue(mockProcess as any); const { getDevServerService } = await import('@/services/dev-server-service.js'); const service = getDevServerService(); await service.startDevServer(testDir, testDir); expect(spawn).toHaveBeenCalledWith('bun', ['run', 'dev'], expect.any(Object)); }); it('should return existing server info if already running', async () => { vi.mocked(secureFs.access).mockResolvedValue(undefined); const mockProcess = createMockProcess(); vi.mocked(spawn).mockReturnValue(mockProcess as any); const { getDevServerService } = await import('@/services/dev-server-service.js'); const service = getDevServerService(); // Start first server const result1 = await service.startDevServer(testDir, testDir); expect(result1.success).toBe(true); // Try to start again - should return existing const result2 = await service.startDevServer(testDir, testDir); expect(result2.success).toBe(true); expect(result2.result?.message).toContain('already running'); }); it('should start dev server successfully', async () => { vi.mocked(secureFs.access).mockResolvedValue(undefined); const mockProcess = createMockProcess(); vi.mocked(spawn).mockReturnValue(mockProcess as any); const { getDevServerService } = await import('@/services/dev-server-service.js'); const service = getDevServerService(); const result = await service.startDevServer(testDir, testDir); expect(result.success).toBe(true); expect(result.result).toBeDefined(); expect(result.result?.port).toBeGreaterThanOrEqual(3001); expect(result.result?.url).toContain('http://localhost:'); }); }); describe('stopDevServer', () => { it('should return success if server not found', async () => { const { getDevServerService } = await import('@/services/dev-server-service.js'); const service = getDevServerService(); const result = await service.stopDevServer('/nonexistent/path'); expect(result.success).toBe(true); expect(result.result?.message).toContain('already stopped'); }); it('should stop a running server', async () => { vi.mocked(secureFs.access).mockResolvedValue(undefined); const mockProcess = createMockProcess(); vi.mocked(spawn).mockReturnValue(mockProcess as any); const { getDevServerService } = await import('@/services/dev-server-service.js'); const service = getDevServerService(); // Start server await service.startDevServer(testDir, testDir); // Stop server const result = await service.stopDevServer(testDir); expect(result.success).toBe(true); expect(mockProcess.kill).toHaveBeenCalledWith('SIGTERM'); }); }); describe('listDevServers', () => { it('should return empty list when no servers running', async () => { const { getDevServerService } = await import('@/services/dev-server-service.js'); const service = getDevServerService(); const result = service.listDevServers(); expect(result.success).toBe(true); expect(result.result.servers).toEqual([]); }); it('should list running servers', async () => { vi.mocked(secureFs.access).mockResolvedValue(undefined); const mockProcess = createMockProcess(); vi.mocked(spawn).mockReturnValue(mockProcess as any); const { getDevServerService } = await import('@/services/dev-server-service.js'); const service = getDevServerService(); await service.startDevServer(testDir, testDir); const result = service.listDevServers(); expect(result.success).toBe(true); expect(result.result.servers.length).toBeGreaterThanOrEqual(1); expect(result.result.servers[0].worktreePath).toBe(testDir); }); }); describe('isRunning', () => { it('should return false for non-running server', async () => { const { getDevServerService } = await import('@/services/dev-server-service.js'); const service = getDevServerService(); expect(service.isRunning('/some/path')).toBe(false); }); it('should return true for running server', async () => { vi.mocked(secureFs.access).mockResolvedValue(undefined); const mockProcess = createMockProcess(); vi.mocked(spawn).mockReturnValue(mockProcess as any); const { getDevServerService } = await import('@/services/dev-server-service.js'); const service = getDevServerService(); await service.startDevServer(testDir, testDir); expect(service.isRunning(testDir)).toBe(true); }); }); describe('getServerInfo', () => { it('should return undefined for non-running server', async () => { const { getDevServerService } = await import('@/services/dev-server-service.js'); const service = getDevServerService(); expect(service.getServerInfo('/some/path')).toBeUndefined(); }); it('should return info for running server', async () => { vi.mocked(secureFs.access).mockResolvedValue(undefined); const mockProcess = createMockProcess(); vi.mocked(spawn).mockReturnValue(mockProcess as any); const { getDevServerService } = await import('@/services/dev-server-service.js'); const service = getDevServerService(); await service.startDevServer(testDir, testDir); const info = service.getServerInfo(testDir); expect(info).toBeDefined(); expect(info?.worktreePath).toBe(testDir); expect(info?.port).toBeGreaterThanOrEqual(3001); }); }); describe('getAllocatedPorts', () => { it('should return allocated ports', async () => { vi.mocked(secureFs.access).mockResolvedValue(undefined); const mockProcess = createMockProcess(); vi.mocked(spawn).mockReturnValue(mockProcess as any); const { getDevServerService } = await import('@/services/dev-server-service.js'); const service = getDevServerService(); await service.startDevServer(testDir, testDir); const ports = service.getAllocatedPorts(); expect(ports.length).toBeGreaterThanOrEqual(1); expect(ports[0]).toBeGreaterThanOrEqual(3001); }); }); describe('stopAll', () => { it('should stop all running servers', async () => { vi.mocked(secureFs.access).mockResolvedValue(undefined); const mockProcess = createMockProcess(); vi.mocked(spawn).mockReturnValue(mockProcess as any); const { getDevServerService } = await import('@/services/dev-server-service.js'); const service = getDevServerService(); await service.startDevServer(testDir, testDir); await service.stopAll(); expect(service.listDevServers().result.servers).toHaveLength(0); }); }); }); // Helper to create a mock child process function createMockProcess() { const mockProcess = new EventEmitter() as any; mockProcess.stdout = new EventEmitter(); mockProcess.stderr = new EventEmitter(); mockProcess.kill = vi.fn(); mockProcess.killed = false; // Don't exit immediately - let the test control the lifecycle return mockProcess; }