Files
opencode/packages/app/e2e/utils/settings-wsl.fixture.tsx

313 lines
11 KiB
TypeScript

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<void>()
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<typeof input.mode, WslServerRuntime>
)[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<SshItem>) =>
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<Record<string, FixtureMethod>>
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<Record<string, FixtureMethod>>
const methods = new Map<string, Readonly<Record<string, FixtureMethod>>>([
["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<ServerConnection.Any[]>(() => [
{ type: "sidecar", variant: "base", displayName: "Local Server", http: { url: input.server } },
...pluginServers.list(),
])
return (
<Show when={pluginServers.ready()}>
<AppInterface servers={servers()} router={(props) => <MemoryRouter {...props} history={history} />} />
</Show>
)
}
return (
<PlatformProvider value={platform}>
<AppBaseProviders locale="en">
<output aria-label="WSL actions">{store.calls.join(",")}</output>
{/* The WSL plugin's main side going away, as on a reload or failure, and coming back. */}
<label>
<input
type="checkbox"
checked={store.available}
onChange={(event) => {
const available = event.currentTarget.checked
setStore("available", available)
listeners.forEach((listener) => listener({ type: "available", ipc: "wsl", available }))
if (available) publish("wsl")
}}
/>
WSL plugin
</label>
{/* The SSH plugin's main side going away and coming back. */}
<label>
<input
type="checkbox"
checked
onChange={(event) => {
const available = event.currentTarget.checked
listeners.forEach((listener) => listener({ type: "available", ipc: "ssh", available }))
if (available) publish("ssh")
}}
/>
SSH plugin
</label>
{/* The SSH tunnel dropping, which only main notices. */}
<Show when={input.ssh}>
<button
type="button"
onClick={() => {
setSsh({ stage: "disconnected" })
publish("ssh")
}}
>
Drop SSH tunnel
</button>
{/* A background reconnect that needs a password, as main reports it to every window. */}
<button
type="button"
onClick={() => {
setSsh({ stage: "authentication" })
publish("ssh")
}}
>
Require SSH sign-in
</button>
<output aria-label="SSH connects">{store.connects}</output>
</Show>
<Show when={input.hold}>
<button type="button" onClick={() => held.resolve()}>
Load held storage
</button>
</Show>
<Interface />
</AppBaseProviders>
</PlatformProvider>
)
}, root)
}