diff --git a/packages/app/e2e/AGENTS.md b/packages/app/e2e/AGENTS.md index f263e49a02..bdd6ba185b 100644 --- a/packages/app/e2e/AGENTS.md +++ b/packages/app/e2e/AGENTS.md @@ -59,8 +59,10 @@ test("test description", async ({ page, sdk, gotoSession }) => { ### Using Fixtures - `page` - Playwright page -- `sdk` - OpenCode SDK client for API calls -- `gotoSession(sessionID?)` - Navigate to session +- `llm` - Mock LLM server for queuing responses (`text`, `tool`, `toolMatch`, `textMatch`, etc.) +- `project` - Golden-path project fixture (call `project.open()` first, then use `project.sdk`, `project.prompt(...)`, `project.gotoSession(...)`, `project.trackSession(...)`) +- `sdk` - OpenCode SDK client for API calls (worker-scoped, shared directory) +- `gotoSession(sessionID?)` - Navigate to session (worker-scoped, shared directory) ### Helper Functions @@ -73,12 +75,9 @@ test("test description", async ({ page, sdk, gotoSession }) => { - `waitTerminalReady(page, { term? })` - Wait for a mounted terminal to connect and finish rendering output - `runTerminal(page, { cmd, token, term?, timeout? })` - Type into the terminal via the browser and wait for rendered output - `withSession(sdk, title, callback)` - Create temp session -- `withProject(...)` - Create temp project/workspace - `sessionIDFromUrl(url)` - Read session ID from URL - `slugFromUrl(url)` - Read workspace slug from URL - `waitSlug(page, skip?)` - Wait for resolved workspace slug -- `trackSession(sessionID, directory?)` - Register session for fixture cleanup -- `trackDirectory(directory)` - Register directory for fixture cleanup - `clickListItem(container, filter)` - Click list item by key/text **Selectors** (`selectors.ts`): @@ -128,9 +127,9 @@ test("test with cleanup", async ({ page, sdk, gotoSession }) => { }) ``` -- Prefer `withSession(...)` for temp sessions -- In `withProject(...)` tests that create sessions or extra workspaces, call `trackSession(sessionID, directory?)` and `trackDirectory(directory)` -- This lets fixture teardown abort, wait for idle, and clean up safely under CI concurrency +- Prefer the `project` fixture for tests that need a dedicated project with LLM mocking — call `project.open()` then use `project.prompt(...)`, `project.trackSession(...)`, etc. +- Use `withSession(sdk, title, callback)` for lightweight temp sessions on the shared worker directory +- Call `project.trackSession(sessionID, directory?)` and `project.trackDirectory(directory)` for any resources created outside the fixture so teardown can clean them up - Avoid calling `sdk.session.delete(...)` directly ### Timeouts diff --git a/packages/app/e2e/actions.ts b/packages/app/e2e/actions.ts index 32e3d6ba7c..f62c0555d1 100644 --- a/packages/app/e2e/actions.ts +++ b/packages/app/e2e/actions.ts @@ -637,12 +637,6 @@ export async function openSharePopover(page: Page) { return { rightSection: scroller, popoverBody } } -export async function clickPopoverButton(page: Page, buttonName: string | RegExp) { - const button = page.getByRole("button").filter({ hasText: buttonName }).first() - await expect(button).toBeVisible() - await button.click() -} - export async function clickListItem( container: Locator | Page, filter: string | RegExp | { key?: string; text?: string | RegExp; keyStartsWith?: string }, @@ -808,40 +802,6 @@ export async function seedSessionQuestion( return { id: result.id } } -export async function seedSessionPermission( - sdk: ReturnType, - input: { - sessionID: string - permission: string - patterns: string[] - description?: string - }, -) { - const text = [ - "Your only valid response is one bash tool call.", - `Use this JSON input: ${JSON.stringify({ - command: input.patterns[0] ? `ls ${JSON.stringify(input.patterns[0])}` : "pwd", - workdir: "/", - description: input.description ?? `seed ${input.permission} permission request`, - })}`, - "Do not output plain text.", - ].join("\n") - - const result = await seed({ - sdk, - sessionID: input.sessionID, - prompt: text, - timeout: 30_000, - probe: async () => { - const list = await sdk.permission.list().then((x) => x.data ?? []) - return list.find((item) => item.sessionID === input.sessionID) - }, - }) - - if (!result) throw new Error("Timed out seeding permission request") - return { id: result.id } -} - export async function seedSessionTask( sdk: ReturnType, input: { @@ -900,36 +860,6 @@ export async function seedSessionTask( return result } -export async function seedSessionTodos( - sdk: ReturnType, - input: { - sessionID: string - todos: Array<{ content: string; status: string; priority: string }> - }, -) { - const text = [ - "Your only valid response is one todowrite tool call.", - `Use this JSON input: ${JSON.stringify({ todos: input.todos })}`, - "Do not output plain text.", - ].join("\n") - const target = JSON.stringify(input.todos) - - const result = await seed({ - sdk, - sessionID: input.sessionID, - prompt: text, - timeout: 30_000, - probe: async () => { - const todos = await sdk.session.todo({ sessionID: input.sessionID }).then((x) => x.data ?? []) - if (JSON.stringify(todos) !== target) return - return true - }, - }) - - if (!result) throw new Error("Timed out seeding todos") - return true -} - export async function clearSessionDockSeed(sdk: ReturnType, sessionID: string) { const [questions, permissions] = await Promise.all([ sdk.question.list().then((x) => x.data ?? []), diff --git a/packages/app/e2e/fixtures.ts b/packages/app/e2e/fixtures.ts index bb3ea4a6c4..e6f9a62804 100644 --- a/packages/app/e2e/fixtures.ts +++ b/packages/app/e2e/fixtures.ts @@ -18,7 +18,6 @@ import { waitSlug, withNoReplyPrompt, } from "./actions" -import { openaiModel, withMockOpenAI } from "./prompt/mock" import { promptSelector } from "./selectors" import { createSdk, dirSlug, getWorktree, sessionPath } from "./utils" @@ -59,19 +58,6 @@ type LLMWorker = LLMFixture & { reset: () => Promise } -type AssistantFixture = { - reply: (value: string, opts?: { usage?: Usage }) => Promise - tool: (name: string, input: unknown) => Promise - toolHang: (name: string, input: unknown) => Promise - reason: (value: string, opts?: { text?: string; usage?: Usage }) => Promise - fail: (message?: unknown) => Promise - error: (status: number, body: unknown) => Promise - hang: () => Promise - hold: (value: string, wait: PromiseLike) => Promise - calls: () => Promise - pending: () => Promise -} - export const settingsKey = "settings.v3" const seedModel = (() => { @@ -143,13 +129,9 @@ type ProjectFixture = ProjectHandle & { type TestFixtures = { llm: LLMFixture - assistant: AssistantFixture project: ProjectFixture sdk: ReturnType gotoSession: (sessionID?: string) => Promise - withProject: (callback: (project: ProjectHandle) => Promise, options?: ProjectOptions) => Promise - withBackendProject: (callback: (project: ProjectHandle) => Promise, options?: ProjectOptions) => Promise - withMockProject: (callback: (project: ProjectHandle) => Promise, options?: ProjectOptions) => Promise } type WorkerFixtures = { @@ -238,20 +220,6 @@ export const test = base.extend({ throw new Error(`TestLLMServer still has ${pending} queued response(s) after the test finished`) } }, - assistant: async ({ llm }, use) => { - await use({ - reply: llm.text, - tool: llm.tool, - toolHang: llm.toolHang, - reason: llm.reason, - fail: llm.fail, - error: llm.error, - hang: llm.hang, - hold: llm.hold, - calls: llm.calls, - pending: llm.pending, - }) - }, page: async ({ page }, use) => { let boundary: string | undefined setHealthPhase(page, "test") @@ -312,29 +280,6 @@ export const test = base.extend({ await item.cleanup() } }, - withProject: async ({ page }, use) => { - await use((callback, options) => runProject(page, callback, options)) - }, - withBackendProject: async ({ page, backend }, use) => { - await use((callback, options) => - runProject(page, callback, { ...options, serverUrl: backend.url, sdk: backend.sdk }), - ) - }, - withMockProject: async ({ page, llm, backend }, use) => { - await use((callback, options) => - withMockOpenAI({ - serverUrl: backend.url, - llmUrl: llm.url, - fn: () => - runProject(page, callback, { - ...options, - model: options?.model ?? openaiModel, - serverUrl: backend.url, - sdk: backend.sdk, - }), - }), - ) - }, }) function makeProject( @@ -560,63 +505,6 @@ function makeProject( } } -async function runProject( - page: Page, - callback: (project: ProjectHandle) => Promise, - options?: ProjectOptions & { - serverUrl?: string - sdk?: (directory?: string) => ReturnType - }, -) { - const url = options?.serverUrl - const root = await createTestProject(url ? { serverUrl: url } : undefined) - const sdk = options?.sdk?.(root) ?? createSdk(root, url) - const sessions = new Map() - const dirs = new Set() - await options?.setup?.(root) - await seedStorage(page, { - directory: root, - extra: options?.extra, - model: options?.model, - serverUrl: url, - }) - - const gotoSession = async (sessionID?: string) => { - await visit(page, sessionPath(root, sessionID)) - await waitSession(page, { - directory: root, - sessionID, - serverUrl: url, - allowAnySession: !sessionID, - }) - const current = sessionIDFromUrl(page.url()) - if (current) trackSession(current) - } - - const trackSession = (sessionID: string, directory?: string) => { - sessions.set(sessionID, directory ?? root) - } - - const trackDirectory = (directory: string) => { - if (directory !== root) dirs.add(directory) - } - - try { - await options?.beforeGoto?.({ directory: root, sdk }) - await gotoSession() - const slug = await waitSlug(page) - return await callback({ directory: root, slug, gotoSession, trackSession, trackDirectory, sdk }) - } finally { - setHealthPhase(page, "cleanup") - await Promise.allSettled( - Array.from(sessions, ([sessionID, directory]) => cleanupSession({ sessionID, directory, serverUrl: url })), - ) - await Promise.allSettled(Array.from(dirs, (directory) => cleanupTestProject(directory))) - await cleanupTestProject(root) - setHealthPhase(page, "test") - } -} - async function seedStorage( page: Page, input: {