import { MemoryRouter, createMemoryHistory } from "@solidjs/router" import { createMemo, Show } from "solid-js" import { createStore, unwrap } from "solid-js/store" import { render } from "solid-js/web" import type { Bridge, BridgeMessage } from "@opencode/plugin/gui/host" import type { SshItem, SshState } from "../../../gui-plugins/src/ssh/contract" import type { WslServerRuntime, WslServersState } from "../../../gui-plugins/src/wsl/contract" import { AppBaseProviders, AppInterface } from "../../src/app" import { PlatformProvider, type Platform } from "../../src/runtime/platform/platform" import { ServerConnection } from "../../src/runtime/server/registry" import { usePluginServers } from "../../src/runtime/plugin/servers" /** The input of a WSL or SSH method this fixture answers. */ type FixtureInput = { id?: string; name?: string } /** A main-side method: SSH `start` answers with the state revision, the others with nothing. */ type FixtureMethod = (input: FixtureInput) => number | void // A desktop window. `wsl` is the Ubuntu server's endpoint (default `server`); updating OpenCode restarts it on `restart` // (default `wsl`). `ssh` adds a saved SSH server `box`, ready on that endpoint with password `ssh-1`; each connect // brings up a new remote server with the next password (`ssh-2`, ...). `storage=async` stores in localStorage behind // asynchronous reads like the desktop app's; reads of keys containing `hold` wait for "Load held storage". export function mount(input: { server: string mode: "failed" | "stopped" | "ready" wsl?: string | null restart?: string | null path?: string | null ssh?: string | null storage?: string | null hold?: string | null /** Enables the updater Ipc; check and install reject with this message, which may be empty. */ updater?: string | null }) { const root = document.getElementById("root") if (!root) throw new Error("Missing fixture root") const history = createMemoryHistory() history.set({ value: input.path ?? "/settings", replace: true, scroll: false }) const endpoint = { url: input.wsl ?? input.server } const ready = () => ({ kind: "ready" as const, url: endpoint.url, password: null }) const held = Promise.withResolvers() const storage = (name?: string) => { const item = (key: string) => (name ? `${name}:${key}` : key) return { getItem: async (key: string) => { if (input.hold && key.includes(input.hold)) await held.promise return localStorage.getItem(item(key)) }, setItem: async (key: string, value: string) => localStorage.setItem(item(key), value), removeItem: async (key: string) => localStorage.removeItem(item(key)), } } render(() => { const [store, setStore] = createStore<{ calls: string[] connects: number available: boolean state: WslServersState ssh: SshState }>({ calls: [], connects: 0, available: true, ssh: { revision: 0, servers: input.ssh ? [ { config: { id: "box", target: "box", name: "box" }, saved: true, stage: "ready", http: { url: input.ssh, password: "ssh-1" }, detail: "", }, ] : [], }, state: { runtime: { available: true, version: "2", error: null }, installed: [], online: [], distroProbes: {}, pendingRestart: false, job: null, servers: [ { config: { id: "wsl:Ubuntu", distro: "Ubuntu" }, runtime: ( { ready: ready(), failed: { kind: "failed", message: "WSL failed to start" }, stopped: { kind: "stopped" }, } satisfies Record )[input.mode], }, ], opencodeChecks: { Ubuntu: { distro: "Ubuntu", resolvedPath: "/usr/bin/opencode", version: "old", expectedVersion: "current", matchesDesktop: false, error: null, }, }, }, }) // The main-process WSL and SSH plugins, as the plugin bridge sees them. const listeners = new Set<(message: BridgeMessage) => void>() const snapshot = (ipc: string) => structuredClone(unwrap(ipc === "ssh" ? store.ssh : store.state)) const publish = (ipc: string) => listeners.forEach((listener) => listener({ type: "state", ipc, state: snapshot(ipc) })) // The contract state is deeply readonly, so each action replaces the changed branch. const setRuntime = (id: string | undefined, runtime: WslServerRuntime) => setStore("state", (state) => ({ servers: state.servers.map((server) => (server.config.id === id ? { ...server, runtime } : server)), })) const setSsh = (item: Partial) => setStore("ssh", (state) => ({ revision: state.revision + 1, servers: state.servers.map((server) => ({ ...server, ...item })), })) const wsl = { // Like main: stops the distro's server, updates OpenCode, then starts the server again on a new endpoint. installOpencode(value) { const name = value.name ?? "" const id = store.state.servers.find((server) => server.config.distro === name)?.config.id setStore("calls", (calls) => [...calls, `update:${value.name}`]) setRuntime(id, { kind: "stopped" }) publish("wsl") setStore("state", (state) => ({ opencodeChecks: { ...state.opencodeChecks, [name]: { ...state.opencodeChecks[name]!, version: "current", matchesDesktop: true }, }, })) endpoint.url = input.restart ?? endpoint.url setRuntime(id, ready()) }, startServer(value) { setStore("calls", (calls) => [...calls, `start:${value.id}`]) setRuntime(value.id, ready()) }, removeServer(value) { setStore("calls", (calls) => [...calls, `remove:${value.id}`]) setStore("state", (state) => ({ servers: state.servers.filter((server) => server.config.id !== value.id) })) }, } satisfies Readonly> const ssh = { // Like main: a connect opens a new tunnel to a remote server with a new password. start() { setStore("connects", (count) => count + 1) setSsh({ stage: "ready", http: { url: input.ssh ?? "", password: `ssh-${store.connects + 1}` } }) return store.ssh.revision }, } satisfies Readonly> const methods = new Map>>([ ["wsl", wsl], ["ssh", ssh], ]) const bridge: Bridge = { packaged: false, async call(request) { if (request.ipc === "updater" && input.updater != null) throw new Error(input.updater) const method = methods.get(request.ipc)?.[request.method] if (!method) throw new Error("Unexpected fixture action") // SAFETY: every WSL and SSH method the fixture answers takes a struct of these optional string fields. const result = method(request.input as FixtureInput) publish(request.ipc) return result ?? null }, async subscribe(ipc) { if (ipc === "updater" && input.updater != null) return { available: true, state: { status: "idle" } } if (ipc === "wsl" || ipc === "ssh") return { available: true, state: snapshot(ipc) } return { available: false } }, on(listener) { listeners.add(listener) return () => listeners.delete(listener) }, embed: () => undefined, capture: async () => undefined, runMenubarItem: () => undefined, configure: () => undefined, pluginGuis: async () => ({ plugins: [], reserved: [] }), manager: { list: async () => [], enable: async () => undefined, disable: async () => undefined, reload: async () => undefined, }, } const unused = async () => { throw new Error("Unexpected fixture action") } const platform: Platform = { platform: "desktop", os: "windows", windowID: "settings-wsl-test", openExternal: () => undefined, openDirectoryPickerDialog: async () => null, notify: async () => undefined, restart: unused, plugins: bridge, } if (input.storage === "async") platform.storage = storage function Interface() { const pluginServers = usePluginServers() const servers = createMemo(() => [ { type: "sidecar", variant: "base", displayName: "Local Server", http: { url: input.server } }, ...pluginServers.list(), ]) return ( } /> ) } return ( {store.calls.join(",")} {/* The WSL plugin's main side going away, as on a reload or failure, and coming back. */} {/* The SSH plugin's main side going away and coming back. */} {/* The SSH tunnel dropping, which only main notices. */} {/* A background reconnect that needs a password, as main reports it to every window. */} {store.connects} ) }, root) }