import { base64Encode } from "@opencode/util/encode" import { expect, test } from "@playwright/test" import { NO_PROVIDER, REMOTE_SERVER, SERVER, project, provider, session } from "../utils/app" import { mockOpenCodeServer, mockServers } from "../utils/mock-server" import { expectSessionTitle } from "../utils/waits" test.use({ viewport: { width: 1280, height: 900 } }) for (const mode of ["failed", "stopped", "ready"] as const) { test(`manages a ${mode} configured WSL server from the Servers page`, async ({ page }) => { await mockOpenCodeServer(page, { directory: "/repo", project: project({ id: "proj_wsl_settings", directory: "/repo", name: "WSL project" }), provider: NO_PROVIDER, sessions: [], pageMessages: () => ({ items: [] }), }) await page.goto(`/e2e/utils/settings-wsl.html?${new URLSearchParams({ server: SERVER, mode })}`) const settings = page.getByTestId("settings-screen") const switcher = settings.getByRole("button", { name: /^Server / }) await settings.getByRole("tab", { name: "Projects", exact: true }).click() await switcher.click() await page.getByRole("menuitemradio", { name: /^Ubuntu/ }).click() const unavailable = settings.getByRole("status").filter({ hasText: "Can't reach Ubuntu" }) if (mode !== "ready") await expect(unavailable).toBeVisible() await settings.getByRole("tab", { name: "Servers", exact: true }).click() const connection = settings.getByRole("region", { name: "Ubuntu", exact: true }) if (mode !== "ready") { await connection.getByRole("button", { name: "More options", exact: true }).click() await page.getByRole("menuitem", { name: "Retry start", exact: true }).click() await expect(page.getByLabel("WSL actions")).toHaveText("start:wsl:Ubuntu") } await settings.getByRole("tab", { name: "Projects", exact: true }).click() await expect(switcher).toHaveText("ServerUbuntu") await expect(unavailable).toHaveCount(0) await settings.getByRole("tab", { name: "Servers", exact: true }).click() await connection.getByRole("button", { name: "Update OpenCode", exact: true }).click() await expect(page.getByLabel("WSL actions")).toContainText("update:Ubuntu") await expect(connection.getByRole("button", { name: "Update OpenCode", exact: true })).toHaveCount(0) await connection.getByRole("button", { name: "More options", exact: true }).click() await page.getByRole("menuitem", { name: "Remove", exact: true }).click() await expect(page.getByLabel("WSL actions")).toContainText("remove:wsl:Ubuntu") await expect(connection).toHaveCount(0) await expect(settings.getByRole("tab", { name: "Server", exact: true })).toBeEnabled() }) } test("adding a WSL server while the WSL plugin is down shows that WSL is unavailable", async ({ page }) => { await mockOpenCodeServer(page, { directory: "/repo", project: project({ id: "proj_wsl_unavailable", directory: "/repo", name: "WSL project" }), provider: NO_PROVIDER, sessions: [], pageMessages: () => ({ items: [] }), }) await page.goto(`/e2e/utils/settings-wsl.html?${new URLSearchParams({ server: SERVER, mode: "ready" })}`) const settings = page.getByTestId("settings-screen") await settings.getByRole("tab", { name: "Servers", exact: true }).click() await expect(settings.getByRole("region", { name: "Ubuntu", exact: true })).toHaveCount(1) await page.getByRole("checkbox", { name: "WSL plugin" }).uncheck() await settings.getByRole("button", { name: "Add server", exact: true }).press("Enter") await page.getByRole("menuitem", { name: "Add WSL server", exact: true }).click() const dialog = page.getByRole("dialog") await expect(dialog.getByRole("heading", { name: "WSL unavailable", exact: true })).toBeVisible() await expect(dialog.getByText("OpenCode could not verify WSL on this machine.", { exact: true })).toBeVisible() await expect(dialog.getByText("WSL is unavailable", { exact: true })).toBeVisible() }) test("adding an SSH server while the SSH plugin is down fails instead of connecting forever", async ({ page }) => { await mockOpenCodeServer(page, { directory: "/repo", project: project({ id: "proj_ssh_unavailable", directory: "/repo", name: "SSH project" }), provider: NO_PROVIDER, sessions: [], pageMessages: () => ({ items: [] }), }) await page.goto(`/e2e/utils/settings-wsl.html?${new URLSearchParams({ server: SERVER, mode: "ready" })}`) const settings = page.getByTestId("settings-screen") await settings.getByRole("tab", { name: "Servers", exact: true }).click() await expect(settings.getByRole("region", { name: "Ubuntu", exact: true })).toHaveCount(1) await page.getByRole("checkbox", { name: "SSH plugin" }).uncheck() await settings.getByRole("button", { name: "Add server", exact: true }).press("Enter") await page.getByRole("menuitem", { name: "Add SSH server", exact: true }).click() const dialog = page.getByRole("dialog") await dialog.getByRole("textbox", { name: "Host or SSH command" }).fill("ssh devbox") await dialog.getByRole("button", { name: "Add server", exact: true }).click() await expect(dialog.getByRole("alert")).toHaveText("Request failed") await expect(dialog.getByRole("button", { name: "Add server", exact: true })).toBeEnabled() }) test("WSL session and draft tabs outlive the plugin going away until the server is removed", async ({ page }) => { const directory = "/home/ubuntu/project" const wsl = session({ id: "ses_wsl_tabs", directory, title: "WSL tabs session" }) const config = { directory, project: project({ id: "proj_wsl_tabs", directory }), provider: NO_PROVIDER, sessions: [wsl], pageMessages: () => ({ items: [] }), } await mockServers(page, { [SERVER]: { ...config, sessions: [] }, [REMOTE_SERVER]: config }) const href = `/server/${base64Encode("wsl:Ubuntu")}/session/${wsl.id}` await page.goto( `/e2e/utils/settings-wsl.html?${new URLSearchParams({ server: SERVER, mode: "ready", wsl: REMOTE_SERVER, path: href })}`, ) await expectSessionTitle(page, wsl.title) await page.getByRole("button", { name: "New session", exact: true }).click() await expect(page.getByRole("heading", { name: wsl.title })).toHaveCount(0) const editor = page.locator('[data-component="composer-editor"]') await editor.fill("keep this draft") await expect(editor).toHaveText("keep this draft") const tabs = page.locator("a[data-titlebar-tab-link]") const expectTabs = async () => { await expect(tabs).toHaveCount(2) await expect(tabs.nth(0)).toHaveAttribute("href", href) await expect(tabs.nth(1)).toHaveAttribute("href", /^\/new-session\?draftId=/) } await expectTabs() const plugin = page.getByRole("checkbox", { name: "WSL plugin" }) await plugin.uncheck() await expectTabs() await expect(editor).toHaveText("keep this draft") await plugin.check() await expectTabs() await tabs.nth(0).click() await expectSessionTitle(page, wsl.title) await page.keyboard.press("Control+,") const settings = page.getByTestId("settings-screen") await settings.getByRole("tab", { name: "Servers", exact: true }).click() const connection = settings.getByRole("region", { name: "Ubuntu", exact: true }) await connection.getByRole("button", { name: "More options", exact: true }).click() await page.getByRole("menuitem", { name: "Remove", exact: true }).click() await expect(page.getByLabel("WSL actions")).toHaveText("remove:wsl:Ubuntu") await expect(tabs).toHaveCount(0) }) test("an SSH host that asks for sign-in again opens the dialog once per selected tab", async ({ page }) => { const directory = "/home/box/project" const box = session({ id: "ses_ssh_offer", directory, title: "SSH offer session" }) const config = { directory, project: project({ id: "proj_ssh_offer", directory }), provider: provider(), sessions: [box], pageMessages: () => ({ items: [] }), } await mockServers(page, { [SERVER]: { ...config, sessions: [] }, [REMOTE_SERVER]: config }) const path = `/server/${base64Encode("ssh:box")}/session/${box.id}` await page.goto( `/e2e/utils/settings-wsl.html?${new URLSearchParams({ server: SERVER, mode: "ready", ssh: REMOTE_SERVER, path })}`, ) await expectSessionTitle(page, box.title) const connects = page.getByLabel("SSH connects") const signIn = page.getByRole("button", { name: "Require SSH sign-in" }) const authenticate = page.getByRole("button", { name: "Authenticate", exact: true }) // The first request on the tab opens sign-in by itself; the fixture's host connects without a prompt. await signIn.click() await expect(connects).toHaveText("1") await expectSessionTitle(page, box.title) // A later request on the same tab only offers the button. await signIn.click() await expect(authenticate).toBeVisible() await expect(connects).toHaveText("1") await authenticate.click() await expect(connects).toHaveText("2") await expectSessionTitle(page, box.title) // Another tab selected and this one selected again is a new selection, even while the host was ready. const tabs = page.locator("a[data-titlebar-tab-link]") await page.getByRole("button", { name: "New session", exact: true }).click() await expect(tabs).toHaveCount(2) await expect(tabs.nth(1)).toHaveAttribute("href", /^\/new-session\?draftId=/) await expect(page.getByRole("heading", { name: box.title })).toHaveCount(0) await tabs.nth(0).click() await expectSessionTitle(page, box.title) await signIn.click() await expect(connects).toHaveText("3") }) test("an open session moves to the controller a new SSH sign-in creates", async ({ page }) => { const directory = "/home/box/project" const model = { id: "box-model", name: "Box Model" } const box = session({ id: "ses_ssh", directory, title: "SSH session", model: { id: model.id, providerID: "opencode" }, }) const remote = { password: "ssh-1" } const prompts: unknown[] = [] const config = { directory, project: project({ id: "proj_ssh", directory }), provider: provider(model), sessions: [box], pageMessages: () => ({ items: [] }), } const servers = await mockServers(page, { [SERVER]: { ...config, sessions: [] }, [REMOTE_SERVER]: { ...config, password: () => remote.password, onPrompt: (input) => prompts.push(input.body.text), }, }) const path = `/server/${base64Encode("ssh:box")}/session/${box.id}` await page.goto( `/e2e/utils/settings-wsl.html?${new URLSearchParams({ server: SERVER, mode: "ready", ssh: REMOTE_SERVER, path })}`, ) await expectSessionTitle(page, box.title) // The remote server restarts behind the open tunnel with a new password and a renamed session. Health checks run // every 10 s; the first one it rejects disposes the session's controller, which ends its event stream. remote.password = "ssh-2" box.title = "SSH session after restart" await expect .poll(async () => (await servers[REMOTE_SERVER]!.transport.connections()).map((item) => !!item.endedBy), { timeout: 20_000, }) .toEqual([true]) await page.getByRole("button", { name: "Drop SSH tunnel" }).click() await page.getByRole("button", { name: "Reconnect", exact: true }).click() await expectSessionTitle(page, box.title) const editor = page.locator('[data-component="composer-editor"]') await editor.fill("after sign-in") await editor.press("Enter") await expect.poll(() => prompts).toEqual(["after sign-in"]) })