summaryrefslogtreecommitdiffhomepage
path: root/packages
diff options
context:
space:
mode:
authorKit Langton <[email protected]>2026-04-01 13:43:19 -0400
committerGitHub <[email protected]>2026-04-01 17:43:19 +0000
commitf3f728ec27b2b2fc67470a2acec0072a5f1badd0 (patch)
treed8d36b7721d3e48965cc6fd84587fc533a0349b5 /packages
parentc619caefdd1d184cae549749240c5f77f63b150a (diff)
downloadopencode-f3f728ec27b2b2fc67470a2acec0072a5f1badd0.tar.gz
opencode-f3f728ec27b2b2fc67470a2acec0072a5f1badd0.zip
test(app): fix isolated backend follow-ups (#20513)
Diffstat (limited to 'packages')
-rw-r--r--packages/app/e2e/backend.ts15
-rw-r--r--packages/app/e2e/prompt/prompt-history.spec.ts144
-rw-r--r--packages/app/e2e/prompt/prompt-slash-share.spec.ts1
-rw-r--r--packages/app/e2e/session/session-child-navigation.spec.ts32
-rw-r--r--packages/app/e2e/session/session-composer-dock.spec.ts600
-rw-r--r--packages/app/e2e/session/session-undo-redo.spec.ts3
-rw-r--r--packages/app/e2e/session/session.spec.ts4
7 files changed, 417 insertions, 382 deletions
diff --git a/packages/app/e2e/backend.ts b/packages/app/e2e/backend.ts
index 22122a372..4dfa7c64f 100644
--- a/packages/app/e2e/backend.ts
+++ b/packages/app/e2e/backend.ts
@@ -44,6 +44,14 @@ async function waitForHealth(url: string, probe = "/global/health") {
throw new Error(`Timed out waiting for backend health at ${url}${probe}${last ? ` (${last})` : ""}`)
}
+async function waitExit(proc: ReturnType<typeof spawn>, timeout = 10_000) {
+ if (proc.exitCode !== null) return
+ await Promise.race([
+ new Promise<void>((resolve) => proc.once("exit", () => resolve())),
+ new Promise<void>((resolve) => setTimeout(resolve, timeout)),
+ ])
+}
+
const LOG_CAP = 100
function cap(input: string[]) {
@@ -62,7 +70,6 @@ export async function startBackend(label: string): Promise<Handle> {
const opencodeDir = path.join(repoDir, "packages", "opencode")
const env = {
...process.env,
- OPENCODE_DISABLE_SHARE: process.env.OPENCODE_DISABLE_SHARE ?? "true",
OPENCODE_DISABLE_LSP_DOWNLOAD: "true",
OPENCODE_DISABLE_DEFAULT_PLUGINS: "true",
OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER: "true",
@@ -117,7 +124,11 @@ export async function startBackend(label: string): Promise<Handle> {
async stop() {
if (proc.exitCode === null) {
proc.kill("SIGTERM")
- await new Promise((resolve) => proc.once("exit", () => resolve(undefined))).catch(() => undefined)
+ await waitExit(proc)
+ }
+ if (proc.exitCode === null) {
+ proc.kill("SIGKILL")
+ await waitExit(proc)
}
await fs.rm(sandbox, { recursive: true, force: true }).catch(() => undefined)
},
diff --git a/packages/app/e2e/prompt/prompt-history.spec.ts b/packages/app/e2e/prompt/prompt-history.spec.ts
index 6420534e0..f2d15914d 100644
--- a/packages/app/e2e/prompt/prompt-history.spec.ts
+++ b/packages/app/e2e/prompt/prompt-history.spec.ts
@@ -3,9 +3,11 @@ import type { Page } from "@playwright/test"
import { test, expect } from "../fixtures"
import { assistantText, sessionIDFromUrl } from "../actions"
import { promptSelector } from "../selectors"
+import { createSdk } from "../utils"
import { openaiModel, promptMatch, titleMatch, withMockOpenAI } from "./mock"
const text = (value: string | null) => (value ?? "").replace(/\u200B/g, "").trim()
+type Sdk = ReturnType<typeof createSdk>
const isBash = (part: unknown): part is ToolPart => {
if (!part || typeof part !== "object") return false
@@ -14,47 +16,15 @@ const isBash = (part: unknown): part is ToolPart => {
return "state" in part
}
-async function edge(page: Page, pos: "start" | "end") {
- await page.locator(promptSelector).evaluate((el: HTMLDivElement, pos: "start" | "end") => {
- const selection = window.getSelection()
- if (!selection) return
-
- const walk = document.createTreeWalker(el, NodeFilter.SHOW_TEXT)
- const nodes: Text[] = []
- for (let node = walk.nextNode(); node; node = walk.nextNode()) {
- nodes.push(node as Text)
- }
-
- if (nodes.length === 0) {
- const node = document.createTextNode("")
- el.appendChild(node)
- nodes.push(node)
- }
-
- const node = pos === "start" ? nodes[0]! : nodes[nodes.length - 1]!
- const range = document.createRange()
- range.setStart(node, pos === "start" ? 0 : (node.textContent ?? "").length)
- range.collapse(true)
- selection.removeAllRanges()
- selection.addRange(range)
- }, pos)
-}
-
async function wait(page: Page, value: string) {
await expect.poll(async () => text(await page.locator(promptSelector).textContent())).toBe(value)
}
-async function reply(
- sdk: { session: { messages: Parameters<typeof assistantText>[0]["session"] } },
- sessionID: string,
- token: string,
-) {
- await expect
- .poll(() => assistantText(sdk as Parameters<typeof assistantText>[0], sessionID), { timeout: 90_000 })
- .toContain(token)
+async function reply(sdk: Sdk, sessionID: string, token: string) {
+ await expect.poll(() => assistantText(sdk, sessionID), { timeout: 90_000 }).toContain(token)
}
-async function shell(sdk: Parameters<typeof withSession>[0], sessionID: string, cmd: string, token: string) {
+async function shell(sdk: Sdk, sessionID: string, cmd: string, token: string) {
await expect
.poll(
async () => {
@@ -142,76 +112,64 @@ test("prompt history restores unsent draft with arrow navigation", async ({
})
})
-test("shell history stays separate from normal prompt history", async ({ page, llm, backend, withBackendProject }) => {
+test.fixme("shell history stays separate from normal prompt history", async ({ page, sdk, gotoSession }) => {
test.setTimeout(120_000)
- await withMockOpenAI({
- serverUrl: backend.url,
- llmUrl: llm.url,
- fn: async () => {
- const firstToken = `E2E_SHELL_ONE_${Date.now()}`
- const secondToken = `E2E_SHELL_TWO_${Date.now()}`
- const normalToken = `E2E_NORMAL_${Date.now()}`
- const first = `echo ${firstToken}`
- const second = `echo ${secondToken}`
- const normal = `Reply with exactly: ${normalToken}`
+ const firstToken = `E2E_SHELL_ONE_${Date.now()}`
+ const secondToken = `E2E_SHELL_TWO_${Date.now()}`
+ const normalToken = `E2E_NORMAL_${Date.now()}`
+ const first = `echo ${firstToken}`
+ const second = `echo ${secondToken}`
+ const normal = `Reply with exactly: ${normalToken}`
- await llm.textMatch(titleMatch, "E2E Title")
- await llm.textMatch(promptMatch(normalToken), normalToken)
+ await gotoSession()
- await withBackendProject(
- async (project) => {
- const prompt = page.locator(promptSelector)
+ const prompt = page.locator(promptSelector)
- await prompt.click()
- await page.keyboard.type("!")
- await page.keyboard.type(first)
- await page.keyboard.press("Enter")
- await wait(page, "")
+ await prompt.click()
+ await page.keyboard.type("!")
+ await page.keyboard.type(first)
+ await page.keyboard.press("Enter")
+ await wait(page, "")
- await expect(page).toHaveURL(/\/session\/[^/?#]+/, { timeout: 30_000 })
- const sessionID = sessionIDFromUrl(page.url())!
- project.trackSession(sessionID)
- await shell(project.sdk, sessionID, first, firstToken)
+ await expect(page).toHaveURL(/\/session\/[^/?#]+/, { timeout: 30_000 })
+ const sessionID = sessionIDFromUrl(page.url())!
+ await shell(sdk, sessionID, first, firstToken)
- await prompt.click()
- await page.keyboard.type("!")
- await page.keyboard.type(second)
- await page.keyboard.press("Enter")
- await wait(page, "")
- await shell(project.sdk, sessionID, second, secondToken)
+ await prompt.click()
+ await page.keyboard.type("!")
+ await page.keyboard.type(second)
+ await page.keyboard.press("Enter")
+ await wait(page, "")
+ await shell(sdk, sessionID, second, secondToken)
- await prompt.click()
- await page.keyboard.type("!")
- await page.keyboard.press("ArrowUp")
- await wait(page, second)
+ await page.keyboard.press("Escape")
+ await wait(page, "")
- await page.keyboard.press("ArrowUp")
- await wait(page, first)
+ await prompt.click()
+ await page.keyboard.type("!")
+ await page.keyboard.press("ArrowUp")
+ await wait(page, second)
- await page.keyboard.press("ArrowDown")
- await wait(page, second)
+ await page.keyboard.press("ArrowUp")
+ await wait(page, first)
- await page.keyboard.press("ArrowDown")
- await wait(page, "")
+ await page.keyboard.press("ArrowDown")
+ await wait(page, second)
- await page.keyboard.press("Escape")
- await wait(page, "")
+ await page.keyboard.press("ArrowDown")
+ await wait(page, "")
- await prompt.click()
- await page.keyboard.type(normal)
- await page.keyboard.press("Enter")
- await wait(page, "")
- await reply(project.sdk, sessionID, normalToken)
+ await page.keyboard.press("Escape")
+ await wait(page, "")
- await prompt.click()
- await page.keyboard.press("ArrowUp")
- await wait(page, normal)
- },
- {
- model: openaiModel,
- },
- )
- },
- })
+ await prompt.click()
+ await page.keyboard.type(normal)
+ await page.keyboard.press("Enter")
+ await wait(page, "")
+ await reply(sdk, sessionID, normalToken)
+
+ await prompt.click()
+ await page.keyboard.press("ArrowUp")
+ await wait(page, normal)
})
diff --git a/packages/app/e2e/prompt/prompt-slash-share.spec.ts b/packages/app/e2e/prompt/prompt-slash-share.spec.ts
index efb0272b5..5371d8a91 100644
--- a/packages/app/e2e/prompt/prompt-slash-share.spec.ts
+++ b/packages/app/e2e/prompt/prompt-slash-share.spec.ts
@@ -27,6 +27,7 @@ test("/share and /unshare update session share state", async ({ page, withBacken
await withBackendProject(async (project) => {
await withSession(project.sdk, `e2e slash share ${Date.now()}`, async (session) => {
+ project.trackSession(session.id)
const prompt = page.locator(promptSelector)
await seed(project.sdk, session.id)
diff --git a/packages/app/e2e/session/session-child-navigation.spec.ts b/packages/app/e2e/session/session-child-navigation.spec.ts
index 616f694a3..fa366e515 100644
--- a/packages/app/e2e/session/session-child-navigation.spec.ts
+++ b/packages/app/e2e/session/session-child-navigation.spec.ts
@@ -1,5 +1,6 @@
import { seedSessionTask, withSession } from "../actions"
import { test, expect } from "../fixtures"
+import { promptSelector } from "../selectors"
test("task tool child-session link does not trigger stale show errors", async ({ page, withBackendProject }) => {
test.setTimeout(120_000)
@@ -10,17 +11,16 @@ test("task tool child-session link does not trigger stale show errors", async ({
}
page.on("pageerror", onError)
- await withBackendProject(async ({ gotoSession, trackSession, sdk }) => {
- await withSession(sdk, `e2e child nav ${Date.now()}`, async (session) => {
- trackSession(session.id)
- const child = await seedSessionTask(sdk, {
- sessionID: session.id,
- description: "Open child session",
- prompt: "Search the repository for AssistantParts and then reply with exactly CHILD_OK.",
- })
- trackSession(child.sessionID)
+ try {
+ await withBackendProject(async ({ gotoSession, trackSession, sdk }) => {
+ await withSession(sdk, `e2e child nav ${Date.now()}`, async (session) => {
+ const child = await seedSessionTask(sdk, {
+ sessionID: session.id,
+ description: "Open child session",
+ prompt: "Search the repository for AssistantParts and then reply with exactly CHILD_OK.",
+ })
+ trackSession(child.sessionID)
- try {
await gotoSession(session.id)
const link = page
@@ -31,11 +31,11 @@ test("task tool child-session link does not trigger stale show errors", async ({
await link.click()
await expect(page).toHaveURL(new RegExp(`/session/${child.sessionID}(?:[/?#]|$)`), { timeout: 30_000 })
- await page.waitForTimeout(1000)
- expect(errs).toEqual([])
- } finally {
- page.off("pageerror", onError)
- }
+ await expect(page.locator(promptSelector)).toBeVisible({ timeout: 30_000 })
+ await expect.poll(() => errs, { timeout: 5_000 }).toEqual([])
+ })
})
- })
+ } finally {
+ page.off("pageerror", onError)
+ }
})
diff --git a/packages/app/e2e/session/session-composer-dock.spec.ts b/packages/app/e2e/session/session-composer-dock.spec.ts
index 9d44683c8..2c87a309d 100644
--- a/packages/app/e2e/session/session-composer-dock.spec.ts
+++ b/packages/app/e2e/session/session-composer-dock.spec.ts
@@ -22,12 +22,13 @@ async function withDockSession<T>(
sdk: Sdk,
title: string,
fn: (session: { id: string; title: string }) => Promise<T>,
- opts?: { permission?: PermissionRule[] },
+ opts?: { permission?: PermissionRule[]; trackSession?: (sessionID: string) => void },
) {
const session = await sdk.session
.create(opts?.permission ? { title, permission: opts.permission } : { title })
.then((r) => r.data)
if (!session?.id) throw new Error("Session create did not return an id")
+ opts?.trackSession?.(session.id)
try {
return await fn(session)
} finally {
@@ -258,17 +259,22 @@ async function withMockPermission<T>(
test("default dock shows prompt input", async ({ page, withBackendProject }) => {
await withBackendProject(async (project) => {
- await withDockSession(project.sdk, "e2e composer dock default", async (session) => {
- await project.gotoSession(session.id)
+ await withDockSession(
+ project.sdk,
+ "e2e composer dock default",
+ async (session) => {
+ await project.gotoSession(session.id)
- await expect(page.locator(sessionComposerDockSelector)).toBeVisible()
- await expect(page.locator(promptSelector)).toBeVisible()
- await expect(page.locator(questionDockSelector)).toHaveCount(0)
- await expect(page.locator(permissionDockSelector)).toHaveCount(0)
+ await expect(page.locator(sessionComposerDockSelector)).toBeVisible()
+ await expect(page.locator(promptSelector)).toBeVisible()
+ await expect(page.locator(questionDockSelector)).toHaveCount(0)
+ await expect(page.locator(permissionDockSelector)).toHaveCount(0)
- await page.locator(promptSelector).click()
- await expect(page.locator(promptSelector)).toBeFocused()
- })
+ await page.locator(promptSelector).click()
+ await expect(page.locator(promptSelector)).toBeFocused()
+ },
+ { trackSession: project.trackSession },
+ )
})
})
@@ -287,190 +293,220 @@ test("auto-accept toggle works before first submit", async ({ page, withBackendP
test("blocked question flow unblocks after submit", async ({ page, withBackendProject }) => {
await withBackendProject(async (project) => {
- await withDockSession(project.sdk, "e2e composer dock question", async (session) => {
- await withDockSeed(project.sdk, session.id, async () => {
- await project.gotoSession(session.id)
+ await withDockSession(
+ project.sdk,
+ "e2e composer dock question",
+ async (session) => {
+ await withDockSeed(project.sdk, session.id, async () => {
+ await project.gotoSession(session.id)
- await seedSessionQuestion(project.sdk, {
- sessionID: session.id,
- questions: [
- {
- header: "Need input",
- question: "Pick one option",
- options: [
- { label: "Continue", description: "Continue now" },
- { label: "Stop", description: "Stop here" },
- ],
- },
- ],
- })
+ await seedSessionQuestion(project.sdk, {
+ sessionID: session.id,
+ questions: [
+ {
+ header: "Need input",
+ question: "Pick one option",
+ options: [
+ { label: "Continue", description: "Continue now" },
+ { label: "Stop", description: "Stop here" },
+ ],
+ },
+ ],
+ })
- const dock = page.locator(questionDockSelector)
- await expectQuestionBlocked(page)
+ const dock = page.locator(questionDockSelector)
+ await expectQuestionBlocked(page)
- await dock.locator('[data-slot="question-option"]').first().click()
- await dock.getByRole("button", { name: /submit/i }).click()
+ await dock.locator('[data-slot="question-option"]').first().click()
+ await dock.getByRole("button", { name: /submit/i }).click()
- await expectQuestionOpen(page)
- })
- })
+ await expectQuestionOpen(page)
+ })
+ },
+ { trackSession: project.trackSession },
+ )
})
})
test("blocked question flow supports keyboard shortcuts", async ({ page, withBackendProject }) => {
await withBackendProject(async (project) => {
- await withDockSession(project.sdk, "e2e composer dock question keyboard", async (session) => {
- await withDockSeed(project.sdk, session.id, async () => {
- await project.gotoSession(session.id)
+ await withDockSession(
+ project.sdk,
+ "e2e composer dock question keyboard",
+ async (session) => {
+ await withDockSeed(project.sdk, session.id, async () => {
+ await project.gotoSession(session.id)
- await seedSessionQuestion(project.sdk, {
- sessionID: session.id,
- questions: [
- {
- header: "Need input",
- question: "Pick one option",
- options: [
- { label: "Continue", description: "Continue now" },
- { label: "Stop", description: "Stop here" },
- ],
- },
- ],
- })
+ await seedSessionQuestion(project.sdk, {
+ sessionID: session.id,
+ questions: [
+ {
+ header: "Need input",
+ question: "Pick one option",
+ options: [
+ { label: "Continue", description: "Continue now" },
+ { label: "Stop", description: "Stop here" },
+ ],
+ },
+ ],
+ })
- const dock = page.locator(questionDockSelector)
- const first = dock.locator('[data-slot="question-option"]').first()
- const second = dock.locator('[data-slot="question-option"]').nth(1)
+ const dock = page.locator(questionDockSelector)
+ const first = dock.locator('[data-slot="question-option"]').first()
+ const second = dock.locator('[data-slot="question-option"]').nth(1)
- await expectQuestionBlocked(page)
- await expect(first).toBeFocused()
+ await expectQuestionBlocked(page)
+ await expect(first).toBeFocused()
- await page.keyboard.press("ArrowDown")
- await expect(second).toBeFocused()
+ await page.keyboard.press("ArrowDown")
+ await expect(second).toBeFocused()
- await page.keyboard.press("Space")
- await page.keyboard.press(`${modKey}+Enter`)
- await expectQuestionOpen(page)
- })
- })
+ await page.keyboard.press("Space")
+ await page.keyboard.press(`${modKey}+Enter`)
+ await expectQuestionOpen(page)
+ })
+ },
+ { trackSession: project.trackSession },
+ )
})
})
test("blocked question flow supports escape dismiss", async ({ page, withBackendProject }) => {
await withBackendProject(async (project) => {
- await withDockSession(project.sdk, "e2e composer dock question escape", async (session) => {
- await withDockSeed(project.sdk, session.id, async () => {
- await project.gotoSession(session.id)
+ await withDockSession(
+ project.sdk,
+ "e2e composer dock question escape",
+ async (session) => {
+ await withDockSeed(project.sdk, session.id, async () => {
+ await project.gotoSession(session.id)
- await seedSessionQuestion(project.sdk, {
- sessionID: session.id,
- questions: [
- {
- header: "Need input",
- question: "Pick one option",
- options: [
- { label: "Continue", description: "Continue now" },
- { label: "Stop", description: "Stop here" },
- ],
- },
- ],
- })
+ await seedSessionQuestion(project.sdk, {
+ sessionID: session.id,
+ questions: [
+ {
+ header: "Need input",
+ question: "Pick one option",
+ options: [
+ { label: "Continue", description: "Continue now" },
+ { label: "Stop", description: "Stop here" },
+ ],
+ },
+ ],
+ })
- const dock = page.locator(questionDockSelector)
- const first = dock.locator('[data-slot="question-option"]').first()
+ const dock = page.locator(questionDockSelector)
+ const first = dock.locator('[data-slot="question-option"]').first()
- await expectQuestionBlocked(page)
- await expect(first).toBeFocused()
+ await expectQuestionBlocked(page)
+ await expect(first).toBeFocused()
- await page.keyboard.press("Escape")
- await expectQuestionOpen(page)
- })
- })
+ await page.keyboard.press("Escape")
+ await expectQuestionOpen(page)
+ })
+ },
+ { trackSession: project.trackSession },
+ )
})
})
test("blocked permission flow supports allow once", async ({ page, withBackendProject }) => {
await withBackendProject(async (project) => {
- await withDockSession(project.sdk, "e2e composer dock permission once", async (session) => {
- await project.gotoSession(session.id)
- await setAutoAccept(page, false)
- await withMockPermission(
- page,
- {
- id: "per_e2e_once",
- sessionID: session.id,
- permission: "bash",
- patterns: ["/tmp/opencode-e2e-perm-once"],
- metadata: { description: "Need permission for command" },
- },
- undefined,
- async (state) => {
- await page.goto(page.url())
- await expectPermissionBlocked(page)
-
- await clearPermissionDock(page, /allow once/i)
- await state.resolved()
- await page.goto(page.url())
- await expectPermissionOpen(page)
- },
- )
- })
+ await withDockSession(
+ project.sdk,
+ "e2e composer dock permission once",
+ async (session) => {
+ await project.gotoSession(session.id)
+ await setAutoAccept(page, false)
+ await withMockPermission(
+ page,
+ {
+ id: "per_e2e_once",
+ sessionID: session.id,
+ permission: "bash",
+ patterns: ["/tmp/opencode-e2e-perm-once"],
+ metadata: { description: "Need permission for command" },
+ },
+ undefined,
+ async (state) => {
+ await page.goto(page.url())
+ await expectPermissionBlocked(page)
+
+ await clearPermissionDock(page, /allow once/i)
+ await state.resolved()
+ await page.goto(page.url())
+ await expectPermissionOpen(page)
+ },
+ )
+ },
+ { trackSession: project.trackSession },
+ )
})
})
test("blocked permission flow supports reject", async ({ page, withBackendProject }) => {
await withBackendProject(async (project) => {
- await withDockSession(project.sdk, "e2e composer dock permission reject", async (session) => {
- await project.gotoSession(session.id)
- await setAutoAccept(page, false)
- await withMockPermission(
- page,
- {
- id: "per_e2e_reject",
- sessionID: session.id,
- permission: "bash",
- patterns: ["/tmp/opencode-e2e-perm-reject"],
- },
- undefined,
- async (state) => {
- await page.goto(page.url())
- await expectPermissionBlocked(page)
-
- await clearPermissionDock(page, /deny/i)
- await state.resolved()
- await page.goto(page.url())
- await expectPermissionOpen(page)
- },
- )
- })
+ await withDockSession(
+ project.sdk,
+ "e2e composer dock permission reject",
+ async (session) => {
+ await project.gotoSession(session.id)
+ await setAutoAccept(page, false)
+ await withMockPermission(
+ page,
+ {
+ id: "per_e2e_reject",
+ sessionID: session.id,
+ permission: "bash",
+ patterns: ["/tmp/opencode-e2e-perm-reject"],
+ },
+ undefined,
+ async (state) => {
+ await page.goto(page.url())
+ await expectPermissionBlocked(page)
+
+ await clearPermissionDock(page, /deny/i)
+ await state.resolved()
+ await page.goto(page.url())
+ await expectPermissionOpen(page)
+ },
+ )
+ },
+ { trackSession: project.trackSession },
+ )
})
})
test("blocked permission flow supports allow always", async ({ page, withBackendProject }) => {
await withBackendProject(async (project) => {
- await withDockSession(project.sdk, "e2e composer dock permission always", async (session) => {
- await project.gotoSession(session.id)
- await setAutoAccept(page, false)
- await withMockPermission(
- page,
- {
- id: "per_e2e_always",
- sessionID: session.id,
- permission: "bash",
- patterns: ["/tmp/opencode-e2e-perm-always"],
- metadata: { description: "Need permission for command" },
- },
- undefined,
- async (state) => {
- await page.goto(page.url())
- await expectPermissionBlocked(page)
-
- await clearPermissionDock(page, /allow always/i)
- await state.resolved()
- await page.goto(page.url())
- await expectPermissionOpen(page)
- },
- )
- })
+ await withDockSession(
+ project.sdk,
+ "e2e composer dock permission always",
+ async (session) => {
+ await project.gotoSession(session.id)
+ await setAutoAccept(page, false)
+ await withMockPermission(
+ page,
+ {
+ id: "per_e2e_always",
+ sessionID: session.id,
+ permission: "bash",
+ patterns: ["/tmp/opencode-e2e-perm-always"],
+ metadata: { description: "Need permission for command" },
+ },
+ undefined,
+ async (state) => {
+ await page.goto(page.url())
+ await expectPermissionBlocked(page)
+
+ await clearPermissionDock(page, /allow always/i)
+ await state.resolved()
+ await page.goto(page.url())
+ await expectPermissionOpen(page)
+ },
+ )
+ },
+ { trackSession: project.trackSession },
+ )
})
})
@@ -479,45 +515,51 @@ test("child session question request blocks parent dock and unblocks after submi
withBackendProject,
}) => {
await withBackendProject(async (project) => {
- await withDockSession(project.sdk, "e2e composer dock child question parent", async (session) => {
- await project.gotoSession(session.id)
-
- const child = await project.sdk.session
- .create({
- title: "e2e composer dock child question",
- parentID: session.id,
- })
- .then((r) => r.data)
- if (!child?.id) throw new Error("Child session create did not return an id")
+ await withDockSession(
+ project.sdk,
+ "e2e composer dock child question parent",
+ async (session) => {
+ await project.gotoSession(session.id)
- try {
- await withDockSeed(project.sdk, child.id, async () => {
- await seedSessionQuestion(project.sdk, {
- sessionID: child.id,
- questions: [
- {
- header: "Child input",
- question: "Pick one child option",
- options: [
- { label: "Continue", description: "Continue child" },
- { label: "Stop", description: "Stop child" },
- ],
- },
- ],
+ const child = await project.sdk.session
+ .create({
+ title: "e2e composer dock child question",
+ parentID: session.id,
})
+ .then((r) => r.data)
+ if (!child?.id) throw new Error("Child session create did not return an id")
+ project.trackSession(child.id)
+
+ try {
+ await withDockSeed(project.sdk, child.id, async () => {
+ await seedSessionQuestion(project.sdk, {
+ sessionID: child.id,
+ questions: [
+ {
+ header: "Child input",
+ question: "Pick one child option",
+ options: [
+ { label: "Continue", description: "Continue child" },
+ { label: "Stop", description: "Stop child" },
+ ],
+ },
+ ],
+ })
- const dock = page.locator(questionDockSelector)
- await expectQuestionBlocked(page)
+ const dock = page.locator(questionDockSelector)
+ await expectQuestionBlocked(page)
- await dock.locator('[data-slot="question-option"]').first().click()
- await dock.getByRole("button", { name: /submit/i }).click()
+ await dock.locator('[data-slot="question-option"]').first().click()
+ await dock.getByRole("button", { name: /submit/i }).click()
- await expectQuestionOpen(page)
- })
- } finally {
- await cleanupSession({ sdk: project.sdk, sessionID: child.id })
- }
- })
+ await expectQuestionOpen(page)
+ })
+ } finally {
+ await cleanupSession({ sdk: project.sdk, sessionID: child.id })
+ }
+ },
+ { trackSession: project.trackSession },
+ )
})
})
@@ -526,102 +568,118 @@ test("child session permission request blocks parent dock and supports allow onc
withBackendProject,
}) => {
await withBackendProject(async (project) => {
- await withDockSession(project.sdk, "e2e composer dock child permission parent", async (session) => {
- await project.gotoSession(session.id)
- await setAutoAccept(page, false)
-
- const child = await project.sdk.session
- .create({
- title: "e2e composer dock child permission",
- parentID: session.id,
- })
- .then((r) => r.data)
- if (!child?.id) throw new Error("Child session create did not return an id")
+ await withDockSession(
+ project.sdk,
+ "e2e composer dock child permission parent",
+ async (session) => {
+ await project.gotoSession(session.id)
+ await setAutoAccept(page, false)
- try {
- await withMockPermission(
- page,
- {
- id: "per_e2e_child",
- sessionID: child.id,
- permission: "bash",
- patterns: ["/tmp/opencode-e2e-perm-child"],
- metadata: { description: "Need child permission" },
- },
- { child },
- async (state) => {
- await page.goto(page.url())
- await expectPermissionBlocked(page)
+ const child = await project.sdk.session
+ .create({
+ title: "e2e composer dock child permission",
+ parentID: session.id,
+ })
+ .then((r) => r.data)
+ if (!child?.id) throw new Error("Child session create did not return an id")
+ project.trackSession(child.id)
- await clearPermissionDock(page, /allow once/i)
- await state.resolved()
- await page.goto(page.url())
+ try {
+ await withMockPermission(
+ page,
+ {
+ id: "per_e2e_child",
+ sessionID: child.id,
+ permission: "bash",
+ patterns: ["/tmp/opencode-e2e-perm-child"],
+ metadata: { description: "Need child permission" },
+ },
+ { child },
+ async (state) => {
+ await page.goto(page.url())
+ await expectPermissionBlocked(page)
- await expectPermissionOpen(page)
- },
- )
- } finally {
- await cleanupSession({ sdk: project.sdk, sessionID: child.id })
- }
- })
+ await clearPermissionDock(page, /allow once/i)
+ await state.resolved()
+ await page.goto(page.url())
+
+ await expectPermissionOpen(page)
+ },
+ )
+ } finally {
+ await cleanupSession({ sdk: project.sdk, sessionID: child.id })
+ }
+ },
+ { trackSession: project.trackSession },
+ )
})
})
test("todo dock transitions and collapse behavior", async ({ page, withBackendProject }) => {
await withBackendProject(async (project) => {
- await withDockSession(project.sdk, "e2e composer dock todo", async (session) => {
- const dock = await todoDock(page, session.id)
- await project.gotoSession(session.id)
- await expect(page.locator(sessionComposerDockSelector)).toBeVisible()
-
- try {
- await dock.open([
- { content: "first task", status: "pending", priority: "high" },
- { content: "second task", status: "in_progress", priority: "medium" },
- ])
- await dock.expectOpen(["pending", "in_progress"])
-
- await dock.collapse()
- await dock.expectCollapsed(["pending", "in_progress"])
-
- await dock.expand()
- await dock.expectOpen(["pending", "in_progress"])
-
- await dock.finish([
- { content: "first task", status: "completed", priority: "high" },
- { content: "second task", status: "cancelled", priority: "medium" },
- ])
- await dock.expectClosed()
- } finally {
- await dock.clear()
- }
- })
+ await withDockSession(
+ project.sdk,
+ "e2e composer dock todo",
+ async (session) => {
+ const dock = await todoDock(page, session.id)
+ await project.gotoSession(session.id)
+ await expect(page.locator(sessionComposerDockSelector)).toBeVisible()
+
+ try {
+ await dock.open([
+ { content: "first task", status: "pending", priority: "high" },
+ { content: "second task", status: "in_progress", priority: "medium" },
+ ])
+ await dock.expectOpen(["pending", "in_progress"])
+
+ await dock.collapse()
+ await dock.expectCollapsed(["pending", "in_progress"])
+
+ await dock.expand()
+ await dock.expectOpen(["pending", "in_progress"])
+
+ await dock.finish([
+ { content: "first task", status: "completed", priority: "high" },
+ { content: "second task", status: "cancelled", priority: "medium" },
+ ])
+ await dock.expectClosed()
+ } finally {
+ await dock.clear()
+ }
+ },
+ { trackSession: project.trackSession },
+ )
})
})
test("keyboard focus stays off prompt while blocked", async ({ page, withBackendProject }) => {
await withBackendProject(async (project) => {
- await withDockSession(project.sdk, "e2e composer dock keyboard", async (session) => {
- await withDockSeed(project.sdk, session.id, async () => {
- await project.gotoSession(session.id)
+ await withDockSession(
+ project.sdk,
+ "e2e composer dock keyboard",
+ async (session) => {
+ await withDockSeed(project.sdk, session.id, async () => {
+ await project.gotoSession(session.id)
- await seedSessionQuestion(project.sdk, {
- sessionID: session.id,
- questions: [
- {
- header: "Need input",
- question: "Pick one option",
- options: [{ label: "Continue", description: "Continue now" }],
- },
- ],
- })
+ await seedSessionQuestion(project.sdk, {
+ sessionID: session.id,
+ questions: [
+ {
+ header: "Need input",
+ question: "Pick one option",
+ options: [{ label: "Continue", description: "Continue now" }],
+ },
+ ],
+ })
- await expectQuestionBlocked(page)
+ await expectQuestionBlocked(page)
- await page.locator("main").click({ position: { x: 5, y: 5 } })
- await page.keyboard.type("abc")
- await expect(page.locator(promptSelector)).toHaveCount(0)
- })
- })
+ await page.locator("main").click({ position: { x: 5, y: 5 } })
+ await page.keyboard.type("abc")
+ await expect(page.locator(promptSelector)).toHaveCount(0)
+ })
+ },
+ { trackSession: project.trackSession },
+ )
})
})
diff --git a/packages/app/e2e/session/session-undo-redo.spec.ts b/packages/app/e2e/session/session-undo-redo.spec.ts
index b3a75e0dd..a63bd9e3b 100644
--- a/packages/app/e2e/session/session-undo-redo.spec.ts
+++ b/packages/app/e2e/session/session-undo-redo.spec.ts
@@ -58,6 +58,7 @@ test("slash undo sets revert and restores prior prompt", async ({ page, withBack
const sdk = project.sdk
await withSession(sdk, `e2e undo ${Date.now()}`, async (session) => {
+ project.trackSession(session.id)
await project.gotoSession(session.id)
const seeded = await seedConversation({ page, sdk, sessionID: session.id, token })
@@ -90,6 +91,7 @@ test("slash redo clears revert and restores latest state", async ({ page, withBa
const sdk = project.sdk
await withSession(sdk, `e2e redo ${Date.now()}`, async (session) => {
+ project.trackSession(session.id)
await project.gotoSession(session.id)
const seeded = await seedConversation({ page, sdk, sessionID: session.id, token })
@@ -138,6 +140,7 @@ test("slash undo/redo traverses multi-step revert stack", async ({ page, withBac
const sdk = project.sdk
await withSession(sdk, `e2e undo redo stack ${Date.now()}`, async (session) => {
+ project.trackSession(session.id)
await project.gotoSession(session.id)
const first = await seedConversation({
diff --git a/packages/app/e2e/session/session.spec.ts b/packages/app/e2e/session/session.spec.ts
index d56e83f2f..6c885460c 100644
--- a/packages/app/e2e/session/session.spec.ts
+++ b/packages/app/e2e/session/session.spec.ts
@@ -38,6 +38,7 @@ test("session can be renamed via header menu", async ({ page, withBackendProject
await withBackendProject(async (project) => {
await withSession(project.sdk, originalTitle, async (session) => {
+ project.trackSession(session.id)
await seedMessage(project.sdk, session.id)
await project.gotoSession(session.id)
await expect(page.getByRole("heading", { level: 1 }).first()).toHaveText(originalTitle)
@@ -73,6 +74,7 @@ test("session can be archived via header menu", async ({ page, withBackendProjec
await withBackendProject(async (project) => {
await withSession(project.sdk, title, async (session) => {
+ project.trackSession(session.id)
await seedMessage(project.sdk, session.id)
await project.gotoSession(session.id)
const menu = await openSessionMoreMenu(page, session.id)
@@ -100,6 +102,7 @@ test("session can be deleted via header menu", async ({ page, withBackendProject
await withBackendProject(async (project) => {
await withSession(project.sdk, title, async (session) => {
+ project.trackSession(session.id)
await seedMessage(project.sdk, session.id)
await project.gotoSession(session.id)
const menu = await openSessionMoreMenu(page, session.id)
@@ -133,6 +136,7 @@ test("session can be shared and unshared via header button", async ({ page, with
await withBackendProject(async (project) => {
await withSession(project.sdk, title, async (session) => {
+ project.trackSession(session.id)
await seedMessage(project.sdk, session.id)
await project.gotoSession(session.id)