summaryrefslogtreecommitdiffhomepage
path: root/packages/app/e2e
diff options
context:
space:
mode:
authorAdam <[email protected]>2026-03-13 08:56:48 -0500
committerGitHub <[email protected]>2026-03-13 08:56:48 -0500
commitc9e9dbeee1ceb5af3d1b1ce292317390286fe7a0 (patch)
treee679df64078db58eadb3cba2d1ac65b170175360 /packages/app/e2e
parentb88b323049ed7ad7bf41fbec6ce66247ec4047ab (diff)
downloadopencode-c9e9dbeee1ceb5af3d1b1ce292317390286fe7a0.tar.gz
opencode-c9e9dbeee1ceb5af3d1b1ce292317390286fe7a0.zip
fix(app): terminal cloning without retry (#17354)
Diffstat (limited to 'packages/app/e2e')
-rw-r--r--packages/app/e2e/actions.ts31
-rw-r--r--packages/app/e2e/terminal/terminal-reconnect.spec.ts46
-rw-r--r--packages/app/e2e/tsconfig.json3
3 files changed, 75 insertions, 5 deletions
diff --git a/packages/app/e2e/actions.ts b/packages/app/e2e/actions.ts
index a56001248..8e21579e2 100644
--- a/packages/app/e2e/actions.ts
+++ b/packages/app/e2e/actions.ts
@@ -36,6 +36,22 @@ async function terminalID(term: Locator) {
throw new Error(`Active terminal missing ${terminalAttr}`)
}
+export async function terminalConnects(page: Page, input?: { term?: Locator }) {
+ const term = input?.term ?? page.locator(terminalSelector).first()
+ const id = await terminalID(term)
+ return page.evaluate((id) => {
+ return (window as E2EWindow).__opencode_e2e?.terminal?.terminals?.[id]?.connects ?? 0
+ }, id)
+}
+
+export async function disconnectTerminal(page: Page, input?: { term?: Locator }) {
+ const term = input?.term ?? page.locator(terminalSelector).first()
+ const id = await terminalID(term)
+ await page.evaluate((id) => {
+ ;(window as E2EWindow).__opencode_e2e?.terminal?.controls?.[id]?.disconnect?.()
+ }, id)
+}
+
async function terminalReady(page: Page, term?: Locator) {
const next = term ?? page.locator(terminalSelector).first()
const id = await terminalID(next)
@@ -588,12 +604,19 @@ export async function seedSessionTask(
.flatMap((message) => message.parts)
.find((part) => {
if (part.type !== "tool" || part.tool !== "task") return false
- if (part.state.input?.description !== input.description) return false
- return typeof part.state.metadata?.sessionId === "string" && part.state.metadata.sessionId.length > 0
+ if (!("state" in part) || !part.state || typeof part.state !== "object") return false
+ if (!("input" in part.state) || !part.state.input || typeof part.state.input !== "object") return false
+ if (!("description" in part.state.input) || part.state.input.description !== input.description) return false
+ if (!("metadata" in part.state) || !part.state.metadata || typeof part.state.metadata !== "object")
+ return false
+ if (!("sessionId" in part.state.metadata)) return false
+ return typeof part.state.metadata.sessionId === "string" && part.state.metadata.sessionId.length > 0
})
- if (!part) return
- const id = part.state.metadata?.sessionId
+ if (!part || !("state" in part) || !part.state || typeof part.state !== "object") return
+ if (!("metadata" in part.state) || !part.state.metadata || typeof part.state.metadata !== "object") return
+ if (!("sessionId" in part.state.metadata)) return
+ const id = part.state.metadata.sessionId
if (typeof id !== "string" || !id) return
const child = await sdk.session
.get({ sessionID: id })
diff --git a/packages/app/e2e/terminal/terminal-reconnect.spec.ts b/packages/app/e2e/terminal/terminal-reconnect.spec.ts
new file mode 100644
index 000000000..b03ed8956
--- /dev/null
+++ b/packages/app/e2e/terminal/terminal-reconnect.spec.ts
@@ -0,0 +1,46 @@
+import type { Page } from "@playwright/test"
+import { disconnectTerminal, runTerminal, terminalConnects, waitTerminalReady } from "../actions"
+import { test, expect } from "../fixtures"
+import { terminalSelector } from "../selectors"
+import { terminalToggleKey } from "../utils"
+
+async function open(page: Page) {
+ const term = page.locator(terminalSelector).first()
+ const visible = await term.isVisible().catch(() => false)
+ if (!visible) await page.keyboard.press(terminalToggleKey)
+ await waitTerminalReady(page, { term })
+ return term
+}
+
+test("terminal reconnects without replacing the pty", async ({ page, withProject }) => {
+ await withProject(async ({ gotoSession }) => {
+ const name = `OPENCODE_E2E_RECONNECT_${Date.now()}`
+ const token = `E2E_RECONNECT_${Date.now()}`
+
+ await gotoSession()
+
+ const term = await open(page)
+ const id = await term.getAttribute("data-pty-id")
+ if (!id) throw new Error("Active terminal missing data-pty-id")
+
+ const prev = await terminalConnects(page, { term })
+
+ await runTerminal(page, {
+ term,
+ cmd: `export ${name}=${token}; echo ${token}`,
+ token,
+ })
+
+ await disconnectTerminal(page, { term })
+
+ await expect.poll(() => terminalConnects(page, { term }), { timeout: 15_000 }).toBeGreaterThan(prev)
+ await expect.poll(() => term.getAttribute("data-pty-id"), { timeout: 5_000 }).toBe(id)
+
+ await runTerminal(page, {
+ term,
+ cmd: `echo $${name}`,
+ token,
+ timeout: 15_000,
+ })
+ })
+})
diff --git a/packages/app/e2e/tsconfig.json b/packages/app/e2e/tsconfig.json
index 18e88ddc9..a86352b83 100644
--- a/packages/app/e2e/tsconfig.json
+++ b/packages/app/e2e/tsconfig.json
@@ -2,7 +2,8 @@
"extends": "../tsconfig.json",
"compilerOptions": {
"noEmit": true,
+ "rootDir": "..",
"types": ["node", "bun"]
},
- "include": ["./**/*.ts"]
+ "include": ["./**/*.ts", "../src/testing/terminal.ts"]
}