test(app): stabilize review and workspace e2e
parent
cfcdd5c1dd
commit
1c0812fe01
|
|
@ -44,7 +44,7 @@ export async function defocus(page: Page) {
|
|||
}
|
||||
|
||||
export async function withNoReplyPrompt<T>(page: Page, fn: () => Promise<T>) {
|
||||
const url = "**/session/*/prompt_async"
|
||||
const urls = ["**/session/*/prompt_async", "**/session/*/message"]
|
||||
const route = async (input: Route) => {
|
||||
const body = input.request().postDataJSON()
|
||||
await input.continue({
|
||||
|
|
@ -56,11 +56,11 @@ export async function withNoReplyPrompt<T>(page: Page, fn: () => Promise<T>) {
|
|||
})
|
||||
}
|
||||
|
||||
await page.route(url, route)
|
||||
await Promise.all(urls.map((url) => page.route(url, route)))
|
||||
try {
|
||||
return await fn()
|
||||
} finally {
|
||||
await page.unroute(url, route)
|
||||
await Promise.all(urls.map((url) => page.unroute(url, route)))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -479,7 +479,15 @@ export async function waitDir(page: Page, directory: string, input?: { serverUrl
|
|||
return { directory: target, slug: base64Encode(target) }
|
||||
}
|
||||
|
||||
export async function waitSession(page: Page, input: { directory: string; sessionID?: string; serverUrl?: string }) {
|
||||
export async function waitSession(
|
||||
page: Page,
|
||||
input: {
|
||||
directory: string
|
||||
sessionID?: string
|
||||
serverUrl?: string
|
||||
allowAnySession?: boolean
|
||||
},
|
||||
) {
|
||||
const target = await resolveDirectory(input.directory, input.serverUrl)
|
||||
await expect
|
||||
.poll(
|
||||
|
|
@ -491,9 +499,11 @@ export async function waitSession(page: Page, input: { directory: string; sessio
|
|||
if (!resolved || resolved.directory !== target) return false
|
||||
const current = sessionIDFromUrl(page.url())
|
||||
if (input.sessionID && current !== input.sessionID) return false
|
||||
if (!input.sessionID && !input.allowAnySession && current) return false
|
||||
|
||||
const state = await probeSession(page)
|
||||
if (input.sessionID && (!state || state.sessionID !== input.sessionID)) return false
|
||||
if (!input.sessionID && !input.allowAnySession && state?.sessionID) return false
|
||||
if (state?.dir) {
|
||||
const dir = await resolveDirectory(state.dir, input.serverUrl).catch(() => state.dir ?? "")
|
||||
if (dir !== target) return false
|
||||
|
|
@ -1020,7 +1030,7 @@ export async function setWorkspacesEnabled(page: Page, projectSlug: string, enab
|
|||
if ((await current()) === enabled) return
|
||||
|
||||
if (enabled) {
|
||||
await page.goto(page.url())
|
||||
await page.reload()
|
||||
await openSidebar(page)
|
||||
if ((await current()) === enabled) return
|
||||
}
|
||||
|
|
@ -1053,7 +1063,7 @@ export async function setWorkspacesEnabled(page: Page, projectSlug: string, enab
|
|||
}
|
||||
|
||||
if ((await current()) !== enabled) {
|
||||
await page.goto(page.url())
|
||||
await page.reload()
|
||||
await openSidebar(page)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -295,7 +295,12 @@ export const test = base.extend<TestFixtures, WorkerFixtures>({
|
|||
|
||||
const gotoSession = async (sessionID?: string) => {
|
||||
await visit(page, sessionPath(directory, sessionID))
|
||||
await waitSession(page, { directory, sessionID, serverUrl: backend.url })
|
||||
await waitSession(page, {
|
||||
directory,
|
||||
sessionID,
|
||||
serverUrl: backend.url,
|
||||
allowAnySession: !sessionID,
|
||||
})
|
||||
}
|
||||
await use(gotoSession)
|
||||
},
|
||||
|
|
@ -365,7 +370,12 @@ function makeProject(
|
|||
const gotoSession = async (sessionID?: string) => {
|
||||
const cur = need()
|
||||
await visit(page, sessionPath(cur.directory, sessionID))
|
||||
await waitSession(page, { directory: cur.directory, sessionID, serverUrl: backend.url })
|
||||
await waitSession(page, {
|
||||
directory: cur.directory,
|
||||
sessionID,
|
||||
serverUrl: backend.url,
|
||||
allowAnySession: !sessionID,
|
||||
})
|
||||
const current = sessionIDFromUrl(page.url())
|
||||
if (current) trackSession(current)
|
||||
}
|
||||
|
|
@ -394,6 +404,46 @@ function makeProject(
|
|||
}
|
||||
|
||||
const send = async (text: string, input: { noReply: boolean; shell: boolean }) => {
|
||||
if (input.noReply) {
|
||||
const cur = need()
|
||||
const state = await page.evaluate(() => {
|
||||
const model = (window as E2EWindow).__opencode_e2e?.model?.current
|
||||
if (!model) return null
|
||||
return {
|
||||
dir: model.dir,
|
||||
sessionID: model.sessionID,
|
||||
agent: model.agent,
|
||||
model: model.model ? { providerID: model.model.providerID, modelID: model.model.modelID } : undefined,
|
||||
variant: model.variant ?? undefined,
|
||||
}
|
||||
})
|
||||
const dir = state?.dir ?? cur.directory
|
||||
const sdk = backend.sdk(dir)
|
||||
const sessionID = state?.sessionID
|
||||
? state.sessionID
|
||||
: await sdk.session.create({ directory: dir, title: "E2E Session" }).then((res) => {
|
||||
if (!res.data?.id) throw new Error("Failed to create no-reply session")
|
||||
return res.data.id
|
||||
})
|
||||
await sdk.session.prompt({
|
||||
sessionID,
|
||||
agent: state?.agent,
|
||||
model: state?.model,
|
||||
variant: state?.variant,
|
||||
noReply: true,
|
||||
parts: [{ type: "text", text }],
|
||||
})
|
||||
await visit(page, sessionPath(dir, sessionID))
|
||||
const active = await waitSession(page, {
|
||||
directory: dir,
|
||||
sessionID,
|
||||
serverUrl: backend.url,
|
||||
})
|
||||
trackSession(sessionID, active.directory)
|
||||
await waitSessionSaved(active.directory, sessionID, 90_000, backend.url)
|
||||
return sessionID
|
||||
}
|
||||
|
||||
const prev = await promptSend(page)
|
||||
if (!input.noReply && !input.shell && (await llm.pending()) === 0) {
|
||||
await llm.text("ok")
|
||||
|
|
@ -430,11 +480,7 @@ function makeProject(
|
|||
await expect.poll(async () => (await promptSend(page)).started, { timeout: 5_000 }).toBeGreaterThan(prev.started)
|
||||
}
|
||||
|
||||
if (input.noReply) {
|
||||
await withNoReplyPrompt(page, submit)
|
||||
} else {
|
||||
await submit()
|
||||
}
|
||||
await submit()
|
||||
|
||||
let next: { sessionID: string; directory: string } | undefined
|
||||
await expect
|
||||
|
|
@ -537,7 +583,12 @@ async function runProject<T>(
|
|||
|
||||
const gotoSession = async (sessionID?: string) => {
|
||||
await visit(page, sessionPath(root, sessionID))
|
||||
await waitSession(page, { directory: root, sessionID, serverUrl: url })
|
||||
await waitSession(page, {
|
||||
directory: root,
|
||||
sessionID,
|
||||
serverUrl: url,
|
||||
allowAnySession: !sessionID,
|
||||
})
|
||||
const current = sessionIDFromUrl(page.url())
|
||||
if (current) trackSession(current)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -315,7 +315,20 @@ test("can delete a workspace", async ({ page, project }) => {
|
|||
await page.setViewportSize({ width: 1400, height: 800 })
|
||||
await project.open()
|
||||
|
||||
const { rootSlug, slug, directory } = await setupWorkspaceTest(page, project)
|
||||
const rootSlug = project.slug
|
||||
await openSidebar(page)
|
||||
await setWorkspacesEnabled(page, rootSlug, true)
|
||||
|
||||
const created = await project.sdk.worktree.create({ directory: project.directory }).then((res) => res.data)
|
||||
if (!created?.directory) throw new Error("Failed to create workspace for delete test")
|
||||
|
||||
const directory = created.directory
|
||||
const slug = dirSlug(directory)
|
||||
project.trackDirectory(directory)
|
||||
|
||||
await page.reload()
|
||||
await openSidebar(page)
|
||||
await expect(page.locator(workspaceItemSelector(slug)).first()).toBeVisible({ timeout: 60_000 })
|
||||
|
||||
await expect
|
||||
.poll(
|
||||
|
|
|
|||
|
|
@ -222,7 +222,7 @@ async function goto(page: Page, directory: string, sessionID?: string) {
|
|||
}
|
||||
|
||||
async function submit(project: Parameters<typeof test>[0]["project"], value: string) {
|
||||
return project.user(value)
|
||||
return project.prompt(value)
|
||||
}
|
||||
|
||||
async function createWorkspace(page: Page, root: string, seen: string[]) {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { waitSessionIdle, withSession } from "../actions"
|
||||
import { test, expect } from "../fixtures"
|
||||
import { promptMatch } 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(promptMatch("Apply the provided patch exactly once."), "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,12 @@ 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 () => (await sdk.session.get({ sessionID }).then((res) => res.data?.summary?.files)) ?? 0, {
|
||||
timeout: 120_000,
|
||||
})
|
||||
.toBeGreaterThan(0)
|
||||
}
|
||||
|
||||
async function show(page: Parameters<typeof test>[0]["page"]) {
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import type {
|
|||
import type { State, VcsCache } from "./types"
|
||||
import { trimSessions } from "./session-trim"
|
||||
import { dropSessionCaches } from "./session-cache"
|
||||
import { sanitizeProject } from "./utils"
|
||||
|
||||
const SKIP_PARTS = new Set(["patch", "step-start", "step-finish"])
|
||||
|
||||
|
|
@ -33,12 +34,12 @@ export function applyGlobalEvent(input: {
|
|||
const result = Binary.search(input.project, properties.id, (s) => s.id)
|
||||
if (result.found) {
|
||||
input.setGlobalProject((draft) => {
|
||||
draft[result.index] = { ...draft[result.index], ...properties }
|
||||
draft[result.index] = sanitizeProject({ ...draft[result.index], ...properties })
|
||||
})
|
||||
return
|
||||
}
|
||||
input.setGlobalProject((draft) => {
|
||||
draft.splice(result.index, 0, properties)
|
||||
draft.splice(result.index, 0, sanitizeProject(properties))
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -27,13 +27,21 @@ export function normalizeProviderList(input: ProviderListResponse): ProviderList
|
|||
}
|
||||
|
||||
export function sanitizeProject(project: Project) {
|
||||
if (!project.icon?.url && !project.icon?.override) return project
|
||||
return {
|
||||
...project,
|
||||
commands: project.commands
|
||||
? {
|
||||
...project.commands,
|
||||
}
|
||||
: undefined,
|
||||
icon: {
|
||||
...project.icon,
|
||||
url: undefined,
|
||||
override: undefined,
|
||||
},
|
||||
sandboxes: [...(project.sandboxes ?? [])],
|
||||
time: {
|
||||
...project.time,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue