Files
PaperClipAI/tests/runner-e2e/project-context-readback.spec.ts
T
Dotta a4308b9791 test(e2e): add browser project and task context journey (#18353)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - People group tasks into projects and use those projects to keep work
in context.
> - Deployment journeys already test task creation and assignment.
> - They do not yet prove that a new project and its task keep the same
saved relationship after reload.
> - This pull request adds an explicit browser journey for that gap.
> - Independent API reads and visible task properties must agree on the
exact records and selected settings.
> - The journey uses no model runs. Each deployment must pass before a
daily plan can include it.

## Linked Issues or Issue Description

**What existing behavior does this improve?**

The deployment-targeted Product E2E suite can now check project creation
and task context through the production dialogs.

**Current behavior**

The suite covers standalone task creation and assignment. It does not
prove that a newly created project contains the task selected in the
composer, or that the relation survives reload.

**Proposed behavior**

Select `project-task-context`. Create a unique project and a
self-assigned task with that project selected. Reopen the project, open
its task, and reload. Require exact saved company, project, title,
prompt, user assignment and standard work mode. Require no agent runs
before or after reload.

**Reason and benefit**

The same journey can qualify different deployments. It catches lost
project links and incorrect saved context without a model call or a
separate QA-only test implementation.

## What Changed

- Add the browser flow and an independent saved-state oracle with
positive and negative cases.
- Scope project and task navigation to unique links in the main content
so sidebar links cannot bypass list coverage.
- Bind reopened routes to saved project and task references. Verify the
main project heading and visible task content instead of a hidden
duplicate sidebar label. Keep browser failures as closed stage codes.
- Register the journey, its closed success checks, and its source files
in the definition hash. Default model-matrix selections stay unchanged.
- Associate qualification-report coverage with project lifecycle,
project task context and task creation. Reject retained browser-only
passes containing run IDs, provider/model metadata or native/legacy
execution.
- Add `project` to the existing durable ownership resource contract. A
target provider remains responsible for cleanup of its own project
fixtures.
- Update discovery expectations, the deployment journey guide and the
projects feature map.

## Verification

- Focused runner tests: 110 passed across the new oracle, discovery,
journey contracts and existing report subprocess checks. The first
sandboxed attempt blocked the existing report subprocess IPC socket; the
same selection passed with local IPC enabled.
- Late review regression checks: both report association and unexpected
browser-only model work reproduced as failing tests before the fix. The
final contract, saved-state oracle and report catalog selection passed
all 72 tests. Runner typecheck passed again after these changes.
- Browser readback: the old project-name selector failed in Chromium
when a hidden sidebar label preceded the production project heading. All
six browser regression cases now pass, including missing rows, wrong
project/task destinations and a wrong project property. The fixture uses
the production heading and task-row overlay conventions. Credential-free
Node 24 imports pass.
- Current-master integration: 208 focused catalog, project/search,
campaign and reporting tests passed. All nine project/search browser
regressions passed. Runner typecheck, map checks and Node 24 imports for
both journeys passed. The combined catalog, dispatch, source hashes,
success checks, browser-only guard and feature associations retain both
selections.
- The first deployment qualification created both owned records but
failed during later readback with a generic browser code.
Authentication, setup, collection and cleanup passed. That failed grade
is retained. The corrected source subsequently passed a fresh attached
QA campaign; the original failure remains preserved.
- New oracle alone: 7 tests passed after the final catalog mapping
change.
- `node scripts/check-maps.mjs` — passed; structural checks only.
- Credential-free `--suite product-journeys --list` — passed and lists
this browser journey.
- `node node_modules/typescript/bin/tsc -p
tests/runner-e2e/tsconfig.json` — passed after building required plugin,
runner and eval-kernel declarations. The initial fresh-checkout check
failed because those declarations were not built.
- All current-head CI gates passed, including repository typecheck,
tests, build and browser checks. Greptile reviewed the exact head at 5/5
with no unresolved findings. Fresh attached QA passed at
`f19557c9eebde14c2def37195d6b154c23eb9690`: [sanitized
report](https://pages.paperclip.ing/evals-qa-smoke-41119722363-1/). All
five independent checks and authentication, setup, journey, collection
and cleanup passed; zero agent runs started.

## Risks

- This adds an opt-in test and changes no production behavior or schema.
UI control changes can fail the journey.
- The target provider must support durable `project` ownership before it
can run this case. Project cleanup must settle owned tasks, reject
foreign linked tasks and archive the project while preserving history.
- Source registration alone does not include the candidate in any saved
daily plan. A fresh pass against that deployment is required first.

## Model Used

- OpenAI Codex, GPT-6 family, with code inspection and tool execution.
The host does not expose the exact underlying model revision or
context-window size.

## 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 (no duplicate journey found)
- [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
- [x] All Paperclip CI gates are green
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge
2026-10-30 15:07:21 -05:00

52 lines
4.2 KiB
TypeScript

import { expect, test, type Page } from "@playwright/test";
import { openProjectTaskThroughList, reopenProjectThroughList } from "./product-journeys/project-context-readback.js";
const origin = "https://project-context.test", projectName = "Journey QA project owned-campaign";
const taskTitle = "Journey QA project task owned-campaign", prompt = "I will review this task myself.";
const projectPath = "/QA/projects/owned-project-f962db91/issues", taskPath = "/QA/issues/QA-201";
const projectInput = { origin, issuePrefix: "QA", projectName, projectRef: "owned-project-f962db91" };
const taskInput = { origin, issuePrefix: "QA", projectName, taskTitle, prompt, taskRef: "QA-201" };
async function fixture(page: Page, changes: { missingProject?: boolean; wrongProject?: boolean; missingTask?: boolean; wrongTask?: boolean; wrongProperty?: boolean } = {}) {
const requests: string[] = [];
await page.route(`${origin}/**`, async route => {
const path = new URL(route.request().url()).pathname;
requests.push(path);
// Layout can retain a hidden sidebar label. ProjectDetail renders its
// production InlineEditor as h2; IssueRow has an overlay link with sr-only text.
const sidebar = `<aside hidden><a href="/wrong-sidebar">${projectName}</a><a href="/wrong-task">${taskTitle}</a><p>${prompt}</p></aside>`;
const body = path === "/QA/projects" ? `${sidebar}<main id="main-content">${changes.missingProject ? "" : `<a href="${changes.wrongProject ? "/QA/projects/foreign/issues" : projectPath}"><span>${projectName}</span></a>`}</main>`
: path.includes("/projects/") ? `${sidebar}<main id="main-content"><h2>${projectName}</h2>${changes.missingTask ? "" : `<div data-issue-row-id="owned-task" style="position:relative;min-height:44px"><a style="position:absolute;inset:0" href="${changes.wrongTask ? "/QA/issues/QA-999" : taskPath}"><span class="sr-only">Open QA-201: ${taskTitle}</span></a><span>${taskTitle}</span></div>`}</main>`
: `${sidebar}<main id="main-content"><h1>${taskTitle}</h1><p>${prompt}</p><button onclick="document.querySelector('#properties').hidden=false;this.hidden=true">Show properties</button><section id="properties" hidden><div data-property-row><span data-property-label="Project">Project</span><div data-property-value>${changes.wrongProperty ? "Other project" : projectName}</div></div></section></main>`;
await route.fulfill({ contentType: "text/html", body: `<style>.sr-only{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0,0,0,0)}</style>${body}` });
});
return requests;
}
test("reopens and reloads the owned project and task despite hidden duplicate sidebar labels", async ({ page }) => {
const requests = await fixture(page);
await reopenProjectThroughList({ ...projectInput, page });
await openProjectTaskThroughList({ ...taskInput, page });
expect(requests.filter(path => path === projectPath)).toHaveLength(2);
expect(requests.filter(path => path === taskPath)).toHaveLength(2);
await expect(page.locator("main#main-content [data-property-value]")).toContainText(projectName);
});
for (const [name, changes, code] of [
["missing project list row", { missingProject: true }, "project_context_project_open_failed"],
["wrong project route with a matching label", { wrongProject: true }, "project_context_project_open_failed"],
] as const) test(`rejects ${name} with a closed stage code`, async ({ page }) => {
await fixture(page, changes);
await expect(reopenProjectThroughList({ ...projectInput, page })).rejects.toMatchObject({ outcome: "failed", code, message: code });
});
for (const [name, changes, code] of [
["missing task in the project", { missingTask: true }, "project_context_project_task_open_failed"],
["wrong task route with a matching label", { wrongTask: true }, "project_context_project_task_open_failed"],
["wrong saved project property", { wrongProperty: true }, "project_context_project_property_failed"],
] as const) test(`rejects ${name} with a closed stage code`, async ({ page }) => {
await fixture(page, changes);
await page.goto(`${origin}${projectPath}`);
await expect(openProjectTaskThroughList({ ...taskInput, page })).rejects.toMatchObject({ outcome: "failed", code, message: code });
});