Files
PaperClipAI/tests/runner-e2e/product-journeys/cloud-onboarding.ts
T
DottaandPaperclip 970127a022 test: retain onboarding Connect failure evidence (#17529)
## Thinking Path

> - Paperclip manages agents and their work through normal company
controls.
> - Product E2E journeys check these controls through the browser and
public API.
> - Cloud onboarding uses one Connect action to validate the environment
and hire the first agent.
> - A failed Connect action previously left only a general journey error
in the report.
> - This pull request retains a closed diagnostic receipt for that
action.
> - The report can show the observed failure boundary without retaining
private response text.

## Linked Issues or Issue Description

**What existing behavior does this improve?**

Cloud onboarding Product E2E failure evidence and report detail.

**Current behavior**

A validation or hire failure can reach the journey as a general
operation failure. The report does not retain which Connect response
failed.

**Proposed behavior**

Observe only the same-origin, same-company validation and hire POST
requests made during one Connect action. Retain a closed receipt with
the checkpoint, response state, bounded HTTP status, and allowlisted
adapter, outcome, check code, level, and server code. The Get started
control and saved-agent readback remain required.

**Reason and benefit**

The next failure can be attributed to the exact observed browser/API
boundary. This change does not claim the cause of a historical failure.

**Breaking changes**

The new receipt is optional. Historical results remain valid. Explicit
validation and hire rejections now stop the journey with a precise
closed error code. This changes only E2E source; it does not change
product behavior or qualification gates.

## What Changed

- Add a bounded Connect observer with exact request matching and
listener cleanup.
- Retain known server codes from `details.code` or `code` without
response messages, hints, headers, request bodies, provider text, or
credentials.
- Validate the receipt as a closed schema and render its allowlisted
fields in existing report detail.
- Include the new helper in the journey definition hash.
- Add route, privacy, malformed-body, deadline, late-response,
delayed-failure, success, and historical compatibility tests. Document
the evidence boundary.

## Verification

- Focused Product E2E tests passed: 3 files, 58 tests on the review-fix
head.
- E2E typecheck, workspace typecheck, and full build passed on the
preceding implementation head. The review fix changes only report
labels, checkpoint timing, and the matching tests.
- The local full `pnpm test:run` command stopped progressing at the
general-server start header. Its owned session was interrupted. This is
not recorded as a pass.
- All 58 remote checks completed on the final review-fix head: 55
passed, one neutral, and two expected skips. Both Greptile and Paperclip
reviews are 5/5 with no unresolved review threads.
- No live campaign was run with this patch. The observed historical
failure is preserved.

## Risks

- Unknown or missing responses remain explicit diagnostic states. They
do not establish a product root cause.
- Response body reads stop after five seconds or the action deadline and
accept at most 64 KiB of JSON. Checks are capped at 32 retained entries.
- New server or check codes appear as `unrecognized` until the closed
allowlist is updated.
- The production launch classifier, native gateway qualification, and
Get started requirement are unchanged.

## Model Used

OpenAI Codex (GPT-6 family), with code execution and tool use. The host
does not expose the exact model ID or context window to this run; these
values are unavailable.

## 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
- [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

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-10-24 21:32:42 -07:00

92 lines
6.1 KiB
TypeScript

import { expect } from "@playwright/test";
import { createConnectionProof } from "../connection-evidence.js";
import { onboardingConnect } from "./onboarding-connect.js";
import { onboardingFirstTask } from "./onboarding-first-task.js";
import { JourneyError, type JourneyContext, type JourneyEvidence, type TargetSession } from "./types.js";
type Row = Record<string, any>;
export interface CloudOnboardingInput {
page: JourneyContext["page"];
campaignId: string;
deadlineAt: number;
gateway: { model: string; baseUrl: string; apiKey: string };
maxRuns: number;
evidence: JourneyEvidence;
own: TargetSession["own"];
beforeWorkspace(name: string): Promise<void>;
workspaceCreated(): Promise<JourneyContext>;
/** The saved route must pass a deny-only runtime probe before Connect can spend. */
qualify(context: JourneyContext, connectionId: string): Promise<void>;
verifyExecution(context: JourneyContext, runIds: string[]): Promise<void>;
phase(phase: "setup" | "agent"): Promise<void>;
observeRun(run: Row): Promise<void>;
}
/** Measured actions use normal Cloud and tenant controls; infrastructure stays in the provider. */
export async function cloudOnboarding(input: CloudOnboardingInput): Promise<JourneyContext> {
const { page, evidence, campaignId } = input;
const name = `Journey QA ${campaignId}`;
const firstRunName = page.getByLabel("Name", { exact: true });
const organizationName = page.getByLabel("Organization name", { exact: true });
const createLink = page.getByRole("link", { name: /^(Create (?:a new )?organization|\+ New organization)$/ }).first();
// The first organization uses the signup Name/Next form. Later organizations
// use Organization name/Create organization, reached from the managed portfolio.
await expect(firstRunName.or(organizationName).or(createLink).first()).toBeVisible();
if (!await firstRunName.isVisible() && !await organizationName.isVisible()) {
await createLink.click();
await expect(organizationName).toBeVisible();
}
const firstRun = await firstRunName.isVisible();
await (firstRun ? firstRunName : organizationName).fill(name);
await input.beforeWorkspace(name);
await page.getByRole("button", { name: firstRun ? "Next" : "Create organization", exact: true }).click();
const context = await input.workspaceCreated();
const { api, company } = context;
evidence.deployment = context.identity;
evidence.checks.push({ id: "fresh-workspace", passed: true });
await input.phase("setup");
await expect(page.getByRole("heading", { name: "Create your first agent", exact: true })).toBeVisible({ timeout: 180_000 });
await input.own({ kind: "agent", name });
await page.getByLabel("Agent name", { exact: true }).fill(name);
await page.getByRole("button", { name: "Next", exact: true }).click();
await page.getByRole("button", { name: "Use API key instead", exact: true }).click();
await page.getByRole("radio", { name: /OpenAI.*API/ }).click();
await page.getByRole("button", { name: "Use a custom gateway", exact: true }).click();
const dialog = page.getByRole("dialog", { name: "Custom gateway connection", exact: true });
await dialog.getByLabel("Provider URL", { exact: true }).fill(input.gateway.baseUrl);
await dialog.getByLabel("API key", { exact: true }).fill(input.gateway.apiKey);
await dialog.getByRole("button", { name: "Change", exact: true }).click();
await dialog.getByLabel("Model IDs", { exact: true }).fill(input.gateway.model);
const connectionName = `My ${new URL(input.gateway.baseUrl).hostname}`;
await input.own({ kind: "connection", name: connectionName });
await dialog.getByRole("button", { name: "Save connection", exact: true }).click();
await expect(dialog).not.toBeVisible();
const connections = (await api.get(`/api/companies/${company.id}/ai-connections`)).connections as Row[];
const connection = connections.find(c => c.name === connectionName && c.provider === "openai" && c.method === "api_key" && c.status === "connected");
if (!connection || connections.length !== 1) throw new JourneyError("failed", "onboarding_connection_not_saved");
await input.own({ kind: "connection", name: connectionName, id: connection.id });
await page.getByRole("combobox", { name: "Model", exact: true }).selectOption(input.gateway.model);
await page.locator("summary").filter({ hasText: "Advanced" }).click();
await page.getByRole("button", { name: "Runner", exact: true }).click();
await page.getByRole("option", { name: /^Paperclip Runner/ }).click();
await input.qualify(context, connection.id);
// Connect performs the production probe and agent creation. Never press Retry.
const connect = await onboardingConnect({ page, origin: api.origin, companyId: company.id, deadlineAt: input.deadlineAt,
retain: value => { evidence.onboardingConnect = value; } });
const agents = await api.get<Row[]>(`/api/companies/${company.id}/agents`);
const agent = agents.find(a => a.name === name);
if (!agent || agents.length !== 1 || agent.adapterType !== "paperclip_runner" || agent.adapterConfig?.provider !== "codex" || agent.adapterConfig?.model !== input.gateway.model) throw new JourneyError("failed", "onboarding_agent_configuration_mismatch");
connect.checkpoint = "agent-verification";
await input.own({ kind: "agent", name, id: agent.id });
evidence.checks.push({ id: "ui-connection-and-native-agent", passed: true });
await input.own({ kind: "issue", name: "Paperclip onboarding" });
await page.getByRole("button", { name: "Get started", exact: true }).click();
await input.phase("agent");
const completed = await onboardingFirstTask({ context, agent: { id: agent.id }, connectionId: connection.id,
proof: createConnectionProof(), campaignId, maxRuns: input.maxRuns, deadlineAt: input.deadlineAt,
own: input.own, observeRun: input.observeRun, onArtifact: artifact => { evidence.artifact = artifact; } });
await input.verifyExecution(context, [completed.artifact.originatingRunId]);
evidence.checks.push({ id: "first-task-complete", passed: true },
{ id: "downloadable-artifact-verified", passed: true }, { id: "native-trace-verified", passed: true });
return { ...context, startingState: "onboarding" };
}