Files
automaker/apps/server/tests/unit/services/dev-server-service.test.ts
gsxdsm 0a5540c9a2 Fix concurrency limits and remote branch fetching issues (#788)
* Changes from fix/bug-fixes

* feat: Refactor worktree iteration and improve error logging across services

* feat: Extract URL/port patterns to module level and fix abort condition

* fix: Improve IPv6 loopback handling, select component layout, and terminal UI

* feat: Add thinking level defaults and adjust list row padding

* Update apps/ui/src/store/app-store.ts

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* feat: Add worktree-aware terminal creation and split options, fix npm security issues from audit

* feat: Add tracked remote detection to pull dialog flow

* feat: Add merge state tracking to git operations

* feat: Improve merge detection and add post-merge action preferences

* Update apps/ui/src/components/views/board-view/dialogs/git-pull-dialog.tsx

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* Update apps/ui/src/components/views/board-view/dialogs/git-pull-dialog.tsx

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* fix: Pass merge detection info to stash reapplication and handle merge state consistently

* fix: Call onPulled callback in merge handlers and add validation checks

---------

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2026-02-20 13:48:22 -08:00

901 lines
32 KiB
TypeScript

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;
}