Files
PaperClipAI/tests/runner-e2e/product-journeys/project-context-flow.ts
T
DottaandClaude Opus 5.5 aaaad177eb fix(e2e): follow renamed project and Artifacts V1 download controls (#19323)
## 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 `<a
href=".../content?download=1" aria-label="Download slugify.zip">` 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 <noreply@anthropic.com>
2026-11-05 14:19:50 -06:00

59 lines
4.3 KiB
TypeScript

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}`;
const taskTitle = `Journey QA project task ${campaignId}`;
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 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 });
const response = await withBrowserEvent(
() => page.waitForResponse(response => response.request().method() === "POST"
&& new URL(response.url()).pathname === `/api/companies/${company.id}/projects`),
() => dialog.getByRole("button", { name: "Create project", exact: true }).click());
if (response.status() !== 201) throw new JourneyError("failed", "project_ui_creation_failed");
const createdProject = await response.json();
if (typeof createdProject.id !== "string" || !createdProject.id || createdProject.companyId !== company.id || createdProject.name !== projectName) {
throw new JourneyError("failed", "project_ui_creation_mismatch");
}
await session.own({ kind: "project", name: projectName, id: createdProject.id, journeyId });
await expect(dialog).not.toBeVisible();
const projectRoute = `/api/projects/${createdProject.id}`;
const projectBeforeReload = await api.get<ProjectContextObservation["projectBeforeReload"]>(projectRoute);
const task = await createTaskThroughUi({ page, origin: api.origin, issuePrefix: company.issuePrefix,
agentName: "Me", title: taskTitle, prompt, workMode: "standard", projectName, requireExplicitTitle: true,
beforeDispatch: () => session.own({ kind: "issue", name: taskTitle, journeyId }) });
await session.own({ kind: "issue", name: taskTitle, id: task.issueId, journeyId });
const taskRoute = `/api/issues/${task.issueId}`;
const taskBeforeReload = await api.get<ProjectContextObservation["taskBeforeReload"]>(taskRoute);
const runsBeforeReload = await api.get<unknown[]>(`${taskRoute}/runs`);
// Open the project from its list, then open its task from that project's Tasks view.
await reopenProjectThroughList({ page, origin: api.origin, issuePrefix: company.issuePrefix, projectName,
projectRef: projectBeforeReload.urlKey ?? createdProject.id });
const projectTasks = await api.get<ProjectContextObservation["projectTasks"]>(`/api/companies/${company.id}/issues?projectId=${createdProject.id}&limit=100&offset=0`);
await openProjectTaskThroughList({ page, origin: api.origin, issuePrefix: company.issuePrefix, projectName, taskTitle, prompt,
taskRef: taskBeforeReload.identifier ?? task.issueId });
const observed: ProjectContextObservation = {
companyId: company.id, userId: context.user.id, projectId: createdProject.id, projectName,
taskId: task.issueId, taskTitle, prompt, projectBeforeReload, taskBeforeReload, projectTasks, runsBeforeReload,
projectAfterReload: await api.get(projectRoute), taskAfterReload: await api.get(taskRoute), runsAfterReload: await api.get(`${taskRoute}/runs`),
};
if (!projectContextEvidencePasses(observed)) throw new JourneyError("failed", "project_task_context_mismatch");
evidence.checks.push(...["browser-created-project-persisted", "browser-created-project-task-persisted",
"project-task-association-reloaded", "project-task-context-visible", "no-agent-work"].map(id => ({ id, passed: true })));
}