From f3f728ec27b2b2fc67470a2acec0072a5f1badd0 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Wed, 1 Apr 2026 13:43:19 -0400 Subject: [PATCH] test(app): fix isolated backend follow-ups (#20513) --- packages/app/e2e/backend.ts | 15 +- .../app/e2e/prompt/prompt-history.spec.ts | 144 ++-- .../app/e2e/prompt/prompt-slash-share.spec.ts | 1 + .../session/session-child-navigation.spec.ts | 32 +- .../e2e/session/session-composer-dock.spec.ts | 658 ++++++++++-------- .../app/e2e/session/session-undo-redo.spec.ts | 3 + packages/app/e2e/session/session.spec.ts | 4 + 7 files changed, 446 insertions(+), 411 deletions(-) diff --git a/packages/app/e2e/backend.ts b/packages/app/e2e/backend.ts index 22122a3726..4dfa7c64f0 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, timeout = 10_000) { + if (proc.exitCode !== null) return + await Promise.race([ + new Promise((resolve) => proc.once("exit", () => resolve())), + new Promise((resolve) => setTimeout(resolve, timeout)), + ]) +} + const LOG_CAP = 100 function cap(input: string[]) { @@ -62,7 +70,6 @@ export async function startBackend(label: string): Promise { 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 { 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 6420534e02..f2d15914d3 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 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[0]["session"] } }, - sessionID: string, - token: string, -) { - await expect - .poll(() => assistantText(sdk as Parameters[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[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 efb0272b57..5371d8a918 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 616f694a30..fa366e5157 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 9d44683c8b..2c87a309d1 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( sdk: Sdk, title: string, fn: (session: { id: string; title: string }) => Promise, - 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( 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,220 +293,22 @@ 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" }, - ], - }, - ], - }) - - 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 expectQuestionOpen(page) - }) - }) - }) -}) - -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 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) - - await expectQuestionBlocked(page) - await expect(first).toBeFocused() - - await page.keyboard.press("ArrowDown") - await expect(second).toBeFocused() - - await page.keyboard.press("Space") - await page.keyboard.press(`${modKey}+Enter`) - await expectQuestionOpen(page) - }) - }) - }) -}) - -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 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() - - await expectQuestionBlocked(page) - await expect(first).toBeFocused() - - await page.keyboard.press("Escape") - await expectQuestionOpen(page) - }) - }) - }) -}) - -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) - }, - ) - }) - }) -}) - -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) - }, - ) - }) - }) -}) - -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) - }, - ) - }) - }) -}) - -test("child session question request blocks parent dock and unblocks after submit", async ({ - page, - 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") - - try { - await withDockSeed(project.sdk, child.id, async () => { await seedSessionQuestion(project.sdk, { - sessionID: child.id, + sessionID: session.id, questions: [ { - header: "Child input", - question: "Pick one child option", + header: "Need input", + question: "Pick one option", options: [ - { label: "Continue", description: "Continue child" }, - { label: "Stop", description: "Stop child" }, + { label: "Continue", description: "Continue now" }, + { label: "Stop", description: "Stop here" }, ], }, ], @@ -514,10 +322,244 @@ test("child session question request blocks parent dock and unblocks after submi await expectQuestionOpen(page) }) - } finally { - await cleanupSession({ sdk: project.sdk, sessionID: child.id }) - } - }) + }, + { 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 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) + + await expectQuestionBlocked(page) + await expect(first).toBeFocused() + + await page.keyboard.press("ArrowDown") + await expect(second).toBeFocused() + + 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 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() + + await expectQuestionBlocked(page) + await expect(first).toBeFocused() + + 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) + }, + ) + }, + { 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) + }, + ) + }, + { 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) + }, + ) + }, + { trackSession: project.trackSession }, + ) + }) +}) + +test("child session question request blocks parent dock and unblocks after submit", async ({ + page, + 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") + 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) + + 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 }) + } + }, + { 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) + 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") + 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) - 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) + 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 clearPermissionDock(page, /allow once/i) - await state.resolved() - await page.goto(page.url()) + 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 }) - } - }) + 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() + 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"]) + 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.collapse() + await dock.expectCollapsed(["pending", "in_progress"]) - await dock.expand() - await dock.expectOpen(["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 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 page.locator("main").click({ position: { x: 5, y: 5 } }) + await page.keyboard.type("abc") + await expect(page.locator(promptSelector)).toHaveCount(0) }) - - 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) - }) - }) + }, + { 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 b3a75e0dd1..a63bd9e3b5 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 d56e83f2fe..6c885460c4 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)