Skip to content

Commit f3f728e

Browse files
authored
test(app): fix isolated backend follow-ups (#20513)
1 parent c619cae commit f3f728e

7 files changed

Lines changed: 417 additions & 382 deletions

File tree

packages/app/e2e/backend.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,14 @@ async function waitForHealth(url: string, probe = "/global/health") {
4444
throw new Error(`Timed out waiting for backend health at ${url}${probe}${last ? ` (${last})` : ""}`)
4545
}
4646

47+
async function waitExit(proc: ReturnType<typeof spawn>, timeout = 10_000) {
48+
if (proc.exitCode !== null) return
49+
await Promise.race([
50+
new Promise<void>((resolve) => proc.once("exit", () => resolve())),
51+
new Promise<void>((resolve) => setTimeout(resolve, timeout)),
52+
])
53+
}
54+
4755
const LOG_CAP = 100
4856

4957
function cap(input: string[]) {
@@ -62,7 +70,6 @@ export async function startBackend(label: string): Promise<Handle> {
6270
const opencodeDir = path.join(repoDir, "packages", "opencode")
6371
const env = {
6472
...process.env,
65-
OPENCODE_DISABLE_SHARE: process.env.OPENCODE_DISABLE_SHARE ?? "true",
6673
OPENCODE_DISABLE_LSP_DOWNLOAD: "true",
6774
OPENCODE_DISABLE_DEFAULT_PLUGINS: "true",
6875
OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER: "true",
@@ -117,7 +124,11 @@ export async function startBackend(label: string): Promise<Handle> {
117124
async stop() {
118125
if (proc.exitCode === null) {
119126
proc.kill("SIGTERM")
120-
await new Promise((resolve) => proc.once("exit", () => resolve(undefined))).catch(() => undefined)
127+
await waitExit(proc)
128+
}
129+
if (proc.exitCode === null) {
130+
proc.kill("SIGKILL")
131+
await waitExit(proc)
121132
}
122133
await fs.rm(sandbox, { recursive: true, force: true }).catch(() => undefined)
123134
},

packages/app/e2e/prompt/prompt-history.spec.ts

Lines changed: 51 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,11 @@ import type { Page } from "@playwright/test"
33
import { test, expect } from "../fixtures"
44
import { assistantText, sessionIDFromUrl } from "../actions"
55
import { promptSelector } from "../selectors"
6+
import { createSdk } from "../utils"
67
import { openaiModel, promptMatch, titleMatch, withMockOpenAI } from "./mock"
78

89
const text = (value: string | null) => (value ?? "").replace(/\u200B/g, "").trim()
10+
type Sdk = ReturnType<typeof createSdk>
911

1012
const isBash = (part: unknown): part is ToolPart => {
1113
if (!part || typeof part !== "object") return false
@@ -14,47 +16,15 @@ const isBash = (part: unknown): part is ToolPart => {
1416
return "state" in part
1517
}
1618

17-
async function edge(page: Page, pos: "start" | "end") {
18-
await page.locator(promptSelector).evaluate((el: HTMLDivElement, pos: "start" | "end") => {
19-
const selection = window.getSelection()
20-
if (!selection) return
21-
22-
const walk = document.createTreeWalker(el, NodeFilter.SHOW_TEXT)
23-
const nodes: Text[] = []
24-
for (let node = walk.nextNode(); node; node = walk.nextNode()) {
25-
nodes.push(node as Text)
26-
}
27-
28-
if (nodes.length === 0) {
29-
const node = document.createTextNode("")
30-
el.appendChild(node)
31-
nodes.push(node)
32-
}
33-
34-
const node = pos === "start" ? nodes[0]! : nodes[nodes.length - 1]!
35-
const range = document.createRange()
36-
range.setStart(node, pos === "start" ? 0 : (node.textContent ?? "").length)
37-
range.collapse(true)
38-
selection.removeAllRanges()
39-
selection.addRange(range)
40-
}, pos)
41-
}
42-
4319
async function wait(page: Page, value: string) {
4420
await expect.poll(async () => text(await page.locator(promptSelector).textContent())).toBe(value)
4521
}
4622

47-
async function reply(
48-
sdk: { session: { messages: Parameters<typeof assistantText>[0]["session"] } },
49-
sessionID: string,
50-
token: string,
51-
) {
52-
await expect
53-
.poll(() => assistantText(sdk as Parameters<typeof assistantText>[0], sessionID), { timeout: 90_000 })
54-
.toContain(token)
23+
async function reply(sdk: Sdk, sessionID: string, token: string) {
24+
await expect.poll(() => assistantText(sdk, sessionID), { timeout: 90_000 }).toContain(token)
5525
}
5626

57-
async function shell(sdk: Parameters<typeof withSession>[0], sessionID: string, cmd: string, token: string) {
27+
async function shell(sdk: Sdk, sessionID: string, cmd: string, token: string) {
5828
await expect
5929
.poll(
6030
async () => {
@@ -142,76 +112,64 @@ test("prompt history restores unsent draft with arrow navigation", async ({
142112
})
143113
})
144114

145-
test("shell history stays separate from normal prompt history", async ({ page, llm, backend, withBackendProject }) => {
115+
test.fixme("shell history stays separate from normal prompt history", async ({ page, sdk, gotoSession }) => {
146116
test.setTimeout(120_000)
147117

148-
await withMockOpenAI({
149-
serverUrl: backend.url,
150-
llmUrl: llm.url,
151-
fn: async () => {
152-
const firstToken = `E2E_SHELL_ONE_${Date.now()}`
153-
const secondToken = `E2E_SHELL_TWO_${Date.now()}`
154-
const normalToken = `E2E_NORMAL_${Date.now()}`
155-
const first = `echo ${firstToken}`
156-
const second = `echo ${secondToken}`
157-
const normal = `Reply with exactly: ${normalToken}`
118+
const firstToken = `E2E_SHELL_ONE_${Date.now()}`
119+
const secondToken = `E2E_SHELL_TWO_${Date.now()}`
120+
const normalToken = `E2E_NORMAL_${Date.now()}`
121+
const first = `echo ${firstToken}`
122+
const second = `echo ${secondToken}`
123+
const normal = `Reply with exactly: ${normalToken}`
158124

159-
await llm.textMatch(titleMatch, "E2E Title")
160-
await llm.textMatch(promptMatch(normalToken), normalToken)
125+
await gotoSession()
161126

162-
await withBackendProject(
163-
async (project) => {
164-
const prompt = page.locator(promptSelector)
127+
const prompt = page.locator(promptSelector)
165128

166-
await prompt.click()
167-
await page.keyboard.type("!")
168-
await page.keyboard.type(first)
169-
await page.keyboard.press("Enter")
170-
await wait(page, "")
129+
await prompt.click()
130+
await page.keyboard.type("!")
131+
await page.keyboard.type(first)
132+
await page.keyboard.press("Enter")
133+
await wait(page, "")
171134

172-
await expect(page).toHaveURL(/\/session\/[^/?#]+/, { timeout: 30_000 })
173-
const sessionID = sessionIDFromUrl(page.url())!
174-
project.trackSession(sessionID)
175-
await shell(project.sdk, sessionID, first, firstToken)
135+
await expect(page).toHaveURL(/\/session\/[^/?#]+/, { timeout: 30_000 })
136+
const sessionID = sessionIDFromUrl(page.url())!
137+
await shell(sdk, sessionID, first, firstToken)
176138

177-
await prompt.click()
178-
await page.keyboard.type("!")
179-
await page.keyboard.type(second)
180-
await page.keyboard.press("Enter")
181-
await wait(page, "")
182-
await shell(project.sdk, sessionID, second, secondToken)
139+
await prompt.click()
140+
await page.keyboard.type("!")
141+
await page.keyboard.type(second)
142+
await page.keyboard.press("Enter")
143+
await wait(page, "")
144+
await shell(sdk, sessionID, second, secondToken)
183145

184-
await prompt.click()
185-
await page.keyboard.type("!")
186-
await page.keyboard.press("ArrowUp")
187-
await wait(page, second)
146+
await page.keyboard.press("Escape")
147+
await wait(page, "")
188148

189-
await page.keyboard.press("ArrowUp")
190-
await wait(page, first)
149+
await prompt.click()
150+
await page.keyboard.type("!")
151+
await page.keyboard.press("ArrowUp")
152+
await wait(page, second)
191153

192-
await page.keyboard.press("ArrowDown")
193-
await wait(page, second)
154+
await page.keyboard.press("ArrowUp")
155+
await wait(page, first)
194156

195-
await page.keyboard.press("ArrowDown")
196-
await wait(page, "")
157+
await page.keyboard.press("ArrowDown")
158+
await wait(page, second)
197159

198-
await page.keyboard.press("Escape")
199-
await wait(page, "")
160+
await page.keyboard.press("ArrowDown")
161+
await wait(page, "")
200162

201-
await prompt.click()
202-
await page.keyboard.type(normal)
203-
await page.keyboard.press("Enter")
204-
await wait(page, "")
205-
await reply(project.sdk, sessionID, normalToken)
163+
await page.keyboard.press("Escape")
164+
await wait(page, "")
206165

207-
await prompt.click()
208-
await page.keyboard.press("ArrowUp")
209-
await wait(page, normal)
210-
},
211-
{
212-
model: openaiModel,
213-
},
214-
)
215-
},
216-
})
166+
await prompt.click()
167+
await page.keyboard.type(normal)
168+
await page.keyboard.press("Enter")
169+
await wait(page, "")
170+
await reply(sdk, sessionID, normalToken)
171+
172+
await prompt.click()
173+
await page.keyboard.press("ArrowUp")
174+
await wait(page, normal)
217175
})

packages/app/e2e/prompt/prompt-slash-share.spec.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ test("/share and /unshare update session share state", async ({ page, withBacken
2727

2828
await withBackendProject(async (project) => {
2929
await withSession(project.sdk, `e2e slash share ${Date.now()}`, async (session) => {
30+
project.trackSession(session.id)
3031
const prompt = page.locator(promptSelector)
3132

3233
await seed(project.sdk, session.id)
Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { seedSessionTask, withSession } from "../actions"
22
import { test, expect } from "../fixtures"
3+
import { promptSelector } from "../selectors"
34

45
test("task tool child-session link does not trigger stale show errors", async ({ page, withBackendProject }) => {
56
test.setTimeout(120_000)
@@ -10,17 +11,16 @@ test("task tool child-session link does not trigger stale show errors", async ({
1011
}
1112
page.on("pageerror", onError)
1213

13-
await withBackendProject(async ({ gotoSession, trackSession, sdk }) => {
14-
await withSession(sdk, `e2e child nav ${Date.now()}`, async (session) => {
15-
trackSession(session.id)
16-
const child = await seedSessionTask(sdk, {
17-
sessionID: session.id,
18-
description: "Open child session",
19-
prompt: "Search the repository for AssistantParts and then reply with exactly CHILD_OK.",
20-
})
21-
trackSession(child.sessionID)
14+
try {
15+
await withBackendProject(async ({ gotoSession, trackSession, sdk }) => {
16+
await withSession(sdk, `e2e child nav ${Date.now()}`, async (session) => {
17+
const child = await seedSessionTask(sdk, {
18+
sessionID: session.id,
19+
description: "Open child session",
20+
prompt: "Search the repository for AssistantParts and then reply with exactly CHILD_OK.",
21+
})
22+
trackSession(child.sessionID)
2223

23-
try {
2424
await gotoSession(session.id)
2525

2626
const link = page
@@ -31,11 +31,11 @@ test("task tool child-session link does not trigger stale show errors", async ({
3131
await link.click()
3232

3333
await expect(page).toHaveURL(new RegExp(`/session/${child.sessionID}(?:[/?#]|$)`), { timeout: 30_000 })
34-
await page.waitForTimeout(1000)
35-
expect(errs).toEqual([])
36-
} finally {
37-
page.off("pageerror", onError)
38-
}
34+
await expect(page.locator(promptSelector)).toBeVisible({ timeout: 30_000 })
35+
await expect.poll(() => errs, { timeout: 5_000 }).toEqual([])
36+
})
3937
})
40-
})
38+
} finally {
39+
page.off("pageerror", onError)
40+
}
4141
})

0 commit comments

Comments
 (0)