Files
PaperClipAI/tests/runner-e2e/product-journeys/project-context-flow.ts
T
DottaandPaperclip e5713d95c6 test(e2e): add scheduled routine journey (#18728)
## Thinking Path

> - Paperclip manages agent work through tasks, projects, and routines.
> - Daily QA must exercise the staging app through normal browser and
API surfaces.
> - A schedule must create real work and retain its result and history.
> - Pausing that schedule must prevent the next invocation.
> - The routine journey in #18652 passed in QA, while its separate
completion journey has not passed.
> - This PR delivers the routine test independently and preserves its
measured flow and assertions.

## Linked Issues or Issue Description

Refs: #18652. This is the routine-only delivery from that PR. The
original completion implementation and its failed attempts remain on
#18652.

## What Changed

- Add the target-independent `routine-scheduled-run` journey with one
native run and seven required checks.
- Start from the measured routine source
`ac4efb7c6f4782ea806661683c656fd2b4cc3189`. Keep its request, seven
success assertions, one native-run limit, and 65-second pause check.
- Bound each observed GET by the original remaining journey deadline.
Preserve the normal API timeout for other callers. Keep the original
routine error if final trigger disabling also fails, save a closed
secondary receipt, and fail an otherwise successful journey when
disabling fails.
- Narrow the shared output-reader type to its one-argument GET
capability without changing its runtime code.
- Register only this case in the catalog, types, feature associations,
dispatcher, and definition hash. Keep the strict catalog expectation in
sync.
- Retain the real Routines component controls and the existing
project-flow coverage for the shared browser helper.
- Document resource cleanup and the 65-second pause observation.
Preserve the current routine map content.

## Verification

- The original source passed [QA staging run
41567574185](https://pages.paperclip.ing/evals-qa-smoke-41567574185-1/).
TestOps launch 1657 / result 3431 maps to case 718. All seven checks and
five phases passed. One scheduler-bound native run, saved output
attribution, the full pause window, and cleanup were verified.
- Current source `8f62137f130f59fd3e39276e3d60185ddff24517` passed
[fresh QA staging run
41582706162](https://pages.paperclip.ing/evals-qa-smoke-41582706162-1/)
against Core `09162fe893b464dd2ccdafd14bd3ef1611202a86`. TestOps launch
1855. All seven checks, five phases, and all three workflow jobs passed.
The run used one scheduler-bound native turn and observed 65.352 seconds
after pause and completion. Canonical evidence collection and normal
cleanup completed, with no active operations and the owned agent
terminal.
- This fresh pass covers the reviewed deadline and failure-reporting
changes in the current source. The earlier ac4 pass and the
byte-preserving b940 extraction remain historical records under their
original conditions; neither failed attempts nor prior source conditions
are regraded.
- Causal controls: six assertions failed on the old behavior, then all
62 focused controls passed after the repair. Local: 834 related support
tests, 41 shared chat/output-reader tests, and 26 unchanged real
component controls passed. Runner E2E typecheck, recursive typecheck,
build, maps, token gates, and `git diff --check` passed. The temporary
output-reader type mismatch exposed by typecheck was corrected by
narrowing the capability type.
- The first restricted build/typecheck invocations failed because local
tsx IPC was denied. A restricted support run also lacked the report CLI
output. The same source passed with normal owned local IPC allowed; the
original logs remain retained.
- The original head's hosted e2e shard 2/8 failed at the unchanged
mobile preference frame-count test: expected more than five frames,
observed five. The cause is unknown. Its original log remains retained;
it is not labeled a proven flake. No CI rerun was requested.
- Current-head full CI run 41580423918, attempt 1, passed. The fresh
review is 5/5 with zero unresolved threads. The broad local suite was
not repeated. The earlier full local run recorded in #18643 had four
failures and did not reach 164 serialized groups; this PR does not claim
an all-local pass.
- Prior failed routine attempts remain recorded. They exposed composer
focus, collapsed schedule controls, pending browser events, and Markdown
serialization problems. The measured later pass does not regrade those
attempts.

## Risks

- The shared browser-event helper also serves task creation and project
creation. Its regression tests and existing consumers remain included.
- The staging pass qualifies the captured source and configuration. The
coordinator retains source merging, daily pinning, and plan selection.
- The repository test and the unpassed completion test are not
registered by this PR.

## Model Used

OpenAI GPT-6 through Codex, with tool use and code execution. The exact
deployment ID and context window are not exposed by this session.

## 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 issue/PR references)

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-11-02 23:25:56 -06:00

54 lines
4.1 KiB
TypeScript

import { expect } 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";
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 page.getByRole("button", { name: "Add Project", exact: true }).first().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 })));
}