The merge lands Step 7 of FN-3398, adding documentation and delivery artifacts across the mobile and desktop packages with updated READMEs, mobile-specific docs, and architecture references. Fusion-Task-Id: FN-3398
112 lines
3.4 KiB
TypeScript
112 lines
3.4 KiB
TypeScript
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
|
|
const mocks = vi.hoisted(() => {
|
|
type Handler = (...args: unknown[]) => void;
|
|
class SimpleEmitter {
|
|
private listeners = new Map<string, Handler[]>();
|
|
on(event: string, handler: Handler) {
|
|
const current = this.listeners.get(event) ?? [];
|
|
current.push(handler);
|
|
this.listeners.set(event, current);
|
|
return this;
|
|
}
|
|
once(event: string, handler: Handler) {
|
|
const wrapped: Handler = (...args) => {
|
|
this.removeListener(event, wrapped);
|
|
handler(...args);
|
|
};
|
|
return this.on(event, wrapped);
|
|
}
|
|
removeListener(event: string, handler: Handler) {
|
|
const current = this.listeners.get(event) ?? [];
|
|
this.listeners.set(event, current.filter((item) => item !== handler));
|
|
return this;
|
|
}
|
|
emit(event: string, ...args: unknown[]) {
|
|
const current = this.listeners.get(event) ?? [];
|
|
for (const handler of current) {
|
|
handler(...args);
|
|
}
|
|
}
|
|
}
|
|
|
|
const store = {
|
|
init: vi.fn(async () => undefined),
|
|
watch: vi.fn(async () => undefined),
|
|
close: vi.fn(),
|
|
};
|
|
|
|
class TaskStore {
|
|
constructor(_rootDir: string) {}
|
|
init = store.init;
|
|
watch = store.watch;
|
|
close = store.close;
|
|
}
|
|
|
|
const server = Object.assign(new SimpleEmitter(), {
|
|
address: vi.fn(() => ({ port: 4545 })),
|
|
close: vi.fn((cb: () => void) => cb()),
|
|
});
|
|
|
|
const listen = vi.fn(() => {
|
|
queueMicrotask(() => server.emit("listening"));
|
|
return server;
|
|
});
|
|
|
|
const createServer = vi.fn(() => ({ listen }));
|
|
|
|
return { TaskStore, createServer, store, listen };
|
|
});
|
|
|
|
vi.mock("@fusion/core", () => ({ TaskStore: mocks.TaskStore }));
|
|
vi.mock("@fusion/dashboard", () => ({ createServer: mocks.createServer }));
|
|
|
|
describe("DesktopLocalServerManager", () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
});
|
|
|
|
it("starts local runtime and exposes port", async () => {
|
|
const { DesktopLocalServerManager } = await import("../local-server.ts");
|
|
const manager = new DesktopLocalServerManager("/repo");
|
|
|
|
const runtime = await manager.start();
|
|
|
|
expect(runtime.port).toBe(4545);
|
|
expect(manager.getPort()).toBe(4545);
|
|
expect(manager.getState().status).toBe("ready");
|
|
});
|
|
|
|
it("stops local runtime and resets state", async () => {
|
|
const { DesktopLocalServerManager } = await import("../local-server.ts");
|
|
const manager = new DesktopLocalServerManager("/repo");
|
|
await manager.start();
|
|
|
|
await manager.stop();
|
|
|
|
expect(mocks.store.close).toHaveBeenCalled();
|
|
expect(manager.getState().status).toBe("idle");
|
|
expect(manager.getPort()).toBeUndefined();
|
|
});
|
|
|
|
it("sets error state when startup fails", async () => {
|
|
mocks.store.init.mockRejectedValueOnce(new Error("init failed"));
|
|
const { DesktopLocalServerManager } = await import("../local-server.ts");
|
|
const manager = new DesktopLocalServerManager("/repo");
|
|
|
|
await expect(manager.start()).rejects.toThrow("init failed");
|
|
expect(manager.getState()).toMatchObject({ status: "error", error: "init failed" });
|
|
});
|
|
|
|
it("returns existing runtime when start is called twice", async () => {
|
|
const { DesktopLocalServerManager } = await import("../local-server.ts");
|
|
const manager = new DesktopLocalServerManager("/repo");
|
|
|
|
const first = await manager.start();
|
|
const second = await manager.start();
|
|
|
|
expect(first).toBe(second);
|
|
expect(mocks.listen).toHaveBeenCalledTimes(1);
|
|
});
|
|
});
|