mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-01-30 14:22:02 +00:00
384 lines
13 KiB
TypeScript
384 lines
13 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;
|
|
|
|
beforeEach(async () => {
|
|
vi.clearAllMocks();
|
|
vi.resetModules();
|
|
|
|
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 () => {
|
|
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;
|
|
}
|