diff --git a/tests/runner-e2e/README.md b/tests/runner-e2e/README.md index e37229a11ed..a5e956d3b89 100644 --- a/tests/runner-e2e/README.md +++ b/tests/runner-e2e/README.md @@ -176,6 +176,9 @@ fails the case instead of silently testing another phase. Restart continuity requires the agent to recall a phrase after the server restarts. The final prompt does not reveal that phrase. A generic successful reply after restart cannot pass this check. +The browser leaves the old development client before the server stops, then +opens the canonical chat route and waits for the composer. This avoids racing +Vite's automatic reconnect navigation against the test's explicit navigation. The blocker query requests a JSON status snapshot. It must name the current recorded blocker and report zero active runs independently of the task's blocked diff --git a/tests/runner-e2e/chat-restart.spec.ts b/tests/runner-e2e/chat-restart.spec.ts new file mode 100644 index 00000000000..d74c19d6ec1 --- /dev/null +++ b/tests/runner-e2e/chat-restart.spec.ts @@ -0,0 +1,31 @@ +import { createServer } from "node:http"; +import { expect, test } from "@playwright/test"; +import { restartChatServer } from "./chat-restart.js"; + +test("unloads the old browser client before server restart and opens the new document", async ({ page }) => { + let generation = 1; + const server = createServer((_req, res) => { + res.writeHead(200, { "content-type": "text/html" }); + res.end(``); + }); + await new Promise(resolve => server.listen(0, "127.0.0.1", resolve)); + const address = server.address(); + if (!address || typeof address === "string") throw new Error("Missing listener"); + const url = `http://127.0.0.1:${address.port}`; + try { + await page.goto(url); + await expect(page.getByTestId("composer")).toHaveValue("server generation 1"); + await restartChatServer(page, async () => { + expect(page.url(), "The old client must be gone before it can auto-reload").toBe("about:blank"); + server.closeAllConnections(); + await new Promise((resolve, reject) => server.close(error => error ? reject(error) : resolve())); + generation += 1; + await new Promise(resolve => server.listen(address.port, "127.0.0.1", resolve)); + }); + await page.goto(url, { waitUntil: "commit" }); + await expect(page.getByTestId("composer")).toHaveValue("server generation 2"); + } finally { + server.closeAllConnections(); + await new Promise((resolve, reject) => server.close(error => error ? reject(error) : resolve())); + } +}); diff --git a/tests/runner-e2e/chat-restart.ts b/tests/runner-e2e/chat-restart.ts new file mode 100644 index 00000000000..ef488ab3bd6 --- /dev/null +++ b/tests/runner-e2e/chat-restart.ts @@ -0,0 +1,7 @@ +import type { Page } from "@playwright/test"; + +/** Restart the test server without letting the old Vite client race navigation. */ +export async function restartChatServer(page: Page, restart: () => Promise) { + await page.goto("about:blank", { waitUntil: "commit", timeout: 30_000 }); + await restart(); +} diff --git a/tests/runner-e2e/playwright-support.config.ts b/tests/runner-e2e/playwright-support.config.ts index 517dcfa332c..ad609f75bc6 100644 --- a/tests/runner-e2e/playwright-support.config.ts +++ b/tests/runner-e2e/playwright-support.config.ts @@ -3,7 +3,7 @@ import { defineConfig } from "@playwright/test"; /** Browser-only harness regressions: no Paperclip instance or provider credentials. */ export default defineConfig({ testDir: ".", - testMatch: ["screenshot-readiness.spec.ts", "lost-send.spec.ts"], + testMatch: ["screenshot-readiness.spec.ts", "lost-send.spec.ts", "chat-restart.spec.ts"], workers: 1, retries: 0, timeout: 10_000, diff --git a/tests/runner-e2e/runner.spec.ts b/tests/runner-e2e/runner.spec.ts index 73ce46984c0..135f1036259 100644 --- a/tests/runner-e2e/runner.spec.ts +++ b/tests/runner-e2e/runner.spec.ts @@ -4,6 +4,7 @@ import { createTaskThroughUi, submitTaskReply } from "./user-actions.js"; import { runFirstTaskFlow, setupFirstTaskFixtures } from "./first-task-flow.js"; import { runChatFlow } from "./chat-flow.js"; +import { restartChatServer } from "./chat-restart.js"; import { createHash, randomBytes } from "node:crypto"; import { mkdir, readFile, rename, writeFile } from "node:fs/promises"; import path from "node:path"; @@ -832,7 +833,7 @@ for (const execution of executions) { } else if (execution.task.flow === "agent_chat") { const chat = await runChatFlow({ page, api, fixtures, execution, nonce, workspacePath, - restart: () => restartIsolatedPaperclipServer({ api, requestId: `chat-${nonce}`, deadlineAt: startedAtMs + deadlineMs }), + restart: () => restartChatServer(page, () => restartIsolatedPaperclipServer({ api, requestId: `chat-${nonce}`, deadlineAt: startedAtMs + deadlineMs })), observe: (chatIssue, chatRuns) => { issue = chatIssue; selectedRuns = chatRuns; }, capture: captureScreenshot, evidence: (name, data) => writeSanitizedJson(snapshotsDir, name, data, secrets),