Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 71 additions & 0 deletions packages/desktop/src/main/sidecar/controller.test.ts
Original file line number Diff line number Diff line change
@@ -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")
})
})
83 changes: 83 additions & 0 deletions packages/desktop/src/main/sidecar/types.ts
Original file line number Diff line number Diff line change
@@ -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<string | null>
stop(): Promise<void>
}

export class BaseSidecarController implements SidecarInstance {
private _status: SidecarStatus = "idle"

constructor(
public readonly id: string,
public readonly kind: SidecarKind,
private fetchUrl: () => Promise<string | null>,
private stopFn: () => Promise<void>,
) {}

get status(): SidecarStatus {
return this._status
}

async getUrl(): Promise<string | null> {
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<void> {
try {
await this.stopFn()
} finally {
this._status = "stopped"
}
}
}

export function createLocalSidecarController(
id: string,
start: () => Promise<{ url: string; listener: { stop: () => Promise<void> } }>,
): 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()
},
)
}
Loading