summaryrefslogtreecommitdiffhomepage
path: root/packages/app/e2e
diff options
context:
space:
mode:
authorAdam <[email protected]>2026-02-25 19:05:08 -0600
committerGitHub <[email protected]>2026-02-26 01:05:08 +0000
commitb8337cddc4269ba6e72f74c1e1f39aae41f56af3 (patch)
tree178a80a21af1a24f3568a0ef15e735a5d537c16f /packages/app/e2e
parent444178e079fb41ba2149a1cdfdd3040593715d70 (diff)
downloadopencode-b8337cddc4269ba6e72f74c1e1f39aae41f56af3.tar.gz
opencode-b8337cddc4269ba6e72f74c1e1f39aae41f56af3.zip
fix(app): permissions and questions from child sessions (#15105)
Co-authored-by: adamelmore <[email protected]>
Diffstat (limited to 'packages/app/e2e')
-rw-r--r--packages/app/e2e/session/session-composer-dock.spec.ts298
1 files changed, 246 insertions, 52 deletions
diff --git a/packages/app/e2e/session/session-composer-dock.spec.ts b/packages/app/e2e/session/session-composer-dock.spec.ts
index 6bf7714a6..e9cfc03e4 100644
--- a/packages/app/e2e/session/session-composer-dock.spec.ts
+++ b/packages/app/e2e/session/session-composer-dock.spec.ts
@@ -1,5 +1,5 @@
import { test, expect } from "../fixtures"
-import { clearSessionDockSeed, seedSessionPermission, seedSessionQuestion, seedSessionTodos } from "../actions"
+import { clearSessionDockSeed, seedSessionQuestion, seedSessionTodos } from "../actions"
import {
permissionDockSelector,
promptSelector,
@@ -11,11 +11,23 @@ import {
} from "../selectors"
type Sdk = Parameters<typeof clearSessionDockSeed>[0]
-
-async function withDockSession<T>(sdk: Sdk, title: string, fn: (session: { id: string; title: string }) => Promise<T>) {
- const session = await sdk.session.create({ title }).then((r) => r.data)
+type PermissionRule = { permission: string; pattern: string; action: "allow" | "deny" | "ask" }
+
+async function withDockSession<T>(
+ sdk: Sdk,
+ title: string,
+ fn: (session: { id: string; title: string }) => Promise<T>,
+ opts?: { permission?: PermissionRule[] },
+) {
+ 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")
- return fn(session)
+ try {
+ return await fn(session)
+ } finally {
+ await sdk.session.delete({ sessionID: session.id }).catch(() => undefined)
+ }
}
test.setTimeout(120_000)
@@ -28,6 +40,85 @@ async function withDockSeed<T>(sdk: Sdk, sessionID: string, fn: () => Promise<T>
}
}
+async function clearPermissionDock(page: any, label: RegExp) {
+ const dock = page.locator(permissionDockSelector)
+ for (let i = 0; i < 3; i++) {
+ const count = await dock.count()
+ if (count === 0) return
+ await dock.getByRole("button", { name: label }).click()
+ await page.waitForTimeout(150)
+ }
+}
+
+async function withMockPermission<T>(
+ page: any,
+ request: {
+ id: string
+ sessionID: string
+ permission: string
+ patterns: string[]
+ metadata?: Record<string, unknown>
+ always?: string[]
+ },
+ opts: { child?: any } | undefined,
+ fn: () => Promise<T>,
+) {
+ let pending = [
+ {
+ ...request,
+ always: request.always ?? ["*"],
+ metadata: request.metadata ?? {},
+ },
+ ]
+
+ const list = async (route: any) => {
+ await route.fulfill({
+ status: 200,
+ contentType: "application/json",
+ body: JSON.stringify(pending),
+ })
+ }
+
+ const reply = async (route: any) => {
+ const url = new URL(route.request().url())
+ const id = url.pathname.split("/").pop()
+ pending = pending.filter((item) => item.id !== id)
+ await route.fulfill({
+ status: 200,
+ contentType: "application/json",
+ body: JSON.stringify(true),
+ })
+ }
+
+ await page.route("**/permission", list)
+ await page.route("**/session/*/permissions/*", reply)
+
+ const sessionList = opts?.child
+ ? async (route: any) => {
+ const res = await route.fetch()
+ const json = await res.json()
+ const list = Array.isArray(json) ? json : Array.isArray(json?.data) ? json.data : undefined
+ if (Array.isArray(list) && !list.some((item) => item?.id === opts.child?.id)) list.push(opts.child)
+ await route.fulfill({
+ status: res.status(),
+ headers: res.headers(),
+ contentType: "application/json",
+ body: JSON.stringify(json),
+ })
+ }
+ : undefined
+
+ if (sessionList) await page.route("**/session?*", sessionList)
+
+ try {
+ return await fn()
+ } finally {
+ await page.unroute("**/permission", list)
+ await page.unroute("**/session/*/permissions/*", reply)
+ if (sessionList) await page.unroute("**/session?*", sessionList)
+ }
+}
+
test("default dock shows prompt input", async ({ page, sdk, gotoSession }) => {
await withDockSession(sdk, "e2e composer dock default", async (session) => {
await gotoSession(session.id)
@@ -76,72 +167,175 @@ test("blocked question flow unblocks after submit", async ({ page, sdk, gotoSess
test("blocked permission flow supports allow once", async ({ page, sdk, gotoSession }) => {
await withDockSession(sdk, "e2e composer dock permission once", async (session) => {
- await withDockSeed(sdk, session.id, async () => {
- await gotoSession(session.id)
-
- await seedSessionPermission(sdk, {
+ await gotoSession(session.id)
+ await withMockPermission(
+ page,
+ {
+ id: "per_e2e_once",
sessionID: session.id,
permission: "bash",
- patterns: ["README.md"],
- description: "Need permission for command",
- })
-
- await expect.poll(() => page.locator(permissionDockSelector).count(), { timeout: 10_000 }).toBe(1)
- await expect(page.locator(promptSelector)).toHaveCount(0)
-
- await page
- .locator(permissionDockSelector)
- .getByRole("button", { name: /allow once/i })
- .click()
- await expect.poll(() => page.locator(permissionDockSelector).count(), { timeout: 10_000 }).toBe(0)
- await expect(page.locator(promptSelector)).toBeVisible()
- })
+ patterns: ["/tmp/opencode-e2e-perm-once"],
+ metadata: { description: "Need permission for command" },
+ },
+ undefined,
+ async () => {
+ await page.goto(page.url())
+ await expect.poll(() => page.locator(permissionDockSelector).count(), { timeout: 10_000 }).toBe(1)
+ await expect(page.locator(promptSelector)).toHaveCount(0)
+
+ await clearPermissionDock(page, /allow once/i)
+ await page.goto(page.url())
+ await expect.poll(() => page.locator(permissionDockSelector).count(), { timeout: 10_000 }).toBe(0)
+ await expect(page.locator(promptSelector)).toBeVisible()
+ },
+ )
})
})
test("blocked permission flow supports reject", async ({ page, sdk, gotoSession }) => {
await withDockSession(sdk, "e2e composer dock permission reject", async (session) => {
- await withDockSeed(sdk, session.id, async () => {
- await gotoSession(session.id)
-
- await seedSessionPermission(sdk, {
+ await gotoSession(session.id)
+ await withMockPermission(
+ page,
+ {
+ id: "per_e2e_reject",
sessionID: session.id,
permission: "bash",
- patterns: ["REJECT.md"],
- })
-
- await expect.poll(() => page.locator(permissionDockSelector).count(), { timeout: 10_000 }).toBe(1)
- await expect(page.locator(promptSelector)).toHaveCount(0)
-
- await page.locator(permissionDockSelector).getByRole("button", { name: /deny/i }).click()
- await expect.poll(() => page.locator(permissionDockSelector).count(), { timeout: 10_000 }).toBe(0)
- await expect(page.locator(promptSelector)).toBeVisible()
- })
+ patterns: ["/tmp/opencode-e2e-perm-reject"],
+ },
+ undefined,
+ async () => {
+ await page.goto(page.url())
+ await expect.poll(() => page.locator(permissionDockSelector).count(), { timeout: 10_000 }).toBe(1)
+ await expect(page.locator(promptSelector)).toHaveCount(0)
+
+ await clearPermissionDock(page, /deny/i)
+ await page.goto(page.url())
+ await expect.poll(() => page.locator(permissionDockSelector).count(), { timeout: 10_000 }).toBe(0)
+ await expect(page.locator(promptSelector)).toBeVisible()
+ },
+ )
})
})
test("blocked permission flow supports allow always", async ({ page, sdk, gotoSession }) => {
await withDockSession(sdk, "e2e composer dock permission always", async (session) => {
- await withDockSeed(sdk, session.id, async () => {
- await gotoSession(session.id)
-
- await seedSessionPermission(sdk, {
+ await gotoSession(session.id)
+ await withMockPermission(
+ page,
+ {
+ id: "per_e2e_always",
sessionID: session.id,
permission: "bash",
- patterns: ["README.md"],
- description: "Need permission for command",
+ patterns: ["/tmp/opencode-e2e-perm-always"],
+ metadata: { description: "Need permission for command" },
+ },
+ undefined,
+ async () => {
+ await page.goto(page.url())
+ await expect.poll(() => page.locator(permissionDockSelector).count(), { timeout: 10_000 }).toBe(1)
+ await expect(page.locator(promptSelector)).toHaveCount(0)
+
+ await clearPermissionDock(page, /allow always/i)
+ await page.goto(page.url())
+ await expect.poll(() => page.locator(permissionDockSelector).count(), { timeout: 10_000 }).toBe(0)
+ await expect(page.locator(promptSelector)).toBeVisible()
+ },
+ )
+ })
+})
+
+test("child session question request blocks parent dock and unblocks after submit", async ({
+ page,
+ sdk,
+ gotoSession,
+}) => {
+ await withDockSession(sdk, "e2e composer dock child question parent", async (session) => {
+ await gotoSession(session.id)
+
+ const child = await 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(sdk, child.id, async () => {
+ await seedSessionQuestion(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 expect.poll(() => dock.count(), { timeout: 10_000 }).toBe(1)
+ await expect(page.locator(promptSelector)).toHaveCount(0)
+
+ await dock.locator('[data-slot="question-option"]').first().click()
+ await dock.getByRole("button", { name: /submit/i }).click()
+
+ await expect.poll(() => page.locator(questionDockSelector).count(), { timeout: 10_000 }).toBe(0)
+ await expect(page.locator(promptSelector)).toBeVisible()
})
+ } finally {
+ await sdk.session.delete({ sessionID: child.id }).catch(() => undefined)
+ }
+ })
+})
- await expect.poll(() => page.locator(permissionDockSelector).count(), { timeout: 10_000 }).toBe(1)
- await expect(page.locator(promptSelector)).toHaveCount(0)
+test("child session permission request blocks parent dock and supports allow once", async ({
+ page,
+ sdk,
+ gotoSession,
+}) => {
+ await withDockSession(sdk, "e2e composer dock child permission parent", async (session) => {
+ await gotoSession(session.id)
- await page
- .locator(permissionDockSelector)
- .getByRole("button", { name: /allow always/i })
- .click()
- await expect.poll(() => page.locator(permissionDockSelector).count(), { timeout: 10_000 }).toBe(0)
- await expect(page.locator(promptSelector)).toBeVisible()
- })
+ const child = await 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")
+
+ 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 () => {
+ await page.goto(page.url())
+ const dock = page.locator(permissionDockSelector)
+ await expect.poll(() => dock.count(), { timeout: 10_000 }).toBe(1)
+ await expect(page.locator(promptSelector)).toHaveCount(0)
+
+ await clearPermissionDock(page, /allow once/i)
+ await page.goto(page.url())
+
+ await expect.poll(() => page.locator(permissionDockSelector).count(), { timeout: 10_000 }).toBe(0)
+ await expect(page.locator(promptSelector)).toBeVisible()
+ },
+ )
+ } finally {
+ await sdk.session.delete({ sessionID: child.id }).catch(() => undefined)
+ }
})
})