Files
automaker/apps/server/tests/unit/services/dev-server-service.test.ts
DhanushSantosh 84570842d3 fix: resolve three critical bugs from GitHub issue tracker
Fix #684: Prevent Windows reserved filename creation
- Add sanitizeFilename() utility to detect and prefix Windows reserved names
  (NUL, CON, PRN, AUX, COM1-9, LPT1-9)
- Apply sanitization to save-image route to prevent "nul" file creation
- Add 23 comprehensive tests for filename sanitization edge cases

Fix #576: Detect actual dev server port from output
- Parse stdout/stderr for real server URLs (Vite, Next.js, generic formats)
- Update server URL when detected instead of using allocated PORT
- Emit dev-server:url-detected event for frontend updates
- Add 6 tests for URL detection patterns

Fix #193: Commit only feature-specific changes
- Change from 'git add -A' to branch-aware file staging
- Use git diff to find files changed on feature branch only
- Prevent committing unrelated changes from other features
- Maintain backward compatibility with main branch workflow

All fixes include comprehensive tests and maintain backward compatibility.
Test results: 1,968 tests passed (547 package + 1,421 server tests)
2026-02-05 10:42:56 +05:30

538 lines
19 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 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/');
});
});
});
// Helper to create a mock child process
function createMockProcess() {
const mockProcess = new EventEmitter() as any;
mockProcess.stdout = new EventEmitter();
mockProcess.stderr = new EventEmitter();
mockProcess.kill = vi.fn();
mockProcess.killed = false;
// Don't exit immediately - let the test control the lifecycle
return mockProcess;
}