Skip to content
Merged
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
111 changes: 111 additions & 0 deletions packages/app/e2e/terminal/terminal-stale-restore.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import type { Page } from "@playwright/test"
import { test, expect } from "../fixtures"
import { waitSession, waitTerminalReady } from "../actions"
import { terminalSelector } from "../selectors"
import { terminalToggleKey } from "../utils"

type PersistedTerminalStateV2 = {
version: 2
activeTabID?: string
tabs: Array<{
tabID: string
title: string
titleNumber: number
order: number
snapshot?: {
buffer?: string
cursor?: number
scrollY?: number
size?: { rows: number; cols: number }
}
}>
}

function readPersistedTerminalEntry() {
const keys = Array.from({ length: localStorage.length }, (_, index) => localStorage.key(index)).filter(
(key): key is string => !!key && key.endsWith(":workspace:terminal"),
)
for (const key of keys) {
const raw = localStorage.getItem(key)
if (!raw) continue
const state = JSON.parse(raw) as PersistedTerminalStateV2
if (state.version === 2) return { key, state }
}
return undefined
}

async function openTerminal(page: Page) {
const terminal = page.locator(terminalSelector).first()
const visible = await terminal.isVisible().catch(() => false)
if (!visible) await page.keyboard.press(terminalToggleKey)
await waitTerminalReady(page, { term: terminal })
return terminal
}

test("restored legacy terminal state creates a fresh runtime pty", async ({ page, project }) => {
const oldRequests: string[] = []

page.on("request", (request) => {
const url = request.url()
if (url.includes("/pty/pty_old")) oldRequests.push(url)
})
page.on("websocket", (socket) => {
const url = socket.url()
if (url.includes("/pty/pty_old")) oldRequests.push(url)
})

await project.open()
const initialTerminal = await openTerminal(page)
const initialTabID = await initialTerminal.getAttribute("data-pty-id")
expect(initialTabID).toMatch(/^tab_/)

const entry = await expect
.poll(() => page.evaluate(readPersistedTerminalEntry), { timeout: 5_000 })
.toMatchObject({ state: { version: 2 } })
.then(() => page.evaluate(readPersistedTerminalEntry))
if (!entry) throw new Error("Terminal storage entry was not persisted")

await page.evaluate((key) => {
localStorage.setItem(
key,
JSON.stringify({
active: "pty_old",
all: [
{
id: "pty_old",
title: "Legacy terminal",
titleNumber: 1,
rows: 24,
cols: 80,
buffer: "LEGACY_BUFFER",
cursor: 13,
scrollY: 0,
},
],
}),
)
}, entry.key)
oldRequests.length = 0
await page.reload()
await waitSession(page, { directory: project.directory, serverUrl: project.url, allowAnySession: true })

const terminal = await openTerminal(page)
const tabID = await terminal.getAttribute("data-pty-id")
expect(tabID).toMatch(/^tab_/)
expect(tabID).not.toBe("pty_old")

const state = await expect
.poll(() => page.evaluate(readPersistedTerminalEntry), { timeout: 5_000 })
.toMatchObject({ state: { version: 2 } })
.then(() => page.evaluate(readPersistedTerminalEntry).then((entry) => entry?.state))

expect(oldRequests).toEqual([])
expect(state?.version).toBe(2)
expect(state?.activeTabID).toBe(tabID)
expect(state?.tabs).toHaveLength(1)
expect(state?.tabs[0]?.tabID).toBe(tabID)
expect(state?.tabs[0]?.title).toBe("Legacy terminal")
expect(state?.tabs[0]?.order).toBe(0)
expect(JSON.stringify(state)).not.toContain("pty_old")
expect(JSON.stringify(state)).not.toContain("runtimePty")
})
7 changes: 6 additions & 1 deletion packages/opencode/src/server/instance/pty.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,12 @@ export function PtyRoutes(upgradeWebSocket: UpgradeWebSocket) {
}),
validator("param", z.object({ ptyID: PtyID.zod })),
async (c) => {
await Pty.remove(c.req.valid("param").ptyID)
const id = c.req.valid("param").ptyID
const info = await Pty.get(id)
if (!info) {
throw new NotFoundError({ message: "Session not found" })
}
Comment thread
Astro-Han marked this conversation as resolved.
await Pty.remove(id)
return c.json(true)
},
)
Expand Down
19 changes: 19 additions & 0 deletions packages/opencode/test/server/pty-routes.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,4 +68,23 @@ describe("pty routes", () => {
},
})
})

test("maps missing remove targets as not found", async () => {
await using tmp = await tmpdir({ git: true })
await Instance.provide({
directory: tmp.path,
fn: async () => {
const app = new Hono().route("/pty", PtyRoutes(testUpgradeWebSocket))
app.onError(ErrorMiddleware)

const response = await app.request(`/pty/${PtyID.ascending()}`, {
method: "DELETE",
})
const body = await response.json()

expect(response.status).toBe(404)
expect(body.name).toBe("NotFoundError")
},
})
})
})
Loading