Spaces:
Paused
Paused
| 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(), | |
| execFile: 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; | |
| let originalHostname: string | undefined; | |
| beforeEach(async () => { | |
| vi.clearAllMocks(); | |
| vi.resetModules(); | |
| // Store and set HOSTNAME for consistent test behavior | |
| originalHostname = process.env.HOSTNAME; | |
| process.env.HOSTNAME = 'localhost'; | |
| 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 () => { | |
| // Restore original HOSTNAME | |
| if (originalHostname === undefined) { | |
| delete process.env.HOSTNAME; | |
| } else { | |
| process.env.HOSTNAME = originalHostname; | |
| } | |
| 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); | |
| }); | |
| }); | |
| describe('URL detection from output', () => { | |
| it('should detect Vite format URL', 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); | |
| // Simulate Vite output | |
| mockProcess.stdout.emit('data', Buffer.from(' VITE v5.0.0 ready in 123 ms\n')); | |
| mockProcess.stdout.emit('data', Buffer.from(' ➜ Local: http://localhost:5173/\n')); | |
| // Give it a moment to process | |
| await new Promise((resolve) => setTimeout(resolve, 50)); | |
| const serverInfo = service.getServerInfo(testDir); | |
| expect(serverInfo?.url).toBe('http://localhost:5173/'); | |
| expect(serverInfo?.urlDetected).toBe(true); | |
| }); | |
| it('should detect Next.js format URL', 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); | |
| // Simulate Next.js output | |
| mockProcess.stdout.emit( | |
| 'data', | |
| Buffer.from('ready - started server on 0.0.0.0:3000, url: http://localhost:3000\n') | |
| ); | |
| await new Promise((resolve) => setTimeout(resolve, 50)); | |
| const serverInfo = service.getServerInfo(testDir); | |
| expect(serverInfo?.url).toBe('http://localhost:3000'); | |
| expect(serverInfo?.urlDetected).toBe(true); | |
| }); | |
| it('should detect generic localhost URL', 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); | |
| // Simulate generic output with URL | |
| mockProcess.stdout.emit('data', Buffer.from('Server running at http://localhost:8080\n')); | |
| await new Promise((resolve) => setTimeout(resolve, 50)); | |
| const serverInfo = service.getServerInfo(testDir); | |
| expect(serverInfo?.url).toBe('http://localhost:8080'); | |
| expect(serverInfo?.urlDetected).toBe(true); | |
| }); | |
| it('should keep initial URL if no URL detected in output', 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); | |
| // Simulate output without URL | |
| mockProcess.stdout.emit('data', Buffer.from('Server starting...\n')); | |
| mockProcess.stdout.emit('data', Buffer.from('Ready!\n')); | |
| await new Promise((resolve) => setTimeout(resolve, 50)); | |
| const serverInfo = service.getServerInfo(testDir); | |
| // Should keep the initial allocated URL | |
| expect(serverInfo?.url).toBe(result.result?.url); | |
| expect(serverInfo?.urlDetected).toBe(false); | |
| }); | |
| it('should detect HTTPS URLs', 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); | |
| // Simulate HTTPS dev server | |
| mockProcess.stdout.emit('data', Buffer.from('Server listening at https://localhost:3443\n')); | |
| await new Promise((resolve) => setTimeout(resolve, 50)); | |
| const serverInfo = service.getServerInfo(testDir); | |
| expect(serverInfo?.url).toBe('https://localhost:3443'); | |
| expect(serverInfo?.urlDetected).toBe(true); | |
| }); | |
| it('should only detect URL once (not update after first detection)', 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); | |
| // First URL | |
| mockProcess.stdout.emit('data', Buffer.from('Local: http://localhost:5173/\n')); | |
| await new Promise((resolve) => setTimeout(resolve, 50)); | |
| const firstUrl = service.getServerInfo(testDir)?.url; | |
| // Try to emit another URL | |
| mockProcess.stdout.emit('data', Buffer.from('Network: http://192.168.1.1:5173/\n')); | |
| await new Promise((resolve) => setTimeout(resolve, 50)); | |
| // Should keep the first detected URL | |
| const serverInfo = service.getServerInfo(testDir); | |
| expect(serverInfo?.url).toBe(firstUrl); | |
| expect(serverInfo?.url).toBe('http://localhost:5173/'); | |
| }); | |
| it('should detect Astro format URL', 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); | |
| // Astro uses the same "Local:" prefix as Vite | |
| mockProcess.stdout.emit('data', Buffer.from(' 🚀 astro v4.0.0 started in 200ms\n')); | |
| mockProcess.stdout.emit('data', Buffer.from(' ┃ Local http://localhost:4321/\n')); | |
| await new Promise((resolve) => setTimeout(resolve, 50)); | |
| const serverInfo = service.getServerInfo(testDir); | |
| // Astro doesn't use "Local:" with colon, so it should be caught by the localhost URL pattern | |
| expect(serverInfo?.url).toBe('http://localhost:4321/'); | |
| expect(serverInfo?.urlDetected).toBe(true); | |
| }); | |
| it('should detect Remix format URL', 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); | |
| mockProcess.stdout.emit( | |
| 'data', | |
| Buffer.from('Remix App Server started at http://localhost:3000\n') | |
| ); | |
| await new Promise((resolve) => setTimeout(resolve, 50)); | |
| const serverInfo = service.getServerInfo(testDir); | |
| expect(serverInfo?.url).toBe('http://localhost:3000'); | |
| expect(serverInfo?.urlDetected).toBe(true); | |
| }); | |
| it('should detect Django format URL', 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); | |
| mockProcess.stdout.emit( | |
| 'data', | |
| Buffer.from('Starting development server at http://127.0.0.1:8000/\n') | |
| ); | |
| await new Promise((resolve) => setTimeout(resolve, 50)); | |
| const serverInfo = service.getServerInfo(testDir); | |
| expect(serverInfo?.url).toBe('http://127.0.0.1:8000/'); | |
| expect(serverInfo?.urlDetected).toBe(true); | |
| }); | |
| it('should detect Webpack Dev Server format URL', 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); | |
| mockProcess.stdout.emit( | |
| 'data', | |
| Buffer.from('<i> [webpack-dev-server] Project is running at http://localhost:8080/\n') | |
| ); | |
| await new Promise((resolve) => setTimeout(resolve, 50)); | |
| const serverInfo = service.getServerInfo(testDir); | |
| expect(serverInfo?.url).toBe('http://localhost:8080/'); | |
| expect(serverInfo?.urlDetected).toBe(true); | |
| }); | |
| it('should detect PHP built-in server format URL', 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); | |
| mockProcess.stdout.emit( | |
| 'data', | |
| Buffer.from('Development Server (http://localhost:8000) started\n') | |
| ); | |
| await new Promise((resolve) => setTimeout(resolve, 50)); | |
| const serverInfo = service.getServerInfo(testDir); | |
| expect(serverInfo?.url).toBe('http://localhost:8000'); | |
| expect(serverInfo?.urlDetected).toBe(true); | |
| }); | |
| it('should detect "listening on port" format (port-only detection)', 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); | |
| // Some servers only print the port number, not a full URL | |
| mockProcess.stdout.emit('data', Buffer.from('Server listening on port 4000\n')); | |
| await new Promise((resolve) => setTimeout(resolve, 50)); | |
| const serverInfo = service.getServerInfo(testDir); | |
| expect(serverInfo?.url).toBe('http://localhost:4000'); | |
| expect(serverInfo?.urlDetected).toBe(true); | |
| }); | |
| it('should detect "running on port" format (port-only detection)', 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); | |
| mockProcess.stdout.emit('data', Buffer.from('Application running on port 9000\n')); | |
| await new Promise((resolve) => setTimeout(resolve, 50)); | |
| const serverInfo = service.getServerInfo(testDir); | |
| expect(serverInfo?.url).toBe('http://localhost:9000'); | |
| expect(serverInfo?.urlDetected).toBe(true); | |
| }); | |
| it('should strip ANSI escape codes before detecting URL', 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); | |
| // Simulate Vite output with ANSI color codes | |
| mockProcess.stdout.emit( | |
| 'data', | |
| Buffer.from( | |
| ' \x1B[32m➜\x1B[0m \x1B[1mLocal:\x1B[0m \x1B[36mhttp://localhost:5173/\x1B[0m\n' | |
| ) | |
| ); | |
| await new Promise((resolve) => setTimeout(resolve, 50)); | |
| const serverInfo = service.getServerInfo(testDir); | |
| expect(serverInfo?.url).toBe('http://localhost:5173/'); | |
| expect(serverInfo?.urlDetected).toBe(true); | |
| }); | |
| it('should normalize 0.0.0.0 to localhost', 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); | |
| mockProcess.stdout.emit('data', Buffer.from('Server listening at http://0.0.0.0:3000\n')); | |
| await new Promise((resolve) => setTimeout(resolve, 50)); | |
| const serverInfo = service.getServerInfo(testDir); | |
| expect(serverInfo?.url).toBe('http://localhost:3000'); | |
| expect(serverInfo?.urlDetected).toBe(true); | |
| }); | |
| it('should normalize [::] to localhost', 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); | |
| mockProcess.stdout.emit('data', Buffer.from('Local: http://[::]:4000/\n')); | |
| await new Promise((resolve) => setTimeout(resolve, 50)); | |
| const serverInfo = service.getServerInfo(testDir); | |
| expect(serverInfo?.url).toBe('http://localhost:4000/'); | |
| expect(serverInfo?.urlDetected).toBe(true); | |
| }); | |
| it('should update port field when detected URL has different port', 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); | |
| const allocatedPort = result.result?.port; | |
| // Server starts on a completely different port (ignoring PORT env var) | |
| mockProcess.stdout.emit('data', Buffer.from('Local: http://localhost:9999/\n')); | |
| await new Promise((resolve) => setTimeout(resolve, 50)); | |
| const serverInfo = service.getServerInfo(testDir); | |
| expect(serverInfo?.url).toBe('http://localhost:9999/'); | |
| expect(serverInfo?.port).toBe(9999); | |
| // The port should be different from what was initially allocated | |
| if (allocatedPort !== 9999) { | |
| expect(serverInfo?.port).not.toBe(allocatedPort); | |
| } | |
| }); | |
| it('should detect URL from stderr output', 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); | |
| // Some servers output URL info to stderr | |
| mockProcess.stderr.emit('data', Buffer.from('Local: http://localhost:3000/\n')); | |
| await new Promise((resolve) => setTimeout(resolve, 50)); | |
| const serverInfo = service.getServerInfo(testDir); | |
| expect(serverInfo?.url).toBe('http://localhost:3000/'); | |
| expect(serverInfo?.urlDetected).toBe(true); | |
| }); | |
| it('should not match URLs without a port (non-dev-server URLs)', 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); | |
| // CDN/external URLs should not be detected | |
| mockProcess.stdout.emit( | |
| 'data', | |
| Buffer.from('Downloading from https://cdn.example.com/bundle.js\n') | |
| ); | |
| mockProcess.stdout.emit('data', Buffer.from('Fetching https://registry.npmjs.org/package\n')); | |
| await new Promise((resolve) => setTimeout(resolve, 50)); | |
| const serverInfo = service.getServerInfo(testDir); | |
| // Should keep the initial allocated URL since external URLs don't match | |
| expect(serverInfo?.url).toBe(result.result?.url); | |
| expect(serverInfo?.urlDetected).toBe(false); | |
| }); | |
| it('should handle URLs with trailing punctuation', 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); | |
| // URL followed by punctuation | |
| mockProcess.stdout.emit('data', Buffer.from('Server started at http://localhost:3000.\n')); | |
| await new Promise((resolve) => setTimeout(resolve, 50)); | |
| const serverInfo = service.getServerInfo(testDir); | |
| expect(serverInfo?.url).toBe('http://localhost:3000'); | |
| expect(serverInfo?.urlDetected).toBe(true); | |
| }); | |
| it('should detect Express/Fastify format URL', 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); | |
| mockProcess.stdout.emit('data', Buffer.from('Server listening on http://localhost:3000\n')); | |
| await new Promise((resolve) => setTimeout(resolve, 50)); | |
| const serverInfo = service.getServerInfo(testDir); | |
| expect(serverInfo?.url).toBe('http://localhost:3000'); | |
| expect(serverInfo?.urlDetected).toBe(true); | |
| }); | |
| it('should detect Angular CLI format URL', 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); | |
| // Angular CLI output | |
| mockProcess.stderr.emit( | |
| 'data', | |
| Buffer.from( | |
| '** Angular Live Development Server is listening on localhost:4200, open your browser on http://localhost:4200/ **\n' | |
| ) | |
| ); | |
| await new Promise((resolve) => setTimeout(resolve, 50)); | |
| const serverInfo = service.getServerInfo(testDir); | |
| expect(serverInfo?.url).toBe('http://localhost:4200/'); | |
| expect(serverInfo?.urlDetected).toBe(true); | |
| }); | |
| }); | |
| }); | |
| // 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; | |
| mockProcess.pid = 12345; | |
| // Don't exit immediately - let the test control the lifecycle | |
| return mockProcess; | |
| } | |