From 1fe2fe2751f70a58953232dbf3ca485e00db5598 Mon Sep 17 00:00:00 2001 From: Dotta Date: Mon, 21 Sep 2026 14:35:51 -0500 Subject: [PATCH] test: unload chat client before restarting its server Co-Authored-By: Paperclip --- tests/runner-e2e/README.md | 3 ++ tests/runner-e2e/chat-restart.spec.ts | 31 +++++++++++++++++++ tests/runner-e2e/chat-restart.ts | 7 +++++ tests/runner-e2e/playwright-support.config.ts | 2 +- tests/runner-e2e/runner.spec.ts | 3 +- 5 files changed, 44 insertions(+), 2 deletions(-) create mode 100644 tests/runner-e2e/chat-restart.spec.ts create mode 100644 tests/runner-e2e/chat-restart.ts 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),