From aaaad177eb116f64f4e74af33e110590207b996b Mon Sep 17 00:00:00 2001 From: Dotta <34892728+cryppadotta@users.noreply.github.com> Date: Thu, 5 Nov 2026 14:19:50 -0600 Subject: [PATCH] fix(e2e): follow renamed project and Artifacts V1 download controls (#19323) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work > - Product E2E journeys drive the real UI and run in the QA staging smoke plan > - Two UI changes landed after the previous QA deployment: the Projects toolbar action is now "Create project" (#18716), and Artifacts V1 is on for every instance (#19164), so attachment rows offer download as an icon link with an `aria-label` > - The journeys still clicked "Add Project" and required a visible "Download" text, so they timed out before reaching the server > - This pull request accepts both the old and the current controls > - The benefit is that project-task-context, build-revise and stop-redirect measure the product again on current deployments ## Linked Issues or Issue Description **What happened?** On the QA deployment of cb8df178c8, three journeys failed with the generic `journey_operation_failed`: project-task-context, build-revise and stop-redirect. Records on the stack show that build-revise and stop-redirect delivered their ZIP and finished. The user's next interaction never reached the server. **Expected behavior** The journeys use the controls that the current UI shows. **Steps to reproduce** 1. Run the `project-task-context` product journey against a managed local target at cb8df178c8. 2. Playwright waits 30 seconds for `getByRole('button', { name: 'Add Project', exact: true })` and times out. 3. For downloads, an Artifacts V1 row renders `` with only an icon. `filter({ hasText: /Download/i })` matches no element. **Paperclip version or commit** master at 03eb49053f; deployed QA app cb8df178c8. ## What Changed - `project-context-flow.ts`: open the new-project dialog with the button named "Add Project" or "Create project". - `story-artifact.ts`: find the attachment download link by "Download" text or by an `aria-label` that starts with "Download". The attachment ID is still required in the link. ## Verification - Local managed target at cb8df178c8: project-task-context failed with the old selector (Playwright timeout on "Add Project") and passed 1/1 with this change. - A Chromium check with Playwright on V1, legacy and no-download markup: the old locator finds 0 elements on V1 markup. The new locator finds the download link on V1 and legacy markup, and finds nothing on markup without a download link. - `npx vitest run --config tests/runner-e2e/vitest.config.ts tests/runner-e2e/product-journeys tests/runner-e2e/story-artifact`: 47 files, 1237 tests passed. - Not run live on QA yet. QA uses the pinned harness (`QA_SMOKE_HARNESS_SHA`), so the pin must move to a commit that contains this change. ## Risks - Low risk. Only the selectors change. All assertions, download checks and artifact checks stay the same. - The download link must still point at the exact attachment's content route. The "Open raw" icon link is not selected. ## Model Used - Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, via Claude Code with tool use (shell, browser, code editing). Extended reasoning was used. ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used (with version and capability details) - [x] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work - [x] I have searched GitHub for duplicate or related PRs and linked them above - [x] I have either (a) linked existing issues with `Fixes: #` / `Closes #` / `Refs #` OR (b) described the issue in-PR following the relevant issue template - [x] I have not referenced internal/instance-local Paperclip issues or links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip` URLs) - [x] My branch name describes the change (e.g. `docs/...`, `fix/...`) and contains no internal Paperclip ticket id or instance-derived details - [x] I have run tests locally and they pass - [x] I have added or updated tests where applicable - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [ ] All Paperclip CI gates are green - [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups - [x] I will address all Greptile and reviewer comments before requesting merge 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 --- tests/runner-e2e/current-ui-controls.spec.ts | 31 +++++++++++++++++++ tests/runner-e2e/playwright-support.config.ts | 2 +- .../product-journeys/project-context-flow.ts | 9 ++++-- tests/runner-e2e/story-artifact.ts | 9 +++++- 4 files changed, 47 insertions(+), 4 deletions(-) create mode 100644 tests/runner-e2e/current-ui-controls.spec.ts diff --git a/tests/runner-e2e/current-ui-controls.spec.ts b/tests/runner-e2e/current-ui-controls.spec.ts new file mode 100644 index 00000000000..1071f312a60 --- /dev/null +++ b/tests/runner-e2e/current-ui-controls.spec.ts @@ -0,0 +1,31 @@ +import { expect, test } from "@playwright/test"; +import { newProjectButton } from "./product-journeys/project-context-flow.js"; +import { storyArtifactDownloadLink } from "./story-artifact.js"; + +const id = "11111111-2222-4333-8444-555555555555"; + +test("finds the attachment download link in legacy and Artifacts V1 markup only", async ({ page }) => { + const markup = { + legacy: `Download`, + // Artifacts V1: an icon-only "Open raw" link precedes the icon-only download link. + v1: `slugify.zip` + + ``, + none: `Open` + + `Download`, + }; + for (const kind of ["legacy", "v1"] as const) { + await page.setContent(markup[kind]); + await expect(storyArtifactDownloadLink(page, id)).toHaveAttribute("href", `/api/attachments/${id}/content?download=1`); + } + await page.setContent(markup.none); + await expect(storyArtifactDownloadLink(page, id)).toHaveCount(0); +}); + +test("opens new-project creation with the previous or current toolbar label only", async ({ page }) => { + for (const label of ["Add Project", "Create project"]) { + await page.setContent(``); + await expect(newProjectButton(page)).toHaveText(label); + } + await page.setContent(``); + await expect(newProjectButton(page)).toHaveCount(0); +}); diff --git a/tests/runner-e2e/playwright-support.config.ts b/tests/runner-e2e/playwright-support.config.ts index 3e2887eed4c..706d0d71ea1 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: ["hire-delegate-composer.spec.ts", "question-resume-ui.spec.ts", "clarification-readback.spec.ts", "project-context-readback.spec.ts", "skill-studio-flow.spec.ts", "native-task-reply.spec.ts", "screenshot-readiness.spec.ts", "service-worker-reload.spec.ts", "lost-send.spec.ts", "chat-restart.spec.ts", "continuation-approval.spec.ts", "settings-toggle.spec.ts", "browser-bootstrap-diagnostics.spec.ts", "service-worker-module-loading.spec.ts", "browser-bootstrap-recovery.spec.ts", "blocker-input.spec.ts", "native-blocker-visible.spec.ts", "copilot-permission-card.spec.ts", "kimi-permission-card.spec.ts", "github-review-check.spec.ts", "antigravity-journey-browser.spec.ts", "task-search-filter.spec.ts", "task-document-navigation.spec.ts"], + testMatch: ["hire-delegate-composer.spec.ts", "question-resume-ui.spec.ts", "clarification-readback.spec.ts", "project-context-readback.spec.ts", "skill-studio-flow.spec.ts", "native-task-reply.spec.ts", "screenshot-readiness.spec.ts", "service-worker-reload.spec.ts", "lost-send.spec.ts", "chat-restart.spec.ts", "continuation-approval.spec.ts", "settings-toggle.spec.ts", "browser-bootstrap-diagnostics.spec.ts", "service-worker-module-loading.spec.ts", "browser-bootstrap-recovery.spec.ts", "blocker-input.spec.ts", "native-blocker-visible.spec.ts", "copilot-permission-card.spec.ts", "kimi-permission-card.spec.ts", "github-review-check.spec.ts", "antigravity-journey-browser.spec.ts", "task-search-filter.spec.ts", "task-document-navigation.spec.ts", "current-ui-controls.spec.ts"], workers: 1, retries: 0, timeout: 10_000, diff --git a/tests/runner-e2e/product-journeys/project-context-flow.ts b/tests/runner-e2e/product-journeys/project-context-flow.ts index ae83a781a5c..493723a6797 100644 --- a/tests/runner-e2e/product-journeys/project-context-flow.ts +++ b/tests/runner-e2e/product-journeys/project-context-flow.ts @@ -1,9 +1,14 @@ -import { expect } from "@playwright/test"; +import { expect, type Page } from "@playwright/test"; import { createTaskThroughUi, withBrowserEvent } from "../user-actions.js"; import { projectContextEvidencePasses, type ProjectContextObservation } from "./project-context-evidence.js"; import { openProjectTaskThroughList, reopenProjectThroughList } from "./project-context-readback.js"; import { JourneyError, type JourneyContext, type JourneyEvidence, type TargetSession } from "./types.js"; +/** The toolbar action was renamed from "Add Project" to "Create project"; accept both deployed labels. */ +export function newProjectButton(page: Page) { + return page.getByRole("button", { name: /^(?:Add Project|Create project)$/ }).first(); +} + export async function projectTaskContext(context: JourneyContext, session: TargetSession, campaignId: string, evidence: JourneyEvidence) { const { page, api, company } = context; const projectName = `Journey QA project ${campaignId}`; @@ -11,7 +16,7 @@ export async function projectTaskContext(context: JourneyContext, session: Targe const prompt = `Keep this task assigned to me in ${projectName}. I will review it myself.`; const journeyId = "project-task-context" as const; await page.goto(`${api.origin}/${company.issuePrefix}/projects`); - await page.getByRole("button", { name: "Add Project", exact: true }).first().click(); + await newProjectButton(page).click(); const dialog = page.getByRole("dialog"); await dialog.getByRole("textbox", { name: "Project name", exact: true }).fill(projectName); await session.own({ kind: "project", name: projectName, journeyId }); diff --git a/tests/runner-e2e/story-artifact.ts b/tests/runner-e2e/story-artifact.ts index de2e411903e..fa6c7c91700 100644 --- a/tests/runner-e2e/story-artifact.ts +++ b/tests/runner-e2e/story-artifact.ts @@ -34,11 +34,18 @@ export async function runArtifactOracle(file: string | null, mode: "base" | "sep } /** Download through the visible task control; attachment metadata is not a content oracle. */ +/** Older attachment cards label the link "Download"; Artifacts V1 rows use an icon link named by aria-label. */ +export function storyArtifactDownloadLink(page: Page, attachmentId: string) { + const content = `a[href*="/api/attachments/${attachmentId}/content"]`; + return page.locator(content).filter({ hasText: /Download/i }) + .or(page.locator(`${content}[aria-label^="Download" i]`)).first(); +} + export async function downloadStoryArtifact(input: { page: Page; attachmentId: string; directory: string; phase: string; mode: "base" | "separator" | "max-length"; deadlineAt: number; }) { - const link = input.page.locator(`a[href*="/api/attachments/${input.attachmentId}/content"]`).filter({ hasText: /Download/i }).first(); + const link = storyArtifactDownloadLink(input.page, input.attachmentId); await expect(link).toBeVisible({ timeout: 30_000 }); const [download] = await Promise.all([input.page.waitForEvent("download", { timeout: 30_000 }), link.click()]); await mkdir(input.directory, { recursive: true, mode: 0o700 });