import { describe, it, expect, vi, beforeEach } from "vitest"; import type { Server } from "node:http"; class FakeServer { private listeners = new Map void>>(); constructor(private readonly port: number) {} on(event: string, handler: (...args: unknown[]) => void): this { const current = this.listeners.get(event) ?? []; current.push(handler); this.listeners.set(event, current); return this; } once(event: string, handler: (...args: unknown[]) => void): this { const wrapped = (...args: unknown[]) => { this.removeListener(event, wrapped); handler(...args); }; return this.on(event, wrapped); } removeListener(event: string, handler: (...args: unknown[]) => void): this { const current = this.listeners.get(event) ?? []; this.listeners.set(event, current.filter((item) => item !== handler)); return this; } emit(event: string, ...args: unknown[]): void { for (const handler of this.listeners.get(event) ?? []) { handler(...args); } } address() { return { port: this.port }; } close(callback: () => void): void { callback(); } } describe("LocalRuntimeManager", () => { const store = { init: vi.fn(async () => undefined), watch: vi.fn(async () => undefined), close: vi.fn(), }; beforeEach(() => { vi.clearAllMocks(); }); it("starts embedded local runtime and reports status", async () => { const { LocalRuntimeManager } = await import("../local-runtime.ts"); const server = new FakeServer(4545); const manager = new LocalRuntimeManager({ rootDir: "/repo", createStore: async () => store, createDashboardServer: async () => { setTimeout(() => server.emit("listening"), 0); return server as unknown as Server; }, }); const status = await manager.startLocal(); expect(store.init).toHaveBeenCalledTimes(1); expect(store.watch).toHaveBeenCalledTimes(1); expect(status).toMatchObject({ source: "embedded-local", state: "running", port: 4545, baseUrl: "http://127.0.0.1:4545", }); expect(manager.getServerPort()).toBe(4545); }); it("returns external-cli status without starting embedded runtime", async () => { const { LocalRuntimeManager } = await import("../local-runtime.ts"); const manager = new LocalRuntimeManager({ rootDir: "/repo", getExternalPort: () => 7777, createStore: async () => store, createDashboardServer: async () => new FakeServer(4545) as unknown as Server, }); const status = await manager.startLocal(); expect(status).toMatchObject({ source: "external-cli", state: "running", port: 7777, baseUrl: "http://127.0.0.1:7777", }); expect(store.init).not.toHaveBeenCalled(); }); it("rolls back and exposes error status when startup fails", async () => { const { LocalRuntimeManager } = await import("../local-runtime.ts"); store.init.mockRejectedValueOnce(new Error("init failed")); const manager = new LocalRuntimeManager({ rootDir: "/repo", createStore: async () => store, }); await expect(manager.startLocal()).rejects.toThrow("init failed"); expect(store.close).toHaveBeenCalledTimes(1); expect(manager.getStatus()).toMatchObject({ source: "embedded-local", state: "error", error: "init failed", }); }); it("stopLocal is idempotent and no-op when inactive", async () => { const { LocalRuntimeManager } = await import("../local-runtime.ts"); const server = new FakeServer(4545); const closeSpy = vi.spyOn(server, "close"); const manager = new LocalRuntimeManager({ rootDir: "/repo", createStore: async () => store, createDashboardServer: async () => { setTimeout(() => server.emit("listening"), 0); return server as unknown as Server; }, }); await manager.startLocal(); await manager.stopLocal(); await manager.stopLocal(); expect(closeSpy).toHaveBeenCalledTimes(1); expect(store.close).toHaveBeenCalledTimes(1); expect(manager.getStatus()).toEqual({ source: "none", state: "stopped" }); }); it("startLocal while already running returns current status", async () => { const { LocalRuntimeManager } = await import("../local-runtime.ts"); const server = new FakeServer(4545); const manager = new LocalRuntimeManager({ rootDir: "/repo", createStore: async () => store, createDashboardServer: async () => { setTimeout(() => server.emit("listening"), 0); return server as unknown as Server; }, }); const first = await manager.startLocal(); const second = await manager.startLocal(); expect(first).toEqual(second); expect(store.init).toHaveBeenCalledTimes(1); }); });