Files
fusion/packages/desktop/src/__tests__/local-runtime.test.ts
Fusion ff6a568f0c feat(FN-3404): add local embedded runtime manager to desktop app
Adds a local runtime manager to the desktop app (FN-3404) — an embedded engine process managed via IPC with packaging for bundled dependencies and lifecycle documentation. Also includes project mapping onboarding UI with persistence and rollback (FN-3505), plus related dashboard API and hook updates

Fusion-Task-Id: FN-3404
2026-05-07 23:21:04 -07:00

158 lines
4.8 KiB
TypeScript

import { describe, it, expect, vi, beforeEach } from "vitest";
import type { Server } from "node:http";
class FakeServer {
private listeners = new Map<string, Array<(...args: unknown[]) => 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);
});
});