prompt-async.spec.ts 2.6 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576
  1. import { test, expect } from "../fixtures"
  2. import { promptSelector } from "../selectors"
  3. import { cleanupSession, sessionIDFromUrl, withSession } from "../actions"
  4. const text = (value: string | null) => (value ?? "").replace(/\u200B/g, "").trim()
  5. // Regression test for Issue #12453: the synchronous POST /message endpoint holds
  6. // the connection open while the agent works, causing "Failed to fetch" over
  7. // VPN/Tailscale. The fix switches to POST /prompt_async which returns immediately.
  8. test("prompt succeeds when sync message endpoint is unreachable", async ({ page, sdk, gotoSession }) => {
  9. test.setTimeout(120_000)
  10. // Simulate Tailscale/VPN killing the long-lived sync connection
  11. await page.route("**/session/*/message", (route) => route.abort("connectionfailed"))
  12. await gotoSession()
  13. const token = `E2E_ASYNC_${Date.now()}`
  14. await page.locator(promptSelector).click()
  15. await page.keyboard.type(`Reply with exactly: ${token}`)
  16. await page.keyboard.press("Enter")
  17. await expect(page).toHaveURL(/\/session\/[^/?#]+/, { timeout: 30_000 })
  18. const sessionID = sessionIDFromUrl(page.url())!
  19. try {
  20. // Agent response arrives via SSE despite sync endpoint being dead
  21. await expect
  22. .poll(
  23. async () => {
  24. const messages = await sdk.session.messages({ sessionID, limit: 50 }).then((r) => r.data ?? [])
  25. return messages
  26. .filter((m) => m.info.role === "assistant")
  27. .flatMap((m) => m.parts)
  28. .filter((p) => p.type === "text")
  29. .map((p) => p.text)
  30. .join("\n")
  31. },
  32. { timeout: 90_000 },
  33. )
  34. .toContain(token)
  35. } finally {
  36. await cleanupSession({ sdk, sessionID })
  37. }
  38. })
  39. test("failed prompt send restores the composer input", async ({ page, sdk, gotoSession }) => {
  40. await withSession(sdk, `e2e prompt failure ${Date.now()}`, async (session) => {
  41. const prompt = page.locator(promptSelector)
  42. const value = `restore ${Date.now()}`
  43. await page.route(`**/session/${session.id}/prompt_async`, (route) =>
  44. route.fulfill({
  45. status: 500,
  46. contentType: "application/json",
  47. body: JSON.stringify({ message: "e2e prompt failure" }),
  48. }),
  49. )
  50. await gotoSession(session.id)
  51. await prompt.click()
  52. await page.keyboard.type(value)
  53. await page.keyboard.press("Enter")
  54. await expect.poll(async () => text(await prompt.textContent())).toBe(value)
  55. await expect
  56. .poll(
  57. async () => {
  58. const messages = await sdk.session.messages({ sessionID: session.id, limit: 50 }).then((r) => r.data ?? [])
  59. return messages.length
  60. },
  61. { timeout: 15_000 },
  62. )
  63. .toBe(0)
  64. })
  65. })