diff --git a/packages/desktop/src/main/sidecar/controller.test.ts b/packages/desktop/src/main/sidecar/controller.test.ts new file mode 100644 index 000000000000..24d831848b47 --- /dev/null +++ b/packages/desktop/src/main/sidecar/controller.test.ts @@ -0,0 +1,71 @@ +import { describe, expect, test, vi } from "bun:test" +import { BaseSidecarController, createLocalSidecarController, createWslSidecarController } from "./types" + +describe("SidecarController Interface", () => { + test("manages sidecar lifecycle state transitions", async () => { + const fetchUrl = vi.fn().mockResolvedValue("http://127.0.0.1:4096") + const stopFn = vi.fn().mockResolvedValue(undefined) + + const controller = new BaseSidecarController("local-1", "local", fetchUrl, stopFn) + + expect(controller.status).toBe("idle") + + const url = await controller.getUrl() + expect(url).toBe("http://127.0.0.1:4096") + expect(controller.status).toBe("ready") + + await controller.stop() + expect(stopFn).toHaveBeenCalled() + expect(controller.status).toBe("stopped") + }) + + test("handles startup failures gracefully", async () => { + const fetchUrl = vi.fn().mockRejectedValue(new Error("Failed to start server")) + const stopFn = vi.fn().mockResolvedValue(undefined) + + const controller = new BaseSidecarController("wsl-ubuntu", "wsl", fetchUrl, stopFn) + + expect(controller.getUrl()).rejects.toThrow("Failed to start server") + expect(controller.status).toBe("failed") + }) + + test("createLocalSidecarController wraps local server startup and stop", async () => { + const stopMock = vi.fn().mockResolvedValue(undefined) + const startMock = vi.fn().mockResolvedValue({ + url: "http://127.0.0.1:4096", + listener: { stop: stopMock }, + }) + + const sidecar = createLocalSidecarController("local-main", startMock) + expect(sidecar.kind).toBe("local") + expect(sidecar.status).toBe("idle") + + const url = await sidecar.getUrl() + expect(url).toBe("http://127.0.0.1:4096") + expect(sidecar.status).toBe("ready") + + await sidecar.stop() + expect(stopMock).toHaveBeenCalled() + expect(sidecar.status).toBe("stopped") + }) + + test("createWslSidecarController wraps WSL server startup and stop", async () => { + const stopMock = vi.fn() + const startMock = vi.fn().mockResolvedValue({ + url: "http://127.0.0.1:8080", + listener: { stop: stopMock }, + }) + + const sidecar = createWslSidecarController("Ubuntu", startMock) + expect(sidecar.kind).toBe("wsl") + expect(sidecar.status).toBe("idle") + + const url = await sidecar.getUrl() + expect(url).toBe("http://127.0.0.1:8080") + expect(sidecar.status).toBe("ready") + + await sidecar.stop() + expect(stopMock).toHaveBeenCalled() + expect(sidecar.status).toBe("stopped") + }) +}) diff --git a/packages/desktop/src/main/sidecar/types.ts b/packages/desktop/src/main/sidecar/types.ts new file mode 100644 index 000000000000..16a85b314ea6 --- /dev/null +++ b/packages/desktop/src/main/sidecar/types.ts @@ -0,0 +1,83 @@ +export type SidecarKind = "local" | "wsl" +export type SidecarStatus = "idle" | "starting" | "ready" | "failed" | "stopped" + +export interface SidecarInstance { + readonly id: string + readonly kind: SidecarKind + readonly status: SidecarStatus + getUrl(): Promise + stop(): Promise +} + +export class BaseSidecarController implements SidecarInstance { + private _status: SidecarStatus = "idle" + + constructor( + public readonly id: string, + public readonly kind: SidecarKind, + private fetchUrl: () => Promise, + private stopFn: () => Promise, + ) {} + + get status(): SidecarStatus { + return this._status + } + + async getUrl(): Promise { + this._status = "starting" + try { + const url = await this.fetchUrl() + this._status = url ? "ready" : "failed" + return url + } catch (err) { + this._status = "failed" + throw err + } + } + + async stop(): Promise { + try { + await this.stopFn() + } finally { + this._status = "stopped" + } + } +} + +export function createLocalSidecarController( + id: string, + start: () => Promise<{ url: string; listener: { stop: () => Promise } }>, +): BaseSidecarController { + let stopFn = async () => {} + return new BaseSidecarController( + id, + "local", + async () => { + const res = await start() + stopFn = () => res.listener.stop() + return res.url + }, + async () => { + await stopFn() + }, + ) +} + +export function createWslSidecarController( + distro: string, + start: () => Promise<{ url: string; listener: { stop: () => void } }>, +): BaseSidecarController { + let stopFn = async () => {} + return new BaseSidecarController( + distro, + "wsl", + async () => { + const res = await start() + stopFn = async () => res.listener.stop() + return res.url + }, + async () => { + await stopFn() + }, + ) +}