mirror of
https://github.com/anomalyco/opencode.git
synced 2026-11-15 04:27:20 +03:00
149 lines
6.1 KiB
TypeScript
149 lines
6.1 KiB
TypeScript
import type { Locator, Page, WebSocketRoute } from "@playwright/test"
|
|
import { expect, sourceURL, story } from "../../stories/playwright/story"
|
|
|
|
const fixture = sourceURL(new URL("../../gui-plugins/src/terminal/terminal.fixture.tsx", import.meta.url))
|
|
|
|
// No page loads anything from this origin; the routes below answer its terminal API and socket.
|
|
const SERVER = "http://127.0.0.1:4599"
|
|
|
|
const SIZE = { cols: 80, rows: 24 }
|
|
|
|
type Frame = { type: number; data: string }
|
|
|
|
/**
|
|
* A server with one terminal that already wrote `output`: it answers the snapshot and connect ticket, then speaks the
|
|
* persistent PTY socket protocol at `inputProtocol`. A missing `output` is a terminal the server no longer has.
|
|
*/
|
|
async function serveTerminal(page: Page, input: { ptyID: string; output?: string; inputProtocol?: 0 | 1 }) {
|
|
const frames: Frame[] = []
|
|
const sockets: WebSocketRoute[] = []
|
|
const tail = Buffer.byteLength(input.output ?? "")
|
|
|
|
const info = {
|
|
id: input.ptyID,
|
|
title: "Terminal 1",
|
|
userTitle: null,
|
|
command: "bash",
|
|
args: [],
|
|
cwd: "C:/OpenCode/Terminal",
|
|
status: "running",
|
|
pid: 1,
|
|
sessionID: "ses_terminal",
|
|
foregroundProcess: null,
|
|
size: SIZE,
|
|
output: { head: 0, tail },
|
|
}
|
|
|
|
await page.route(`${SERVER}/api/experimental/persistent-pty/**`, (route) => {
|
|
const headers = { "access-control-allow-origin": "*", "access-control-allow-headers": "*" }
|
|
|
|
if (route.request().method() === "OPTIONS") return route.fulfill({ status: 204, headers })
|
|
|
|
if (input.output === undefined) {
|
|
return route.fulfill({
|
|
status: 404,
|
|
headers,
|
|
// SAFETY: the server's wire body for a terminal it does not have.
|
|
// oxlint-disable-next-line anti-slop-effect/no-manual-tagged-construction -- see SAFETY above
|
|
json: { _tag: "PtyNotFoundError", ptyID: input.ptyID, message: `PTY session not found: ${input.ptyID}` },
|
|
})
|
|
}
|
|
|
|
if (route.request().url().endsWith("/snapshot")) {
|
|
const checkpoint = Buffer.from(input.output).toString("base64")
|
|
|
|
return route.fulfill({
|
|
headers,
|
|
json: { data: { info, text: input.output, checkpoint, cursor: { x: 0, y: 0 } } },
|
|
})
|
|
}
|
|
|
|
return route.fulfill({ headers, json: { data: { ticket: "component-ticket", expires_in: 60 } } })
|
|
})
|
|
|
|
// The socket mock patches the page's WebSocket when it loads, so this runs before the story mounts.
|
|
await page.routeWebSocket(new RegExp(`/api/experimental/persistent-pty/${input.ptyID}/connect`), (ws) => {
|
|
const url = new URL(ws.url())
|
|
const attachmentID = url.searchParams.get("attachment_id") ?? ""
|
|
sockets.push(ws)
|
|
ws.onMessage((message) => {
|
|
const data = Buffer.from(message)
|
|
|
|
frames.push({ type: data[0] ?? -1, data: data.subarray(5).toString() })
|
|
})
|
|
ws.send(
|
|
JSON.stringify({
|
|
type: "attached",
|
|
attachmentID,
|
|
inputProtocol: input.inputProtocol ?? 1,
|
|
info,
|
|
role: "controller",
|
|
generation: 1,
|
|
replay: { requestedOffset: tail, availableOffset: 0, endOffset: tail, truncated: false },
|
|
}),
|
|
)
|
|
ws.send(JSON.stringify({ type: "replay_complete", endOffset: tail }))
|
|
})
|
|
|
|
return { frames, sockets }
|
|
}
|
|
|
|
async function mountTerminal(page: Page, mount: (id: string) => Promise<Locator>, ptyID: string) {
|
|
// Any story loads the app styles; the fixture mounts the pane beside it.
|
|
await mount("ui-line-comment--editor")
|
|
await page.evaluate(
|
|
async (input) => {
|
|
const { mountTerminal } = await import(input.fixture)
|
|
mountTerminal({ url: input.server, ptyID: input.ptyID })
|
|
},
|
|
{ fixture, server: SERVER, ptyID },
|
|
)
|
|
|
|
return page.getByTestId("terminal-fixture")
|
|
}
|
|
|
|
story("does not send the emulator's own answers to terminal queries as input", async ({ page, mount }) => {
|
|
// Device status and attribute queries, in the restored screen and in live output. The daemon answers them.
|
|
const server = await serveTerminal(page, { ptyID: "pty_queries", output: "restored\x1b[6n\x1b[c\r\n" })
|
|
const root = await mountTerminal(page, mount, "pty_queries")
|
|
|
|
await expect.poll(() => server.sockets.length).toBe(1)
|
|
await expect.poll(() => server.frames.some((frame) => frame.type === 0)).toBe(true)
|
|
server.sockets[0]!.send(Buffer.from("live\x1b[6n\x1b[5n\x1b[c\r\n"))
|
|
|
|
// Typed input lands after the output was written, so any answer would already have been sent.
|
|
await expect(root.locator("textarea")).toBeFocused()
|
|
await page.keyboard.type("x")
|
|
await expect.poll(() => server.frames.filter((frame) => frame.type === 1).length).toBeGreaterThan(0)
|
|
expect(server.frames.filter((frame) => frame.type === 1).map((frame) => frame.data)).toEqual(["x"])
|
|
})
|
|
|
|
story("shows a terminal that has written nothing yet and stays silent", async ({ page, mount }) => {
|
|
// An empty checkpoint and no output: nothing ever reaches the emulator.
|
|
const server = await serveTerminal(page, { ptyID: "pty_silent", output: "" })
|
|
const root = await mountTerminal(page, mount, "pty_silent")
|
|
|
|
await expect.poll(() => server.sockets.length).toBe(1)
|
|
await expect(root.locator('[data-component="terminal"]')).toHaveCSS("opacity", "1")
|
|
await expect(root.locator("[data-failures]")).toHaveAttribute("data-failures", "0")
|
|
})
|
|
|
|
story("tells the user a server that predates the input protocol is too old", async ({ page, mount }) => {
|
|
await serveTerminal(page, { ptyID: "pty_old", output: "old server\r\n", inputProtocol: 0 })
|
|
const root = await mountTerminal(page, mount, "pty_old")
|
|
|
|
await expect(
|
|
root.getByText("This server is too old for terminals; update OpenCode on it", { exact: true }),
|
|
).toBeVisible()
|
|
await expect(root.locator("[data-failures]")).toHaveAttribute("data-failures", "1")
|
|
})
|
|
|
|
story("says the connection failed when the terminal cannot be restored", async ({ page, mount }) => {
|
|
const server = await serveTerminal(page, { ptyID: "pty_missing" })
|
|
const root = await mountTerminal(page, mount, "pty_missing")
|
|
|
|
await expect(root.getByText("Terminal connection failed", { exact: true })).toBeVisible()
|
|
await expect(root.locator("[data-failures]")).toHaveAttribute("data-failures", "1")
|
|
expect(server.sockets).toEqual([])
|
|
})
|