From 1932fd66cb1d9176fa22a0c07caf97886f2beeb8 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Wed, 26 Aug 2026 13:39:23 -0700 Subject: [PATCH] test(ui): drop the resurrected duplicate attachment-URL e2e proof (#130343) #127114 replaced the exact-revoke-count attachment preview test with the invariant-based proof in new-session-page.attachment-url-lifecycle.e2e.test.ts, deleting the old block from new-session-page.prompt-attachments.e2e.test.ts. #124301 branched before that deletion, so its merge resurrected the stale copy and main shipped two tests with the same name. The resurrected copy asserts literal cumulative revokeObjectURL counts. Those counts are not an invariant: on remount the durable IndexedDB draft restore (draft-persistence.ts restoreScope) can re-hydrate the attachment into a fresh id and object URL, then release the copy the in-memory route handoff carried, producing one extra matched createObjectURL/revokeObjectURL pair. Reproduced with stack capture under 6x CDP CPU throttling: revoked reached 3 before the Remove click and 4 after, which is the "expected 4 to be 3" seen in CI. No leak and no double-release: releaseChatAttachmentPayload deletes the payload map entry before revoking, so every URL is created once and revoked once. The surviving lifecycle test asserts exactly that (active URLs, duplicateRevocations and unknownRevocations both zero) and pins the hydration step deterministically, so deleting the duplicate loses no coverage. --- ...ession-page.prompt-attachments.e2e.test.ts | 108 ------------------ 1 file changed, 108 deletions(-) diff --git a/ui/src/e2e/new-session-page.prompt-attachments.e2e.test.ts b/ui/src/e2e/new-session-page.prompt-attachments.e2e.test.ts index 9704a8141c9c..5ef82efc4c69 100644 --- a/ui/src/e2e/new-session-page.prompt-attachments.e2e.test.ts +++ b/ui/src/e2e/new-session-page.prompt-attachments.e2e.test.ts @@ -639,114 +639,6 @@ suite.define(() => { }); }); - it("releases pasted image previews after remove, reset, restored removal, and success", async () => { - await withNewSessionPage(async (page) => { - await page.addInitScript(() => { - const createObjectURL = URL.createObjectURL.bind(URL); - const revokeObjectURL = URL.revokeObjectURL.bind(URL); - const proof = { created: 0, revoked: 0 }; - (globalThis as unknown as { attachmentUrlProof: typeof proof }).attachmentUrlProof = proof; - URL.createObjectURL = (blob: Blob) => { - proof.created += 1; - return createObjectURL(blob); - }; - URL.revokeObjectURL = (url: string) => { - proof.revoked += 1; - revokeObjectURL(url); - }; - }); - await installMockGateway(page, { - methodResponses: { - "agents.list": { - defaultId: "main", - mainKey: "main", - scope: "agent", - agents: [ - { id: "main", name: "Main" }, - { id: "writer", name: "Writer" }, - ], - }, - "sessions.create": { key: "agent:main:preview-cleanup", runStarted: true }, - }, - }); - const proof = () => - page.evaluate( - () => - (globalThis as unknown as { attachmentUrlProof: { created: number; revoked: number } }) - .attachmentUrlProof, - ); - const navigate = (routeId: string, search = "") => - page.evaluate( - ({ targetRouteId, targetSearch }) => { - const app = document.querySelector("openclaw-app") as HTMLElement & { - runtime?: { - context: { - navigate: (routeId: string, options?: { search?: string }) => void; - }; - }; - }; - if (!app.runtime) { - throw new Error("OpenClaw application runtime is unavailable"); - } - app.runtime.context.navigate(targetRouteId, { search: targetSearch }); - }, - { targetRouteId: routeId, targetSearch: search }, - ); - await page.goto(`${suite.server.baseUrl}new`); - const composer = page.locator(".new-session-page__message"); - - await pastePng(composer); - await page.getByRole("img", { name: "pixel.png" }).waitFor(); - await page.getByRole("button", { name: "Remove attachment" }).click(); - await expect.poll(async () => (await proof()).revoked).toBe(1); - - await pastePng(composer); - await page.getByRole("img", { name: "pixel.png" }).waitFor(); - const agentDropdown = page.locator(".new-session-page__select--agent wa-dropdown"); - await page.locator(".new-session-page__select--agent .agent-select__trigger").click(); - await expect - .poll(() => - agentDropdown.evaluate((dropdown) => (dropdown as HTMLElement & { open: boolean }).open), - ) - .toBe(true); - await navigate("new-session", "?agent=main&catalog=missing"); - await expect - .poll(() => - page.evaluate( - () => - ( - document.querySelector(".new-session-page__select--agent wa-dropdown") as - | (HTMLElement & { open: boolean }) - | null - )?.open ?? false, - ), - ) - .toBe(false); - await expect.poll(() => page.locator(".chat-attachment-thumb").count()).toBe(0); - await expect.poll(async () => (await proof()).revoked).toBe(2); - - await navigate("new-session"); - await composer.waitFor(); - await page.getByRole("img", { name: "pixel.png" }).waitFor(); - await navigate("chat"); - await page.waitForURL((url) => url.pathname.endsWith("/chat")); - await expect.poll(async () => (await proof()).revoked).toBe(2); - - await navigate("new-session"); - await composer.waitFor(); - await expect.poll(() => page.locator(".chat-attachment-thumb").count()).toBe(1); - await page.getByRole("button", { name: "Remove attachment" }).click(); - await expect.poll(async () => (await proof()).revoked).toBe(3); - await pastePng(composer); - await page.getByRole("img", { name: "pixel.png" }).waitFor(); - await page.getByRole("button", { name: "Start session" }).click(); - await page.waitForURL( - (url) => url.pathname === controlUiSessionPath("agent:main:preview-cleanup"), - ); - await expect.poll(async () => await proof()).toEqual({ created: 4, revoked: 4 }); - }); - }); - it("locks the submitted draft until creation settles and restores it after failure", async () => { await withNewSessionPage(async (page) => { const sessionKey = "agent:main:locked-new-session-draft";