|
|
@@ -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)
|
|
|
+ }
|
|
|
})
|
|
|
})
|
|
|
|