summaryrefslogtreecommitdiffhomepage
path: root/packages/app/e2e/session
diff options
context:
space:
mode:
authorKit Langton <[email protected]>2026-04-02 14:17:28 -0400
committerGitHub <[email protected]>2026-04-02 18:17:28 +0000
commitc3ef69c8664ab84464d720f75eb9bf35289c1847 (patch)
treed7b1aa78355c39472804bdfbbd9db87722d0587b /packages/app/e2e/session
parent363891126c9b2a909db0e724eb6377bd3d12b38b (diff)
downloadopencode-c3ef69c8664ab84464d720f75eb9bf35289c1847.tar.gz
opencode-c3ef69c8664ab84464d720f75eb9bf35289c1847.zip
test(app): add a golden path for mocked e2e prompts (#20593)
Diffstat (limited to 'packages/app/e2e/session')
-rw-r--r--packages/app/e2e/session/session-child-navigation.spec.ts51
-rw-r--r--packages/app/e2e/session/session-composer-dock.spec.ts670
-rw-r--r--packages/app/e2e/session/session-model-persistence.spec.ts178
-rw-r--r--packages/app/e2e/session/session-review.spec.ts331
-rw-r--r--packages/app/e2e/session/session-undo-redo.spec.ts285
-rw-r--r--packages/app/e2e/session/session.spec.ts248
6 files changed, 846 insertions, 917 deletions
diff --git a/packages/app/e2e/session/session-child-navigation.spec.ts b/packages/app/e2e/session/session-child-navigation.spec.ts
index 1ab4746e4..34a1a9e2e 100644
--- a/packages/app/e2e/session/session-child-navigation.spec.ts
+++ b/packages/app/e2e/session/session-child-navigation.spec.ts
@@ -3,7 +3,7 @@ import { test, expect } from "../fixtures"
import { inputMatch } from "../prompt/mock"
import { promptSelector } from "../selectors"
-test("task tool child-session link does not trigger stale show errors", async ({ page, llm, withMockProject }) => {
+test("task tool child-session link does not trigger stale show errors", async ({ page, llm, project }) => {
test.setTimeout(120_000)
const errs: string[] = []
@@ -13,34 +13,33 @@ test("task tool child-session link does not trigger stale show errors", async ({
page.on("pageerror", onError)
try {
- await withMockProject(async ({ gotoSession, trackSession, sdk }) => {
- await withSession(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(sdk, {
- sessionID: session.id,
- description: taskInput.description,
- prompt: taskInput.prompt,
- })
- trackSession(child.sessionID)
+ 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 gotoSession(session.id)
+ await project.gotoSession(session.id)
- const link = page
- .locator("a.subagent-link")
- .filter({ hasText: /open child session/i })
- .first()
- await expect(link).toBeVisible({ timeout: 30_000 })
- await link.click()
+ const link = page
+ .locator("a.subagent-link")
+ .filter({ hasText: /open child session/i })
+ .first()
+ await expect(link).toBeVisible({ timeout: 30_000 })
+ await link.click()
- await expect(page).toHaveURL(new RegExp(`/session/${child.sessionID}(?:[/?#]|$)`), { timeout: 30_000 })
- await expect(page.locator(promptSelector)).toBeVisible({ timeout: 30_000 })
- await expect.poll(() => errs, { timeout: 5_000 }).toEqual([])
- })
+ await expect(page).toHaveURL(new RegExp(`/session/${child.sessionID}(?:[/?#]|$)`), { timeout: 30_000 })
+ 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 bf0cc35b7..8eeac5b1a 100644
--- a/packages/app/e2e/session/session-composer-dock.spec.ts
+++ b/packages/app/e2e/session/session-composer-dock.spec.ts
@@ -242,9 +242,7 @@ async function withMockPermission<T>(
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",
+ response: res,
body: JSON.stringify(json),
})
}
@@ -269,240 +267,227 @@ async function withMockPermission<T>(
}
}
-test("default dock shows prompt input", async ({ page, withBackendProject }) => {
- await withBackendProject(async (project) => {
- await withDockSession(
- project.sdk,
- "e2e composer dock default",
- async (session) => {
- await project.gotoSession(session.id)
-
- await 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()
- },
- { trackSession: project.trackSession },
- )
- })
+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(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, withBackendProject }) => {
- await withBackendProject(async ({ gotoSession }) => {
- await gotoSession()
+test("auto-accept toggle works before first submit", async ({ page, project }) => {
+ await project.open()
- const button = page.locator('[data-action="prompt-permissions"]').first()
- await expect(button).toBeVisible()
- await expect(button).toHaveAttribute("aria-pressed", "false")
+ const button = page.locator('[data-action="prompt-permissions"]').first()
+ await expect(button).toBeVisible()
+ await expect(button).toHaveAttribute("aria-pressed", "false")
- await setAutoAccept(page, true)
- await setAutoAccept(page, false)
- })
+ await setAutoAccept(page, true)
+ await setAutoAccept(page, false)
})
-test("blocked question flow unblocks after submit", async ({ page, llm, withMockProject }) => {
- await withMockProject(async (project) => {
- await withDockSession(
- project.sdk,
- "e2e composer dock question",
- async (session) => {
- await withDockSeed(project.sdk, session.id, async () => {
- await project.gotoSession(session.id)
+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,
- })
+ 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)
+ const dock = page.locator(questionDockSelector)
+ await expectQuestionBlocked(page)
- await dock.locator('[data-slot="question-option"]').first().click()
- await dock.getByRole("button", { name: /submit/i }).click()
+ await dock.locator('[data-slot="question-option"]').first().click()
+ await dock.getByRole("button", { name: /submit/i }).click()
- await expectQuestionOpen(page)
- })
- },
- { trackSession: project.trackSession },
- )
- })
+ await expectQuestionOpen(page)
+ })
+ },
+ { trackSession: project.trackSession },
+ )
})
-test("blocked question flow supports keyboard shortcuts", async ({ page, llm, withMockProject }) => {
- await withMockProject(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)
+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,
- })
+ 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)
+ const dock = page.locator(questionDockSelector)
+ const first = dock.locator('[data-slot="question-option"]').first()
+ const second = dock.locator('[data-slot="question-option"]').nth(1)
- await expectQuestionBlocked(page)
- await expect(first).toBeFocused()
+ await expectQuestionBlocked(page)
+ await expect(first).toBeFocused()
- await page.keyboard.press("ArrowDown")
- await expect(second).toBeFocused()
+ await page.keyboard.press("ArrowDown")
+ await expect(second).toBeFocused()
- await page.keyboard.press("Space")
- await page.keyboard.press(`${modKey}+Enter`)
- await expectQuestionOpen(page)
- })
- },
- { trackSession: project.trackSession },
- )
- })
+ 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, withMockProject }) => {
- await withMockProject(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)
+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,
- })
+ 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 dock = page.locator(questionDockSelector)
+ const first = dock.locator('[data-slot="question-option"]').first()
- await expectQuestionBlocked(page)
- await expect(first).toBeFocused()
+ await expectQuestionBlocked(page)
+ await expect(first).toBeFocused()
- await page.keyboard.press("Escape")
- await expectQuestionOpen(page)
- })
- },
- { trackSession: project.trackSession },
- )
- })
+ 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 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, 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 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, 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("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,
- withMockProject,
-}) => {
+test("child session question request blocks parent dock and unblocks after submit", async ({ page, llm, project }) => {
const questions = [
{
header: "Child input",
@@ -513,137 +498,131 @@ test("child session question request blocks parent dock and unblocks after submi
],
},
]
- await withMockProject(async (project) => {
- await withDockSession(
- project.sdk,
- "e2e composer dock child question parent",
- async (session) => {
- await project.gotoSession(session.id)
+ 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)
- const child = await project.sdk.session
- .create({
- title: "e2e composer dock child question",
- parentID: session.id,
+ try {
+ await withDockSeed(project.sdk, child.id, async () => {
+ await llm.toolMatch(inputMatch({ questions }), "question", { questions })
+ await seedSessionQuestion(project.sdk, {
+ sessionID: child.id,
+ questions,
})
- .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)
+ const dock = page.locator(questionDockSelector)
+ await expectQuestionBlocked(page)
- await dock.locator('[data-slot="question-option"]').first().click()
- await dock.getByRole("button", { name: /submit/i }).click()
+ await dock.locator('[data-slot="question-option"]').first().click()
+ await dock.getByRole("button", { name: /submit/i }).click()
- await expectQuestionOpen(page)
- })
- } finally {
- await cleanupSession({ sdk: project.sdk, sessionID: child.id })
- }
- },
- { trackSession: project.trackSession },
- )
- })
+ 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,
- 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)
+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)
- 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 },
- )
- })
+ 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, withBackendProject }) => {
- await withBackendProject(async (project) => {
- await withDockSession(
- project.sdk,
- "e2e composer dock todo",
- async (session) => {
- const dock = await todoDock(page, session.id)
- await project.gotoSession(session.id)
- await expect(page.locator(sessionComposerDockSelector)).toBeVisible()
-
- try {
- await dock.open([
- { content: "first task", status: "pending", priority: "high" },
- { content: "second task", status: "in_progress", priority: "medium" },
- ])
- await dock.expectOpen(["pending", "in_progress"])
-
- await dock.collapse()
- await dock.expectCollapsed(["pending", "in_progress"])
-
- await dock.expand()
- await dock.expectOpen(["pending", "in_progress"])
-
- await dock.finish([
- { content: "first task", status: "completed", priority: "high" },
- { content: "second task", status: "cancelled", priority: "medium" },
- ])
- await dock.expectClosed()
- } finally {
- await dock.clear()
- }
- },
- { 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, withMockProject }) => {
+test("keyboard focus stays off prompt while blocked", async ({ page, llm, project }) => {
const questions = [
{
header: "Need input",
@@ -651,28 +630,27 @@ test("keyboard focus stays off prompt while blocked", async ({ page, llm, withMo
options: [{ label: "Continue", description: "Continue now" }],
},
]
- await withMockProject(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 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 llm.toolMatch(inputMatch({ questions }), "question", { questions })
+ await seedSessionQuestion(project.sdk, {
+ sessionID: session.id,
+ questions,
+ })
- await expectQuestionBlocked(page)
+ await expectQuestionBlocked(page)
- await page.locator("main").click({ position: { x: 5, y: 5 } })
- await page.keyboard.type("abc")
- await expect(page.locator(promptSelector)).toHaveCount(0)
- })
- },
- { trackSession: project.trackSession },
- )
- })
+ 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
index 66bc451bc..c107cc518 100644
--- a/packages/app/e2e/session/session-model-persistence.spec.ts
+++ b/packages/app/e2e/session/session-model-persistence.spec.ts
@@ -1,15 +1,6 @@
import type { Locator, Page } from "@playwright/test"
import { test, expect } from "../fixtures"
-import {
- openSidebar,
- resolveSlug,
- sessionIDFromUrl,
- setWorkspacesEnabled,
- waitSession,
- waitSessionIdle,
- waitSlug,
- withNoReplyPrompt,
-} from "../actions"
+import { openSidebar, resolveSlug, setWorkspacesEnabled, waitSession, waitSlug } from "../actions"
import {
promptAgentSelector,
promptModelSelector,
@@ -230,35 +221,8 @@ async function goto(page: Page, directory: string, sessionID?: string) {
await waitSession(page, { directory, sessionID })
}
-async function submit(page: Page, value: string) {
- const prompt = page.locator('[data-component="prompt-input"]')
- await expect(prompt).toBeVisible()
-
- await withNoReplyPrompt(page, async () => {
- await prompt.click()
- await prompt.fill(value)
- await prompt.press("Enter")
- })
-
- await expect.poll(() => sessionIDFromUrl(page.url()) ?? "", { timeout: 30_000 }).not.toBe("")
- const id = sessionIDFromUrl(page.url())
- if (!id) throw new Error(`Failed to resolve session id from ${page.url()}`)
- return id
-}
-
-async function waitUser(directory: string, sessionID: string) {
- const sdk = createSdk(directory)
- await expect
- .poll(
- async () => {
- const items = await sdk.session.messages({ sessionID, limit: 20 }).then((x) => x.data ?? [])
- return items.some((item) => item.info.role === "user")
- },
- { timeout: 30_000 },
- )
- .toBe(true)
- await sdk.session.abort({ sessionID }).catch(() => undefined)
- await waitSessionIdle(sdk, sessionID, 30_000).catch(() => undefined)
+async function submit(project: Parameters<typeof test>[0]["project"], value: string) {
+ return project.prompt(value)
}
async function createWorkspace(page: Page, root: string, seen: string[]) {
@@ -301,108 +265,98 @@ async function newWorkspaceSession(page: Page, slug: string) {
return waitSession(page, { directory: next.directory }).then((item) => item.directory)
}
-test("session model restore per session without leaking into new sessions", async ({ page, withProject }) => {
+test("session model restore per session without leaking into new sessions", async ({ page, project }) => {
await page.setViewportSize({ width: 1440, height: 900 })
- await withProject(async ({ directory, gotoSession, trackSession }) => {
- await gotoSession()
+ await project.open()
+ await project.gotoSession()
- const firstState = await chooseOtherModel(page)
- const firstKey = await currentModel(page)
- const first = await submit(page, `session variant ${Date.now()}`)
- trackSession(first)
- await waitUser(directory, first)
+ 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, sessionID: first })
- await waitFooter(page, firstState)
+ await page.reload()
+ await waitSession(page, { directory: project.directory, sessionID: first })
+ await waitFooter(page, firstState)
- await gotoSession()
- const fresh = await read(page)
- expect(fresh.model).not.toBe(firstState.model)
+ 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(page, `session model ${Date.now()}`)
- trackSession(second)
- await waitUser(directory, second)
+ const secondState = await chooseOtherModel(page, [firstKey])
+ const second = await submit(project, `session model ${Date.now()}`)
- await goto(page, directory, first)
- await waitFooter(page, firstState)
+ await goto(page, project.directory, first)
+ await waitFooter(page, firstState)
- await goto(page, directory, second)
- await waitFooter(page, secondState)
+ await goto(page, project.directory, second)
+ await waitFooter(page, secondState)
- await gotoSession()
- await waitFooter(page, fresh)
- })
+ await project.gotoSession()
+ await page.reload()
+ await waitSession(page, { directory: project.directory })
+ await waitFooter(page, fresh)
})
-test("session model restore across workspaces", async ({ page, withProject }) => {
+test("session model restore across workspaces", async ({ page, project }) => {
await page.setViewportSize({ width: 1440, height: 900 })
- await withProject(async ({ directory: root, slug, gotoSession, trackDirectory, trackSession }) => {
- await gotoSession()
+ await project.open()
+ const root = project.directory
+ await project.gotoSession()
- const firstState = await chooseOtherModel(page)
- const firstKey = await currentModel(page)
- const first = await submit(page, `root session ${Date.now()}`)
- trackSession(first, root)
- await waitUser(root, first)
+ 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, slug, true)
+ await openSidebar(page)
+ await setWorkspacesEnabled(page, project.slug, true)
- const one = await createWorkspace(page, slug, [])
- const oneDir = await newWorkspaceSession(page, one.slug)
- trackDirectory(oneDir)
+ 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(page, `workspace one ${Date.now()}`)
- trackSession(second, oneDir)
- await waitUser(oneDir, second)
+ 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, slug, [one.slug])
- const twoDir = await newWorkspaceSession(page, two.slug)
- trackDirectory(twoDir)
+ 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(page, `workspace two ${Date.now()}`)
- trackSession(third, twoDir)
- await waitUser(twoDir, third)
+ 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, root, first)
+ await waitFooter(page, firstState)
- await goto(page, oneDir, second)
- await waitFooter(page, secondState)
+ await goto(page, oneDir, second)
+ await waitFooter(page, secondState)
- await goto(page, twoDir, third)
- await waitFooter(page, thirdState)
+ await goto(page, twoDir, third)
+ await waitFooter(page, thirdState)
- await goto(page, root, first)
- await waitFooter(page, firstState)
- })
+ await goto(page, root, first)
+ await waitFooter(page, firstState)
})
-test("variant preserved when switching agent modes", async ({ page, withProject }) => {
+test("variant preserved when switching agent modes", async ({ page, project }) => {
await page.setViewportSize({ width: 1440, height: 900 })
- await withProject(async ({ directory, gotoSession }) => {
- await gotoSession()
+ await project.open()
+ await project.gotoSession()
- await ensureVariant(page, directory)
- const updated = await chooseDifferentVariant(page)
+ 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
+ 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, other)
+ await waitFooter(page, { agent: other, variant: updated.variant })
- await choose(page, promptAgentSelector, updated.agent)
- await waitFooter(page, { agent: updated.agent, 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
index c7529112f..c0a98cb2e 100644
--- a/packages/app/e2e/session/session-review.spec.ts
+++ b/packages/app/e2e/session/session-review.spec.ts
@@ -1,6 +1,6 @@
import { waitSessionIdle, withSession } from "../actions"
import { test, expect } from "../fixtures"
-import { inputMatch } from "../prompt/mock"
+import { bodyText } from "../prompt/mock"
const count = 14
@@ -47,8 +47,12 @@ async function patchWithMock(
patchText: string,
) {
const callsBefore = await llm.calls()
- await llm.toolMatch(inputMatch({ patchText }), "apply_patch", { patchText })
- await sdk.session.promptAsync({
+ 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: [
@@ -61,12 +65,16 @@ async function patchWithMock(
parts: [{ type: "text", text: "Apply the provided patch exactly once." }],
})
- // Wait for the agent loop to actually start before checking idle.
- // promptAsync is fire-and-forget — without this, waitSessionIdle can
- // return immediately because the session status is still undefined.
await expect.poll(() => llm.calls().then((c) => c > callsBefore), { timeout: 30_000 }).toBe(true)
-
- await waitSessionIdle(sdk, sessionID, 120_000)
+ 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"]) {
@@ -245,7 +253,7 @@ async function fileOverflow(page: Parameters<typeof test>[0]["page"]) {
}
}
-test("review applies inline comment clicks without horizontal overflow", async ({ page, llm, withMockProject }) => {
+test("review applies inline comment clicks without horizontal overflow", async ({ page, llm, project }) => {
test.setTimeout(180_000)
const tag = `review-comment-${Date.now()}`
@@ -254,46 +262,45 @@ test("review applies inline comment clicks without horizontal overflow", async (
await page.setViewportSize({ width: 1280, height: 900 })
- await withMockProject(async (project) => {
- 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)
- })
+ 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, withMockProject }) => {
+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()}`
@@ -302,47 +309,46 @@ test("review file comments submit on click without clipping actions", async ({ p
await page.setViewportSize({ width: 1280, height: 900 })
- await withMockProject(async (project) => {
- 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)
- })
+ 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, withMockProject }) => {
+test.fixme("review keeps scroll position after a live diff update", async ({ page, llm, project }) => {
test.setTimeout(180_000)
const tag = `review-${Date.now()}`
@@ -352,84 +358,83 @@ test.fixme("review keeps scroll position after a live diff update", async ({ pag
await page.setViewportSize({ width: 1600, height: 1000 })
- await withMockProject(async (project) => {
- 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)
- })
+ 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
index a63bd9e3b..709a45b4c 100644
--- a/packages/app/e2e/session/session-undo-redo.spec.ts
+++ b/packages/app/e2e/session/session-undo-redo.spec.ts
@@ -49,188 +49,185 @@ async function seedConversation(input: {
return { prompt, userMessageID }
}
-test("slash undo sets revert and restores prior prompt", async ({ page, withBackendProject }) => {
+test("slash undo sets revert and restores prior prompt", async ({ page, project }) => {
test.setTimeout(120_000)
const token = `undo_${Date.now()}`
- await withBackendProject(async (project) => {
- const sdk = project.sdk
+ 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)
+ 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 })
+ const seeded = await seedConversation({ page, sdk, sessionID: session.id, token })
- await seeded.prompt.click()
- await page.keyboard.type("/undo")
+ 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")
+ 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
+ .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)
- })
+ 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, withBackendProject }) => {
+test("slash redo clears revert and restores latest state", async ({ page, project }) => {
test.setTimeout(120_000)
const token = `redo_${Date.now()}`
- await withBackendProject(async (project) => {
- const sdk = project.sdk
+ 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)
+ 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 })
+ const seeded = await seedConversation({ page, sdk, sessionID: session.id, token })
- await seeded.prompt.click()
- await page.keyboard.type("/undo")
+ 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")
+ 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
+ .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")
+ 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")
+ 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
+ .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)
- })
+ 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, withBackendProject }) => {
+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 withBackendProject(async (project) => {
- const sdk = project.sdk
+ 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")
- await withSession(sdk, `e2e undo redo stack ${Date.now()}`, async (session) => {
- project.trackSession(session.id)
- await project.gotoSession(session.id)
+ const undo = page.locator('[data-slash-id="session.undo"]').first()
+ await expect(undo).toBeVisible()
+ await page.keyboard.press("Enter")
- const first = await seedConversation({
- page,
- sdk,
- sessionID: session.id,
- token: firstToken,
+ await expect
+ .poll(async () => await sdk.session.get({ sessionID: session.id }).then((r) => r.data?.revert?.messageID), {
+ timeout: 30_000,
})
- const second = await seedConversation({
- page,
- sdk,
- sessionID: session.id,
- token: secondToken,
+ .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)
- 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)
- })
+ 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
index 6c885460c..1b5fb1b60 100644
--- a/packages/app/e2e/session/session.spec.ts
+++ b/packages/app/e2e/session/session.spec.ts
@@ -31,156 +31,152 @@ async function seedMessage(sdk: Sdk, sessionID: string) {
.toBeGreaterThan(0)
}
-test("session can be renamed via header menu", async ({ page, withBackendProject }) => {
+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 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)
-
- 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)
- })
+ 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, withBackendProject }) => {
+test("session can be archived via header menu", async ({ page, project }) => {
const stamp = Date.now()
const title = `e2e archive test ${stamp}`
- 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)
- 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)
- })
+ 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, withBackendProject }) => {
+test("session can be deleted via header menu", async ({ page, project }) => {
const stamp = Date.now()
const title = `e2e delete test ${stamp}`
- 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)
- 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)
- })
+ 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, withBackendProject }) => {
+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 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 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,
- })
+ 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,
})
})
})