mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-03-17 10:03:08 +00:00
* 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>
901 lines
32 KiB
TypeScript
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;
|
|
}
|