summaryrefslogtreecommitdiffhomepage
path: root/packages/app/e2e/session
diff options
context:
space:
mode:
authorDax Raad <[email protected]>2026-04-14 23:10:07 -0400
committerDax Raad <[email protected]>2026-04-14 23:10:25 -0400
commit627159acac04409d7697a6739e2c572c2a010943 (patch)
tree5f87465ea69f41aff0cd96ae5411fe438da480b3 /packages/app/e2e/session
parentf44aa02e2677b2b89a1a9f517c0ff8990383deaa (diff)
downloadopencode-627159acac04409d7697a6739e2c572c2a010943.tar.gz
opencode-627159acac04409d7697a6739e2c572c2a010943.zip
delete all e2e tests (#22501)
Cherry-picked from ea463e604cdd2a3e83e1c286e39b789455f0d413
Diffstat (limited to 'packages/app/e2e/session')
-rw-r--r--packages/app/e2e/session/session-child-navigation.spec.ts64
-rw-r--r--packages/app/e2e/session/session-composer-dock.spec.ts655
-rw-r--r--packages/app/e2e/session/session-model-persistence.spec.ts362
-rw-r--r--packages/app/e2e/session/session-review.spec.ts440
-rw-r--r--packages/app/e2e/session/session-undo-redo.spec.ts233
-rw-r--r--packages/app/e2e/session/session.spec.ts182
6 files changed, 0 insertions, 1936 deletions
diff --git a/packages/app/e2e/session/session-child-navigation.spec.ts b/packages/app/e2e/session/session-child-navigation.spec.ts
deleted file mode 100644
index c9fad1af8..000000000
--- a/packages/app/e2e/session/session-child-navigation.spec.ts
+++ /dev/null
@@ -1,64 +0,0 @@
-import { seedSessionTask, withSession } from "../actions"
-import { test, expect } from "../fixtures"
-import { inputMatch } from "../prompt/mock"
-
-test("task tool child-session link does not trigger stale show errors", async ({ page, llm, project }) => {
- test.setTimeout(120_000)
-
- const errs: string[] = []
- const onError = (err: Error) => {
- errs.push(err.message)
- }
- page.on("pageerror", onError)
-
- try {
- await project.open()
- await withSession(project.sdk, `e2e child nav ${Date.now()}`, async (session) => {
- const taskInput = {
- description: "Open child session",
- prompt: "Search the repository for AssistantParts and then reply with exactly CHILD_OK.",
- subagent_type: "general",
- }
- await llm.toolMatch(inputMatch(taskInput), "task", taskInput)
- const child = await seedSessionTask(project.sdk, {
- sessionID: session.id,
- description: taskInput.description,
- prompt: taskInput.prompt,
- })
- project.trackSession(child.sessionID)
-
- await project.gotoSession(session.id)
-
- const header = page.locator("[data-session-title]")
- await expect(header.getByRole("button", { name: "More options" })).toBeVisible({ timeout: 30_000 })
-
- const card = page
- .locator('[data-component="task-tool-card"]')
- .filter({ hasText: /open child session/i })
- .first()
- await expect(card).toBeVisible({ timeout: 30_000 })
- await card.click()
-
- await expect(page).toHaveURL(new RegExp(`/session/${child.sessionID}(?:[/?#]|$)`), { timeout: 30_000 })
- await expect(header.locator('[data-slot="session-title-parent"]')).toHaveText(session.title)
- await expect(header.locator('[data-slot="session-title-child"]')).toHaveText(taskInput.description)
- await expect(header.locator('[data-slot="session-title-separator"]')).toHaveText("/")
- await expect
- .poll(
- () =>
- header.locator('[data-slot="session-title-separator"]').evaluate((el) => ({
- left: getComputedStyle(el).paddingLeft,
- right: getComputedStyle(el).paddingRight,
- })),
- { timeout: 30_000 },
- )
- .toEqual({ left: "8px", right: "8px" })
- await expect(header.getByRole("button", { name: "More options" })).toHaveCount(0)
- await expect(page.getByText("Subagent sessions cannot be prompted.")).toBeVisible({ timeout: 30_000 })
- await expect(page.getByRole("button", { name: "Back to main session." })).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
deleted file mode 100644
index ecacea83d..000000000
--- a/packages/app/e2e/session/session-composer-dock.spec.ts
+++ /dev/null
@@ -1,655 +0,0 @@
-import { test, expect } from "../fixtures"
-import {
- composerEvent,
- type ComposerDriverState,
- type ComposerProbeState,
- type ComposerWindow,
-} from "../../src/testing/session-composer"
-import { cleanupSession, clearSessionDockSeed, closeDialog, openSettings, seedSessionQuestion } from "../actions"
-import {
- permissionDockSelector,
- promptSelector,
- questionDockSelector,
- sessionComposerDockSelector,
- sessionTodoToggleButtonSelector,
-} from "../selectors"
-import { modKey } from "../utils"
-import { inputMatch } from "../prompt/mock"
-
-type Sdk = Parameters<typeof clearSessionDockSeed>[0]
-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[]; 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 {
- await cleanupSession({ sdk, sessionID: session.id })
- }
-}
-
-const defaultQuestions = [
- {
- header: "Need input",
- question: "Pick one option",
- options: [
- { label: "Continue", description: "Continue now" },
- { label: "Stop", description: "Stop here" },
- ],
- },
-]
-
-test.setTimeout(120_000)
-
-async function withDockSeed<T>(sdk: Sdk, sessionID: string, fn: () => Promise<T>) {
- try {
- return await fn()
- } finally {
- await clearSessionDockSeed(sdk, sessionID).catch(() => undefined)
- }
-}
-
-async function clearPermissionDock(page: any, label: RegExp) {
- const dock = page.locator(permissionDockSelector)
- await expect(dock).toBeVisible()
- await dock.getByRole("button", { name: label }).click()
-}
-
-async function setAutoAccept(page: any, enabled: boolean) {
- const dialog = await openSettings(page)
- const toggle = dialog.locator('[data-action="settings-auto-accept-permissions"]').first()
- const input = toggle.locator('[data-slot="switch-input"]').first()
- await expect(toggle).toBeVisible()
- const checked = (await input.getAttribute("aria-checked")) === "true"
- if (checked !== enabled) await toggle.locator('[data-slot="switch-control"]').click()
- await expect(input).toHaveAttribute("aria-checked", enabled ? "true" : "false")
- await closeDialog(page, dialog)
-}
-
-async function expectQuestionBlocked(page: any) {
- await expect(page.locator(questionDockSelector)).toBeVisible()
- await expect(page.locator(promptSelector)).toHaveCount(0)
-}
-
-async function expectQuestionOpen(page: any) {
- await expect(page.locator(questionDockSelector)).toHaveCount(0)
- await expect(page.locator(promptSelector)).toBeVisible()
-}
-
-async function expectPermissionBlocked(page: any) {
- await expect(page.locator(permissionDockSelector)).toBeVisible()
- await expect(page.locator(promptSelector)).toHaveCount(0)
-}
-
-async function expectPermissionOpen(page: any) {
- await expect(page.locator(permissionDockSelector)).toHaveCount(0)
- await expect(page.locator(promptSelector)).toBeVisible()
-}
-
-async function todoDock(page: any, sessionID: string) {
- await page.addInitScript(() => {
- const win = window as ComposerWindow
- win.__opencode_e2e = {
- ...win.__opencode_e2e,
- composer: {
- enabled: true,
- sessions: {},
- },
- }
- })
-
- const write = async (driver: ComposerDriverState | undefined) => {
- await page.evaluate(
- (input: { event: string; sessionID: string; driver: ComposerDriverState | undefined }) => {
- const win = window as ComposerWindow
- const composer = win.__opencode_e2e?.composer
- if (!composer?.enabled) throw new Error("Composer e2e driver is not enabled")
- composer.sessions ??= {}
- const prev = composer.sessions[input.sessionID] ?? {}
- if (!input.driver) {
- if (!prev.probe) {
- delete composer.sessions[input.sessionID]
- } else {
- composer.sessions[input.sessionID] = { probe: prev.probe }
- }
- } else {
- composer.sessions[input.sessionID] = {
- ...prev,
- driver: input.driver,
- }
- }
- window.dispatchEvent(new CustomEvent(input.event, { detail: { sessionID: input.sessionID } }))
- },
- { event: composerEvent, sessionID, driver },
- )
- }
-
- const read = () =>
- page.evaluate((sessionID: string) => {
- const win = window as ComposerWindow
- return win.__opencode_e2e?.composer?.sessions?.[sessionID]?.probe ?? null
- }, sessionID) as Promise<ComposerProbeState | null>
-
- const api = {
- async clear() {
- await write(undefined)
- return api
- },
- async open(todos: NonNullable<ComposerDriverState["todos"]>) {
- await write({ live: true, todos })
- return api
- },
- async finish(todos: NonNullable<ComposerDriverState["todos"]>) {
- await write({ live: false, todos })
- return api
- },
- async expectOpen(states: ComposerProbeState["states"]) {
- await expect.poll(read, { timeout: 10_000 }).toMatchObject({
- mounted: true,
- collapsed: false,
- hidden: false,
- count: states.length,
- states,
- })
- return api
- },
- async expectCollapsed(states: ComposerProbeState["states"]) {
- await expect.poll(read, { timeout: 10_000 }).toMatchObject({
- mounted: true,
- collapsed: true,
- hidden: true,
- count: states.length,
- states,
- })
- return api
- },
- async expectClosed() {
- await expect.poll(read, { timeout: 10_000 }).toMatchObject({ mounted: false })
- return api
- },
- async collapse() {
- await page.locator(sessionTodoToggleButtonSelector).click()
- return api
- },
- async expand() {
- await page.locator(sessionTodoToggleButtonSelector).click()
- return api
- },
- }
-
- return api
-}
-
-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: (state: { resolved: () => Promise<void> }) => Promise<T>,
-) {
- const listUrl = /\/permission(?:\?.*)?$/
- const replyUrls = [/\/session\/[^/]+\/permissions\/[^/?]+(?:\?.*)?$/, /\/permission\/[^/]+\/reply(?:\?.*)?$/]
- 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 parts = url.pathname.split("/").filter(Boolean)
- const id = parts.at(-1) === "reply" ? parts.at(-2) : parts.at(-1)
- pending = pending.filter((item) => item.id !== id)
- await route.fulfill({
- status: 200,
- contentType: "application/json",
- body: JSON.stringify(true),
- })
- }
-
- await page.route(listUrl, list)
- for (const item of replyUrls) {
- await page.route(item, 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({
- response: res,
- body: JSON.stringify(json),
- })
- }
- : undefined
-
- if (sessionList) await page.route("**/session?*", sessionList)
-
- const state = {
- async resolved() {
- await expect.poll(() => pending.length, { timeout: 10_000 }).toBe(0)
- },
- }
-
- try {
- return await fn(state)
- } finally {
- await page.unroute(listUrl, list)
- for (const item of replyUrls) {
- await page.unroute(item, reply)
- }
- if (sessionList) await page.unroute("**/session?*", sessionList)
- }
-}
-
-test("default dock shows prompt input", async ({ page, project }) => {
- await project.open()
- 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('[data-action="prompt-permissions"]')).toHaveCount(0)
- 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()
- },
- { trackSession: project.trackSession },
- )
-})
-
-test("auto-accept toggle works before first submit", async ({ page, project }) => {
- await project.open()
-
- await setAutoAccept(page, true)
- await setAutoAccept(page, false)
-})
-
-test("blocked question flow unblocks after submit", async ({ page, llm, project }) => {
- await project.open()
- await withDockSession(
- project.sdk,
- "e2e composer dock question",
- async (session) => {
- await withDockSeed(project.sdk, session.id, async () => {
- await project.gotoSession(session.id)
-
- await llm.toolMatch(inputMatch({ questions: defaultQuestions }), "question", { questions: defaultQuestions })
- await seedSessionQuestion(project.sdk, {
- sessionID: session.id,
- questions: defaultQuestions,
- })
-
- 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)
- })
- },
- { trackSession: project.trackSession },
- )
-})
-
-test("blocked question flow supports keyboard shortcuts", async ({ page, llm, project }) => {
- await project.open()
- await withDockSession(
- project.sdk,
- "e2e composer dock question keyboard",
- async (session) => {
- await withDockSeed(project.sdk, session.id, async () => {
- await project.gotoSession(session.id)
-
- await llm.toolMatch(inputMatch({ questions: defaultQuestions }), "question", { questions: defaultQuestions })
- await seedSessionQuestion(project.sdk, {
- sessionID: session.id,
- questions: defaultQuestions,
- })
-
- 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, llm, project }) => {
- await project.open()
- await withDockSession(
- project.sdk,
- "e2e composer dock question escape",
- async (session) => {
- await withDockSeed(project.sdk, session.id, async () => {
- await project.gotoSession(session.id)
-
- await llm.toolMatch(inputMatch({ questions: defaultQuestions }), "question", { questions: defaultQuestions })
- await seedSessionQuestion(project.sdk, {
- sessionID: session.id,
- questions: defaultQuestions,
- })
-
- 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, project }) => {
- await project.open()
- 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, project }) => {
- await project.open()
- 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, project }) => {
- await project.open()
- 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, llm, project }) => {
- const questions = [
- {
- header: "Child input",
- question: "Pick one child option",
- options: [
- { label: "Continue", description: "Continue child" },
- { label: "Stop", description: "Stop child" },
- ],
- },
- ]
- await project.open()
- 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 llm.toolMatch(inputMatch({ questions }), "question", { questions })
- await seedSessionQuestion(project.sdk, {
- sessionID: child.id,
- questions,
- })
-
- 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 },
- )
-})
-
-test("child session permission request blocks parent dock and supports allow once", async ({ page, project }) => {
- await project.open()
- 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")
- 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)
-
- 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, project }) => {
- await project.open()
- 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, llm, project }) => {
- const questions = [
- {
- header: "Need input",
- question: "Pick one option",
- options: [{ label: "Continue", description: "Continue now" }],
- },
- ]
- await project.open()
- await withDockSession(
- project.sdk,
- "e2e composer dock keyboard",
- async (session) => {
- await withDockSeed(project.sdk, session.id, async () => {
- await project.gotoSession(session.id)
-
- await llm.toolMatch(inputMatch({ questions }), "question", { questions })
- await seedSessionQuestion(project.sdk, {
- sessionID: session.id,
- questions,
- })
-
- 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-model-persistence.spec.ts b/packages/app/e2e/session/session-model-persistence.spec.ts
deleted file mode 100644
index c107cc518..000000000
--- a/packages/app/e2e/session/session-model-persistence.spec.ts
+++ /dev/null
@@ -1,362 +0,0 @@
-import type { Locator, Page } from "@playwright/test"
-import { test, expect } from "../fixtures"
-import { openSidebar, resolveSlug, setWorkspacesEnabled, waitSession, waitSlug } from "../actions"
-import {
- promptAgentSelector,
- promptModelSelector,
- promptVariantSelector,
- workspaceItemSelector,
- workspaceNewSessionSelector,
-} from "../selectors"
-import { createSdk, sessionPath } from "../utils"
-
-type Footer = {
- agent: string
- model: string
- variant: string
-}
-
-type Probe = {
- dir?: string
- sessionID?: string
- agent?: string
- model?: { providerID: string; modelID: string; name?: string }
- variant?: string | null
- pick?: {
- agent?: string
- model?: { providerID: string; modelID: string }
- variant?: string | null
- }
- variants?: string[]
- models?: Array<{ providerID: string; modelID: string; name: string }>
- agents?: Array<{ name: string }>
-}
-
-const escape = (value: string) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
-
-const text = async (locator: Locator) => ((await locator.textContent()) ?? "").trim()
-
-const modelKey = (state: Probe | null) => (state?.model ? `${state.model.providerID}:${state.model.modelID}` : null)
-
-async function probe(page: Page): Promise<Probe | null> {
- return page.evaluate(() => {
- const win = window as Window & {
- __opencode_e2e?: {
- model?: {
- current?: Probe
- }
- }
- }
- return win.__opencode_e2e?.model?.current ?? null
- })
-}
-
-async function currentModel(page: Page) {
- await expect.poll(() => probe(page).then(modelKey), { timeout: 30_000 }).not.toBe(null)
- const value = await probe(page).then(modelKey)
- if (!value) throw new Error("Failed to resolve current model key")
- return value
-}
-
-async function waitControl(page: Page, key: "setAgent" | "setModel" | "setVariant") {
- await expect
- .poll(
- () =>
- page.evaluate((key) => {
- const win = window as Window & {
- __opencode_e2e?: {
- model?: {
- controls?: Record<string, unknown>
- }
- }
- }
- return !!win.__opencode_e2e?.model?.controls?.[key]
- }, key),
- { timeout: 30_000 },
- )
- .toBe(true)
-}
-
-async function pickAgent(page: Page, value: string) {
- await waitControl(page, "setAgent")
- await page.evaluate((value) => {
- const win = window as Window & {
- __opencode_e2e?: {
- model?: {
- controls?: {
- setAgent?: (value: string | undefined) => void
- }
- }
- }
- }
- const fn = win.__opencode_e2e?.model?.controls?.setAgent
- if (!fn) throw new Error("Model e2e agent control is not enabled")
- fn(value)
- }, value)
-}
-
-async function pickModel(page: Page, value: { providerID: string; modelID: string }) {
- await waitControl(page, "setModel")
- await page.evaluate((value) => {
- const win = window as Window & {
- __opencode_e2e?: {
- model?: {
- controls?: {
- setModel?: (value: { providerID: string; modelID: string } | undefined) => void
- }
- }
- }
- }
- const fn = win.__opencode_e2e?.model?.controls?.setModel
- if (!fn) throw new Error("Model e2e model control is not enabled")
- fn(value)
- }, value)
-}
-
-async function pickVariant(page: Page, value: string) {
- await waitControl(page, "setVariant")
- await page.evaluate((value) => {
- const win = window as Window & {
- __opencode_e2e?: {
- model?: {
- controls?: {
- setVariant?: (value: string | undefined) => void
- }
- }
- }
- }
- const fn = win.__opencode_e2e?.model?.controls?.setVariant
- if (!fn) throw new Error("Model e2e variant control is not enabled")
- fn(value)
- }, value)
-}
-
-async function read(page: Page): Promise<Footer> {
- return {
- agent: await text(page.locator(`${promptAgentSelector} [data-slot="select-select-trigger-value"]`).first()),
- model: await text(page.locator(`${promptModelSelector} [data-action="prompt-model"] span`).first()),
- variant: await text(page.locator(`${promptVariantSelector} [data-slot="select-select-trigger-value"]`).first()),
- }
-}
-
-async function waitFooter(page: Page, expected: Partial<Footer>) {
- let hit: Footer | null = null
- await expect
- .poll(
- async () => {
- const state = await read(page)
- const ok = Object.entries(expected).every(([key, value]) => state[key as keyof Footer] === value)
- if (ok) hit = state
- return ok
- },
- { timeout: 30_000 },
- )
- .toBe(true)
- if (!hit) throw new Error("Failed to resolve prompt footer state")
- return hit
-}
-
-async function waitModel(page: Page, value: string) {
- await expect.poll(() => probe(page).then(modelKey), { timeout: 30_000 }).toBe(value)
-}
-
-async function choose(page: Page, root: string, value: string) {
- const select = page.locator(root)
- await expect(select).toBeVisible()
- await pickAgent(page, value)
-}
-
-async function variantCount(page: Page) {
- return (await probe(page))?.variants?.length ?? 0
-}
-
-async function agents(page: Page) {
- return ((await probe(page))?.agents ?? []).map((item) => item.name).filter(Boolean)
-}
-
-async function ensureVariant(page: Page, directory: string): Promise<Footer> {
- const current = await read(page)
- if ((await variantCount(page)) >= 2) return current
-
- const cfg = await createSdk(directory)
- .config.get()
- .then((x) => x.data)
- const visible = new Set(await agents(page))
- const entry = Object.entries(cfg?.agent ?? {}).find((item) => {
- const value = item[1]
- return !!value && typeof value === "object" && "variant" in value && "model" in value && visible.has(item[0])
- })
- const name = entry?.[0]
- test.skip(!name, "no agent with alternate variants available")
- if (!name) return current
-
- await choose(page, promptAgentSelector, name)
- await expect.poll(() => variantCount(page), { timeout: 30_000 }).toBeGreaterThanOrEqual(2)
- return waitFooter(page, { agent: name })
-}
-
-async function chooseDifferentVariant(page: Page): Promise<Footer> {
- const current = await read(page)
- const next = (await probe(page))?.variants?.find((item) => item !== current.variant)
- if (!next) throw new Error("Current model has no alternate variant to select")
-
- await pickVariant(page, next)
- return waitFooter(page, { agent: current.agent, model: current.model, variant: next })
-}
-
-async function chooseOtherModel(page: Page, skip: string[] = []): Promise<Footer> {
- const current = await currentModel(page)
- const next = (await probe(page))?.models?.find((item) => {
- const key = `${item.providerID}:${item.modelID}`
- return key !== current && !skip.includes(key)
- })
- if (!next) throw new Error("Failed to choose a different model")
- await pickModel(page, { providerID: next.providerID, modelID: next.modelID })
- await expect.poll(async () => (await read(page)).model, { timeout: 30_000 }).toBe(next.name)
- return read(page)
-}
-
-async function goto(page: Page, directory: string, sessionID?: string) {
- await page.goto(sessionPath(directory, sessionID))
- await waitSession(page, { directory, sessionID })
-}
-
-async function submit(project: Parameters<typeof test>[0]["project"], value: string) {
- return project.prompt(value)
-}
-
-async function createWorkspace(page: Page, root: string, seen: string[]) {
- await openSidebar(page)
- await page.getByRole("button", { name: "New workspace" }).first().click()
-
- const next = await resolveSlug(await waitSlug(page, [root, ...seen]))
- await waitSession(page, { directory: next.directory })
- return next
-}
-
-async function waitWorkspace(page: Page, slug: string) {
- await openSidebar(page)
- await expect
- .poll(
- async () => {
- const item = page.locator(workspaceItemSelector(slug)).first()
- try {
- await item.hover({ timeout: 500 })
- return true
- } catch {
- return false
- }
- },
- { timeout: 60_000 },
- )
- .toBe(true)
-}
-
-async function newWorkspaceSession(page: Page, slug: string) {
- await waitWorkspace(page, slug)
- const item = page.locator(workspaceItemSelector(slug)).first()
- await item.hover()
-
- const button = page.locator(workspaceNewSessionSelector(slug)).first()
- await expect(button).toBeVisible()
- await button.click({ force: true })
-
- const next = await resolveSlug(await waitSlug(page))
- return waitSession(page, { directory: next.directory }).then((item) => item.directory)
-}
-
-test("session model restore per session without leaking into new sessions", async ({ page, project }) => {
- await page.setViewportSize({ width: 1440, height: 900 })
-
- await project.open()
- await project.gotoSession()
-
- const firstState = await chooseOtherModel(page)
- const firstKey = await currentModel(page)
- const first = await submit(project, `session variant ${Date.now()}`)
-
- await page.reload()
- await waitSession(page, { directory: project.directory, sessionID: first })
- await waitFooter(page, firstState)
-
- await project.gotoSession()
- const fresh = await read(page)
- expect(fresh.model).not.toBe(firstState.model)
-
- const secondState = await chooseOtherModel(page, [firstKey])
- const second = await submit(project, `session model ${Date.now()}`)
-
- await goto(page, project.directory, first)
- await waitFooter(page, firstState)
-
- await goto(page, project.directory, second)
- await waitFooter(page, secondState)
-
- await project.gotoSession()
- await page.reload()
- await waitSession(page, { directory: project.directory })
- await waitFooter(page, fresh)
-})
-
-test("session model restore across workspaces", async ({ page, project }) => {
- await page.setViewportSize({ width: 1440, height: 900 })
-
- await project.open()
- const root = project.directory
- await project.gotoSession()
-
- const firstState = await chooseOtherModel(page)
- const firstKey = await currentModel(page)
- const first = await submit(project, `root session ${Date.now()}`)
-
- await openSidebar(page)
- await setWorkspacesEnabled(page, project.slug, true)
-
- const one = await createWorkspace(page, project.slug, [])
- const oneDir = await newWorkspaceSession(page, one.slug)
- project.trackDirectory(oneDir)
-
- const secondState = await chooseOtherModel(page, [firstKey])
- const secondKey = await currentModel(page)
- const second = await submit(project, `workspace one ${Date.now()}`)
-
- const two = await createWorkspace(page, project.slug, [one.slug])
- const twoDir = await newWorkspaceSession(page, two.slug)
- project.trackDirectory(twoDir)
-
- const thirdState = await chooseOtherModel(page, [firstKey, secondKey])
- const third = await submit(project, `workspace two ${Date.now()}`)
-
- await goto(page, root, first)
- await waitFooter(page, firstState)
-
- await goto(page, oneDir, second)
- await waitFooter(page, secondState)
-
- await goto(page, twoDir, third)
- await waitFooter(page, thirdState)
-
- await goto(page, root, first)
- await waitFooter(page, firstState)
-})
-
-test("variant preserved when switching agent modes", async ({ page, project }) => {
- await page.setViewportSize({ width: 1440, height: 900 })
-
- await project.open()
- await project.gotoSession()
-
- await ensureVariant(page, project.directory)
- const updated = await chooseDifferentVariant(page)
-
- const available = await agents(page)
- const other = available.find((name) => name !== updated.agent)
- test.skip(!other, "only one agent available")
- if (!other) return
-
- await choose(page, promptAgentSelector, other)
- await waitFooter(page, { agent: other, variant: updated.variant })
-
- await choose(page, promptAgentSelector, updated.agent)
- await waitFooter(page, { agent: updated.agent, variant: updated.variant })
-})
diff --git a/packages/app/e2e/session/session-review.spec.ts b/packages/app/e2e/session/session-review.spec.ts
deleted file mode 100644
index c0a98cb2e..000000000
--- a/packages/app/e2e/session/session-review.spec.ts
+++ /dev/null
@@ -1,440 +0,0 @@
-import { waitSessionIdle, withSession } from "../actions"
-import { test, expect } from "../fixtures"
-import { bodyText } from "../prompt/mock"
-
-const count = 14
-
-function body(mark: string) {
- return [
- `title ${mark}`,
- `mark ${mark}`,
- ...Array.from({ length: 32 }, (_, i) => `line ${String(i + 1).padStart(2, "0")} ${mark}`),
- ]
-}
-
-function files(tag: string) {
- return Array.from({ length: count }, (_, i) => {
- const id = String(i).padStart(2, "0")
- return {
- file: `review-scroll-${id}.txt`,
- mark: `${tag}-${id}`,
- }
- })
-}
-
-function seed(list: ReturnType<typeof files>) {
- const out = ["*** Begin Patch"]
-
- for (const item of list) {
- out.push(`*** Add File: ${item.file}`)
- for (const line of body(item.mark)) out.push(`+${line}`)
- }
-
- out.push("*** End Patch")
- return out.join("\n")
-}
-
-function edit(file: string, prev: string, next: string) {
- return ["*** Begin Patch", `*** Update File: ${file}`, "@@", `-mark ${prev}`, `+mark ${next}`, "*** End Patch"].join(
- "\n",
- )
-}
-
-async function patchWithMock(
- llm: Parameters<typeof test>[0]["llm"],
- sdk: Parameters<typeof withSession>[0],
- sessionID: string,
- patchText: string,
-) {
- const callsBefore = await llm.calls()
- await llm.toolMatch(
- (hit) => bodyText(hit).includes("Your only valid response is one apply_patch tool call."),
- "apply_patch",
- { patchText },
- )
- await sdk.session.prompt({
- sessionID,
- agent: "build",
- system: [
- "You are seeding deterministic e2e UI state.",
- "Your only valid response is one apply_patch tool call.",
- `Use this JSON input: ${JSON.stringify({ patchText })}`,
- "Do not call any other tools.",
- "Do not output plain text.",
- ].join("\n"),
- parts: [{ type: "text", text: "Apply the provided patch exactly once." }],
- })
-
- await expect.poll(() => llm.calls().then((c) => c > callsBefore), { timeout: 30_000 }).toBe(true)
- await expect
- .poll(
- async () => {
- const diff = await sdk.session.diff({ sessionID }).then((res) => res.data ?? [])
- return diff.length
- },
- { timeout: 120_000 },
- )
- .toBeGreaterThan(0)
-}
-
-async function show(page: Parameters<typeof test>[0]["page"]) {
- const btn = page.getByRole("button", { name: "Toggle review" }).first()
- await expect(btn).toBeVisible()
- if ((await btn.getAttribute("aria-expanded")) !== "true") await btn.click()
- await expect(btn).toHaveAttribute("aria-expanded", "true")
-}
-
-async function expand(page: Parameters<typeof test>[0]["page"]) {
- const close = page.getByRole("button", { name: /^Collapse all$/i }).first()
- const open = await close
- .isVisible()
- .then((value) => value)
- .catch(() => false)
-
- const btn = page.getByRole("button", { name: /^Expand all$/i }).first()
- if (open) {
- await close.click()
- await expect(btn).toBeVisible()
- }
-
- await expect(btn).toBeVisible()
- await btn.click()
- await expect(close).toBeVisible()
-}
-
-async function waitMark(page: Parameters<typeof test>[0]["page"], file: string, mark: string) {
- await page.waitForFunction(
- ({ file, mark }) => {
- const view = document.querySelector('[data-slot="session-review-scroll"] .scroll-view__viewport')
- if (!(view instanceof HTMLElement)) return false
-
- const head = Array.from(view.querySelectorAll("h3")).find(
- (node) => node instanceof HTMLElement && node.textContent?.includes(file),
- )
- if (!(head instanceof HTMLElement)) return false
-
- return Array.from(head.parentElement?.querySelectorAll("diffs-container") ?? []).some((host) => {
- if (!(host instanceof HTMLElement)) return false
- const root = host.shadowRoot
- return root?.textContent?.includes(`mark ${mark}`) ?? false
- })
- },
- { file, mark },
- { timeout: 60_000 },
- )
-}
-
-async function spot(page: Parameters<typeof test>[0]["page"], file: string) {
- return page.evaluate((file) => {
- const view = document.querySelector('[data-slot="session-review-scroll"] .scroll-view__viewport')
- if (!(view instanceof HTMLElement)) return null
-
- const row = Array.from(view.querySelectorAll("h3")).find(
- (node) => node instanceof HTMLElement && node.textContent?.includes(file),
- )
- if (!(row instanceof HTMLElement)) return null
-
- const a = row.getBoundingClientRect()
- const b = view.getBoundingClientRect()
- return {
- top: a.top - b.top,
- y: view.scrollTop,
- }
- }, file)
-}
-
-async function comment(page: Parameters<typeof test>[0]["page"], file: string, note: string) {
- const row = page.locator(`[data-file="${file}"]`).first()
- await expect(row).toBeVisible()
-
- const line = row.locator('diffs-container [data-line="2"]').first()
- await expect(line).toBeVisible()
- await line.hover()
-
- const add = row.getByRole("button", { name: /^Comment$/ }).first()
- await expect(add).toBeVisible()
- await add.click()
-
- const area = row.locator('[data-slot="line-comment-textarea"]').first()
- await expect(area).toBeVisible()
- await area.fill(note)
-
- const submit = row.locator('[data-slot="line-comment-action"][data-variant="primary"]').first()
- await expect(submit).toBeEnabled()
- await submit.click()
-
- await expect(row.locator('[data-slot="line-comment-content"]').filter({ hasText: note }).first()).toBeVisible()
- await expect(row.locator('[data-slot="line-comment-tools"]').first()).toBeVisible()
-}
-
-async function overflow(page: Parameters<typeof test>[0]["page"], file: string) {
- const row = page.locator(`[data-file="${file}"]`).first()
- const view = page.locator('[data-slot="session-review-scroll"] .scroll-view__viewport').first()
- const pop = row.locator('[data-slot="line-comment-popover"][data-inline-body]').first()
- const tools = row.locator('[data-slot="line-comment-tools"]').first()
-
- const [width, viewBox, popBox, toolsBox] = await Promise.all([
- view.evaluate((el) => el.scrollWidth - el.clientWidth),
- view.boundingBox(),
- pop.boundingBox(),
- tools.boundingBox(),
- ])
-
- if (!viewBox || !popBox || !toolsBox) return null
-
- return {
- width,
- pop: popBox.x + popBox.width - (viewBox.x + viewBox.width),
- tools: toolsBox.x + toolsBox.width - (viewBox.x + viewBox.width),
- }
-}
-
-async function openReviewFile(page: Parameters<typeof test>[0]["page"], file: string) {
- const row = page.locator(`[data-file="${file}"]`).first()
- await expect(row).toBeVisible()
- await row.hover()
-
- const open = row.getByRole("button", { name: /^Open file$/i }).first()
- await expect(open).toBeVisible()
- await open.click()
-
- const tab = page.getByRole("tab", { name: file }).first()
- await expect(tab).toBeVisible()
- await tab.click()
-
- const viewer = page.locator('[data-component="file"][data-mode="text"]').first()
- await expect(viewer).toBeVisible()
- return viewer
-}
-
-async function fileComment(page: Parameters<typeof test>[0]["page"], note: string) {
- const viewer = page.locator('[data-component="file"][data-mode="text"]').first()
- await expect(viewer).toBeVisible()
-
- const line = viewer.locator('diffs-container [data-line="2"]').first()
- await expect(line).toBeVisible()
- await line.hover()
-
- const add = viewer.getByRole("button", { name: /^Comment$/ }).first()
- await expect(add).toBeVisible()
- await add.click()
-
- const area = viewer.locator('[data-slot="line-comment-textarea"]').first()
- await expect(area).toBeVisible()
- await area.fill(note)
-
- const submit = viewer.locator('[data-slot="line-comment-action"][data-variant="primary"]').first()
- await expect(submit).toBeEnabled()
- await submit.click()
-
- await expect(viewer.locator('[data-slot="line-comment-content"]').filter({ hasText: note }).first()).toBeVisible()
- await expect(viewer.locator('[data-slot="line-comment-tools"]').first()).toBeVisible()
-}
-
-async function fileOverflow(page: Parameters<typeof test>[0]["page"]) {
- const viewer = page.locator('[data-component="file"][data-mode="text"]').first()
- const view = page.locator('[role="tabpanel"] .scroll-view__viewport').first()
- const pop = viewer.locator('[data-slot="line-comment-popover"][data-inline-body]').first()
- const tools = viewer.locator('[data-slot="line-comment-tools"]').first()
-
- const [width, viewBox, popBox, toolsBox] = await Promise.all([
- view.evaluate((el) => el.scrollWidth - el.clientWidth),
- view.boundingBox(),
- pop.boundingBox(),
- tools.boundingBox(),
- ])
-
- if (!viewBox || !popBox || !toolsBox) return null
-
- return {
- width,
- pop: popBox.x + popBox.width - (viewBox.x + viewBox.width),
- tools: toolsBox.x + toolsBox.width - (viewBox.x + viewBox.width),
- }
-}
-
-test("review applies inline comment clicks without horizontal overflow", async ({ page, llm, project }) => {
- test.setTimeout(180_000)
-
- const tag = `review-comment-${Date.now()}`
- const file = `review-comment-${tag}.txt`
- const note = `comment ${tag}`
-
- await page.setViewportSize({ width: 1280, height: 900 })
-
- await project.open()
- await withSession(project.sdk, `e2e review comment ${tag}`, async (session) => {
- project.trackSession(session.id)
- await patchWithMock(llm, project.sdk, session.id, seed([{ file, mark: tag }]))
-
- await expect
- .poll(
- async () => {
- const diff = await project.sdk.session.diff({ sessionID: session.id }).then((res) => res.data ?? [])
- return diff.length
- },
- { timeout: 60_000 },
- )
- .toBe(1)
-
- await project.gotoSession(session.id)
- await show(page)
-
- const tab = page.getByRole("tab", { name: /Review/i }).first()
- await expect(tab).toBeVisible()
- await tab.click()
-
- await expand(page)
- await waitMark(page, file, tag)
- await comment(page, file, note)
-
- await expect
- .poll(async () => (await overflow(page, file))?.width ?? Number.POSITIVE_INFINITY, { timeout: 10_000 })
- .toBeLessThanOrEqual(1)
- await expect
- .poll(async () => (await overflow(page, file))?.pop ?? Number.POSITIVE_INFINITY, { timeout: 10_000 })
- .toBeLessThanOrEqual(1)
- await expect
- .poll(async () => (await overflow(page, file))?.tools ?? Number.POSITIVE_INFINITY, { timeout: 10_000 })
- .toBeLessThanOrEqual(1)
- })
-})
-
-test("review file comments submit on click without clipping actions", async ({ page, llm, project }) => {
- test.setTimeout(180_000)
-
- const tag = `review-file-comment-${Date.now()}`
- const file = `review-file-comment-${tag}.txt`
- const note = `comment ${tag}`
-
- await page.setViewportSize({ width: 1280, height: 900 })
-
- await project.open()
- await withSession(project.sdk, `e2e review file comment ${tag}`, async (session) => {
- project.trackSession(session.id)
- await patchWithMock(llm, project.sdk, session.id, seed([{ file, mark: tag }]))
-
- await expect
- .poll(
- async () => {
- const diff = await project.sdk.session.diff({ sessionID: session.id }).then((res) => res.data ?? [])
- return diff.length
- },
- { timeout: 60_000 },
- )
- .toBe(1)
-
- await project.gotoSession(session.id)
- await show(page)
-
- const tab = page.getByRole("tab", { name: /Review/i }).first()
- await expect(tab).toBeVisible()
- await tab.click()
-
- await expand(page)
- await waitMark(page, file, tag)
- await openReviewFile(page, file)
- await fileComment(page, note)
-
- await expect
- .poll(async () => (await fileOverflow(page))?.width ?? Number.POSITIVE_INFINITY, { timeout: 10_000 })
- .toBeLessThanOrEqual(1)
- await expect
- .poll(async () => (await fileOverflow(page))?.pop ?? Number.POSITIVE_INFINITY, { timeout: 10_000 })
- .toBeLessThanOrEqual(1)
- await expect
- .poll(async () => (await fileOverflow(page))?.tools ?? Number.POSITIVE_INFINITY, { timeout: 10_000 })
- .toBeLessThanOrEqual(1)
- })
-})
-
-test.fixme("review keeps scroll position after a live diff update", async ({ page, llm, project }) => {
- test.setTimeout(180_000)
-
- const tag = `review-${Date.now()}`
- const list = files(tag)
- const hit = list[list.length - 4]!
- const next = `${tag}-live`
-
- await page.setViewportSize({ width: 1600, height: 1000 })
-
- await project.open()
- await withSession(project.sdk, `e2e review ${tag}`, async (session) => {
- project.trackSession(session.id)
- await patchWithMock(llm, project.sdk, session.id, seed(list))
-
- await expect
- .poll(
- async () => {
- const info = await project.sdk.session.get({ sessionID: session.id }).then((res) => res.data)
- return info?.summary?.files ?? 0
- },
- { timeout: 60_000 },
- )
- .toBe(list.length)
-
- await expect
- .poll(
- async () => {
- const diff = await project.sdk.session.diff({ sessionID: session.id }).then((res) => res.data ?? [])
- return diff.length
- },
- { timeout: 60_000 },
- )
- .toBe(list.length)
-
- await project.gotoSession(session.id)
- await show(page)
-
- const tab = page.getByRole("tab", { name: /Review/i }).first()
- await expect(tab).toBeVisible()
- await tab.click()
-
- const view = page.locator('[data-slot="session-review-scroll"] .scroll-view__viewport').first()
- await expect(view).toBeVisible()
- const heads = page.getByRole("heading", { level: 3 }).filter({ hasText: /^review-scroll-/ })
- await expect(heads).toHaveCount(list.length, { timeout: 60_000 })
-
- await expand(page)
- await waitMark(page, hit.file, hit.mark)
-
- const row = page
- .getByRole("heading", {
- level: 3,
- name: new RegExp(hit.file.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")),
- })
- .first()
- await expect(row).toBeVisible()
- await row.evaluate((el) => el.scrollIntoView({ block: "center" }))
-
- await expect.poll(async () => (await spot(page, hit.file))?.y ?? 0).toBeGreaterThan(200)
- const prev = await spot(page, hit.file)
- if (!prev) throw new Error(`missing review row for ${hit.file}`)
-
- await patchWithMock(llm, project.sdk, session.id, edit(hit.file, hit.mark, next))
-
- await expect
- .poll(
- async () => {
- const diff = await project.sdk.session.diff({ sessionID: session.id }).then((res) => res.data ?? [])
- const item = diff.find((item) => item.file === hit.file)
- return typeof item?.after === "string" ? item.after : ""
- },
- { timeout: 60_000 },
- )
- .toContain(`mark ${next}`)
-
- await waitMark(page, hit.file, next)
-
- await expect
- .poll(
- async () => {
- const next = await spot(page, hit.file)
- if (!next) return Number.POSITIVE_INFINITY
- return Math.max(Math.abs(next.top - prev.top), Math.abs(next.y - prev.y))
- },
- { timeout: 60_000 },
- )
- .toBeLessThanOrEqual(32)
- })
-})
diff --git a/packages/app/e2e/session/session-undo-redo.spec.ts b/packages/app/e2e/session/session-undo-redo.spec.ts
deleted file mode 100644
index 709a45b4c..000000000
--- a/packages/app/e2e/session/session-undo-redo.spec.ts
+++ /dev/null
@@ -1,233 +0,0 @@
-import type { Page } from "@playwright/test"
-import { test, expect } from "../fixtures"
-import { withSession } from "../actions"
-import { createSdk, modKey } from "../utils"
-import { promptSelector } from "../selectors"
-
-async function seedConversation(input: {
- page: Page
- sdk: ReturnType<typeof createSdk>
- sessionID: string
- token: string
-}) {
- const messages = async () =>
- await input.sdk.session.messages({ sessionID: input.sessionID, limit: 100 }).then((r) => r.data ?? [])
- const seeded = await messages()
- const userIDs = new Set(seeded.filter((m) => m.info.role === "user").map((m) => m.info.id))
-
- const prompt = input.page.locator(promptSelector)
- await expect(prompt).toBeVisible()
- await input.sdk.session.promptAsync({
- sessionID: input.sessionID,
- noReply: true,
- parts: [{ type: "text", text: input.token }],
- })
-
- let userMessageID: string | undefined
- await expect
- .poll(
- async () => {
- const users = (await messages()).filter(
- (m) =>
- !userIDs.has(m.info.id) &&
- m.info.role === "user" &&
- m.parts.filter((p) => p.type === "text").some((p) => p.text.includes(input.token)),
- )
- if (users.length === 0) return false
-
- const user = users[users.length - 1]
- if (!user) return false
- userMessageID = user.info.id
- return true
- },
- { timeout: 90_000, intervals: [250, 500, 1_000] },
- )
- .toBe(true)
-
- if (!userMessageID) throw new Error("Expected a user message id")
- await expect(input.page.locator(`[data-message-id="${userMessageID}"]`)).toHaveCount(1, { timeout: 30_000 })
- return { prompt, userMessageID }
-}
-
-test("slash undo sets revert and restores prior prompt", async ({ page, project }) => {
- test.setTimeout(120_000)
-
- const token = `undo_${Date.now()}`
-
- await project.open()
- 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 })
-
- await seeded.prompt.click()
- await page.keyboard.type("/undo")
-
- const undo = page.locator('[data-slash-id="session.undo"]').first()
- await expect(undo).toBeVisible()
- await page.keyboard.press("Enter")
-
- await expect
- .poll(async () => await sdk.session.get({ sessionID: session.id }).then((r) => r.data?.revert?.messageID), {
- timeout: 30_000,
- })
- .toBe(seeded.userMessageID)
-
- await expect(seeded.prompt).toContainText(token)
- await expect(page.locator(`[data-message-id="${seeded.userMessageID}"]`)).toHaveCount(0)
- })
-})
-
-test("slash redo clears revert and restores latest state", async ({ page, project }) => {
- test.setTimeout(120_000)
-
- const token = `redo_${Date.now()}`
-
- await project.open()
- 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 })
-
- await seeded.prompt.click()
- await page.keyboard.type("/undo")
-
- const undo = page.locator('[data-slash-id="session.undo"]').first()
- await expect(undo).toBeVisible()
- await page.keyboard.press("Enter")
-
- await expect
- .poll(async () => await sdk.session.get({ sessionID: session.id }).then((r) => r.data?.revert?.messageID), {
- timeout: 30_000,
- })
- .toBe(seeded.userMessageID)
-
- await seeded.prompt.click()
- await page.keyboard.press(`${modKey}+A`)
- await page.keyboard.press("Backspace")
- await page.keyboard.type("/redo")
-
- const redo = page.locator('[data-slash-id="session.redo"]').first()
- await expect(redo).toBeVisible()
- await page.keyboard.press("Enter")
-
- await expect
- .poll(async () => await sdk.session.get({ sessionID: session.id }).then((r) => r.data?.revert?.messageID), {
- timeout: 30_000,
- })
- .toBeUndefined()
-
- await expect(seeded.prompt).not.toContainText(token)
- await expect(page.locator(`[data-message-id="${seeded.userMessageID}"]`)).toHaveCount(1)
- })
-})
-
-test("slash undo/redo traverses multi-step revert stack", async ({ page, project }) => {
- test.setTimeout(120_000)
-
- const firstToken = `undo_redo_first_${Date.now()}`
- const secondToken = `undo_redo_second_${Date.now()}`
-
- await project.open()
- 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({
- page,
- sdk,
- sessionID: session.id,
- token: firstToken,
- })
- const second = await seedConversation({
- page,
- sdk,
- sessionID: session.id,
- token: secondToken,
- })
-
- expect(first.userMessageID).not.toBe(second.userMessageID)
-
- const firstMessage = page.locator(`[data-message-id="${first.userMessageID}"]`)
- const secondMessage = page.locator(`[data-message-id="${second.userMessageID}"]`)
-
- await expect(firstMessage).toHaveCount(1)
- await expect(secondMessage).toHaveCount(1)
-
- await second.prompt.click()
- await page.keyboard.press(`${modKey}+A`)
- await page.keyboard.press("Backspace")
- await page.keyboard.type("/undo")
-
- const undo = page.locator('[data-slash-id="session.undo"]').first()
- await expect(undo).toBeVisible()
- await page.keyboard.press("Enter")
-
- await expect
- .poll(async () => await sdk.session.get({ sessionID: session.id }).then((r) => r.data?.revert?.messageID), {
- timeout: 30_000,
- })
- .toBe(second.userMessageID)
-
- await expect(firstMessage).toHaveCount(1)
- await expect(secondMessage).toHaveCount(0)
-
- await second.prompt.click()
- await page.keyboard.press(`${modKey}+A`)
- await page.keyboard.press("Backspace")
- await page.keyboard.type("/undo")
- await expect(undo).toBeVisible()
- await page.keyboard.press("Enter")
-
- await expect
- .poll(async () => await sdk.session.get({ sessionID: session.id }).then((r) => r.data?.revert?.messageID), {
- timeout: 30_000,
- })
- .toBe(first.userMessageID)
-
- await expect(firstMessage).toHaveCount(0)
- await expect(secondMessage).toHaveCount(0)
-
- await second.prompt.click()
- await page.keyboard.press(`${modKey}+A`)
- await page.keyboard.press("Backspace")
- await page.keyboard.type("/redo")
-
- const redo = page.locator('[data-slash-id="session.redo"]').first()
- await expect(redo).toBeVisible()
- await page.keyboard.press("Enter")
-
- await expect
- .poll(async () => await sdk.session.get({ sessionID: session.id }).then((r) => r.data?.revert?.messageID), {
- timeout: 30_000,
- })
- .toBe(second.userMessageID)
-
- await expect(firstMessage).toHaveCount(1)
- await expect(secondMessage).toHaveCount(0)
-
- await second.prompt.click()
- await page.keyboard.press(`${modKey}+A`)
- await page.keyboard.press("Backspace")
- await page.keyboard.type("/redo")
- await expect(redo).toBeVisible()
- await page.keyboard.press("Enter")
-
- await expect
- .poll(async () => await sdk.session.get({ sessionID: session.id }).then((r) => r.data?.revert?.messageID), {
- timeout: 30_000,
- })
- .toBeUndefined()
-
- await expect(firstMessage).toHaveCount(1)
- await expect(secondMessage).toHaveCount(1)
- })
-})
diff --git a/packages/app/e2e/session/session.spec.ts b/packages/app/e2e/session/session.spec.ts
deleted file mode 100644
index 1b5fb1b60..000000000
--- a/packages/app/e2e/session/session.spec.ts
+++ /dev/null
@@ -1,182 +0,0 @@
-import { test, expect } from "../fixtures"
-import {
- openSidebar,
- openSessionMoreMenu,
- clickMenuItem,
- confirmDialog,
- openSharePopover,
- withSession,
-} from "../actions"
-import { sessionItemSelector, inlineInputSelector } from "../selectors"
-
-const shareDisabled = process.env.OPENCODE_DISABLE_SHARE === "true" || process.env.OPENCODE_DISABLE_SHARE === "1"
-
-type Sdk = Parameters<typeof withSession>[0]
-
-async function seedMessage(sdk: Sdk, sessionID: string) {
- await sdk.session.promptAsync({
- sessionID,
- noReply: true,
- parts: [{ type: "text", text: "e2e seed" }],
- })
-
- await expect
- .poll(
- async () => {
- const messages = await sdk.session.messages({ sessionID, limit: 1 }).then((r) => r.data ?? [])
- return messages.length
- },
- { timeout: 30_000 },
- )
- .toBeGreaterThan(0)
-}
-
-test("session can be renamed via header menu", async ({ page, project }) => {
- const stamp = Date.now()
- const originalTitle = `e2e rename test ${stamp}`
- const renamedTitle = `e2e renamed ${stamp}`
-
- await project.open()
- 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)
-
- const menu = await openSessionMoreMenu(page, session.id)
- await clickMenuItem(menu, /rename/i)
-
- const input = page.locator(".scroll-view__viewport").locator(inlineInputSelector).first()
- await expect(input).toBeVisible()
- await expect(input).toBeFocused()
- await input.fill(renamedTitle)
- await expect(input).toHaveValue(renamedTitle)
- await input.press("Enter")
-
- await expect
- .poll(
- async () => {
- const data = await project.sdk.session.get({ sessionID: session.id }).then((r) => r.data)
- return data?.title
- },
- { timeout: 30_000 },
- )
- .toBe(renamedTitle)
-
- await expect(page.getByRole("heading", { level: 1 }).first()).toHaveText(renamedTitle)
- })
-})
-
-test("session can be archived via header menu", async ({ page, project }) => {
- const stamp = Date.now()
- const title = `e2e archive test ${stamp}`
-
- await project.open()
- 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)
- await clickMenuItem(menu, /archive/i)
-
- await expect
- .poll(
- async () => {
- const data = await project.sdk.session.get({ sessionID: session.id }).then((r) => r.data)
- return data?.time?.archived
- },
- { timeout: 30_000 },
- )
- .not.toBeUndefined()
-
- await openSidebar(page)
- await expect(page.locator(sessionItemSelector(session.id))).toHaveCount(0)
- })
-})
-
-test("session can be deleted via header menu", async ({ page, project }) => {
- const stamp = Date.now()
- const title = `e2e delete test ${stamp}`
-
- await project.open()
- 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)
- await clickMenuItem(menu, /delete/i)
- await confirmDialog(page, /delete/i)
-
- await expect
- .poll(
- async () => {
- const data = await project.sdk.session
- .get({ sessionID: session.id })
- .then((r) => r.data)
- .catch(() => undefined)
- return data?.id
- },
- { timeout: 30_000 },
- )
- .toBeUndefined()
-
- await openSidebar(page)
- await expect(page.locator(sessionItemSelector(session.id))).toHaveCount(0)
- })
-})
-
-test("session can be shared and unshared via header button", async ({ page, project }) => {
- test.skip(shareDisabled, "Share is disabled in this environment (OPENCODE_DISABLE_SHARE).")
-
- const stamp = Date.now()
- const title = `e2e share test ${stamp}`
-
- await project.open()
- await withSession(project.sdk, title, async (session) => {
- project.trackSession(session.id)
- await project.gotoSession(session.id)
- await project.prompt(`share seed ${stamp}`)
-
- const shared = await openSharePopover(page)
- const publish = shared.popoverBody.getByRole("button", { name: "Publish" }).first()
- await expect(publish).toBeVisible({ timeout: 30_000 })
- await publish.click()
-
- await expect(shared.popoverBody.getByRole("button", { name: "Unpublish" }).first()).toBeVisible({
- timeout: 30_000,
- })
-
- await expect
- .poll(
- async () => {
- const data = await project.sdk.session.get({ sessionID: session.id }).then((r) => r.data)
- return data?.share?.url || undefined
- },
- { timeout: 30_000 },
- )
- .not.toBeUndefined()
-
- const unpublish = shared.popoverBody.getByRole("button", { name: "Unpublish" }).first()
- await expect(unpublish).toBeVisible({ timeout: 30_000 })
- await unpublish.click()
-
- await expect(shared.popoverBody.getByRole("button", { name: "Publish" }).first()).toBeVisible({
- timeout: 30_000,
- })
-
- await expect
- .poll(
- async () => {
- const data = await project.sdk.session.get({ sessionID: session.id }).then((r) => r.data)
- return data?.share?.url || undefined
- },
- { timeout: 30_000 },
- )
- .toBeUndefined()
-
- const unshared = await openSharePopover(page)
- await expect(unshared.popoverBody.getByRole("button", { name: "Publish" }).first()).toBeVisible({
- timeout: 30_000,
- })
- })
-})