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(), })); // Mock fs existsSync vi.mock("fs", async (importOriginal) => { const actual = await importOriginal(); return { ...actual, existsSync: vi.fn(), }; }); // Mock net vi.mock("net", () => ({ default: { createServer: vi.fn(), }, createServer: vi.fn(), })); import { spawn, execSync } from "child_process"; import { existsSync } from "fs"; 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 existsSync - return true vi.mocked(existsSync).mockReturnValue(true); // 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(existsSync).mockReturnValue(false); 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(existsSync).mockImplementation((p: any) => { if (p.includes("package.json")) return false; return true; }); 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(existsSync).mockImplementation((p: any) => { if (p.includes("bun.lockb")) return false; if (p.includes("pnpm-lock.yaml")) return false; if (p.includes("yarn.lock")) return false; if (p.includes("package-lock.json")) return true; if (p.includes("package.json")) return true; return true; }); 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(existsSync).mockImplementation((p: any) => { if (p.includes("bun.lockb")) return false; if (p.includes("pnpm-lock.yaml")) return false; if (p.includes("yarn.lock")) return true; if (p.includes("package.json")) return true; return true; }); 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(existsSync).mockImplementation((p: any) => { if (p.includes("bun.lockb")) return false; if (p.includes("pnpm-lock.yaml")) return true; if (p.includes("package.json")) return true; return true; }); 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(existsSync).mockImplementation((p: any) => { if (p.includes("bun.lockb")) return true; if (p.includes("package.json")) return true; return true; }); 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(existsSync).mockReturnValue(true); 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(existsSync).mockReturnValue(true); 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(existsSync).mockReturnValue(true); 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(existsSync).mockReturnValue(true); 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(existsSync).mockReturnValue(true); 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(existsSync).mockReturnValue(true); 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(existsSync).mockReturnValue(true); 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(existsSync).mockReturnValue(true); 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; }