mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-01-30 06:12:03 +00:00
Dev server test was failing on non-localhost hostnames (e.g., 'fedora') because it expected 'localhost' in the URL. Now sets HOSTNAME env var in test setup and restores it in teardown for consistent test behavior across all environments. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
396 lines
14 KiB
TypeScript
396 lines
14 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);
|
|
});
|
|
});
|
|
});
|
|
|
|
// 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;
|
|
}
|