Files
PaperClipAI/tests/e2e/onboarding.spec.ts
T
TonioandClaude Opus 5 eb86fcd498 The agent, drawn as itself (#12274)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - A new customer's first session ends in the tenant's agent arc:
create an agent, connect a model, review
> - Walking it turned up questions the arc had no business asking — a
role picker using a vocabulary the customer has not been given, a model
picker asking them to judge models they have not met — and chrome
restating what they had just watched happen
> - Each one costs a first-session customer attention at the exact
moment they are deciding what this product is
> - This pull request cuts the arc to what it must ask, and draws the
agent as itself so the arc has a visible subject
> - The benefit is three steps that each ask one thing, ending on an
agent that is visibly ready

## Linked Issues or Issue Description

No public issue exists. The changes come from walking the sign-up arc
end to end.

**What happened:**
The agent step asks for a role from a fixed enum before asking for a
name. The model step shows two "Recommended" badges (on both options),
an "Adapter type" eyebrow, and a model picker. The review step lists a
three-row checklist of work the customer just performed. The progress
strip is a full-width segmented bar.

**Expected behavior:**
The agent step asks for a name. The model step offers the two harnesses
and hides the rest behind advanced settings. The review step says the
agent is ready. The strip counts three discrete steps.

**Steps to reproduce:**
1. Sign up and enter the tenant wizard on the agent arc.
2. Observe the role select above the optional name field.
3. Continue to the model step: both options carry a "Recommended" badge,
and a model picker sits below.
4. Continue to review: a checklist restates the organization name,
agent, and model.

**Additional context:**
The brand pill assets (`pill-1-dormant.svg`, `pill-1-alive.svg`) are
transcribed verbatim into a component rather than approximated. The role
removal exposed a latent silent-failure path — see Risks.

## What Changed

- `PillGuy` renders the brand pill in two states; the arc holds one
instance, dormant through create and connect, alive on review.
- The agent step asks for a name only. The name is required; the role
picker is gone.
- `DEFAULT_AGENT_ROLE` (`general`) backs every onboarding hire, and
`agentRole` now defaults to it rather than empty.
- The model step drops both "Recommended" badges, the "Adapter type"
eyebrow, and the model picker; "More Agent Adapter Types" becomes
"Advanced settings"; the sub-line becomes "Paperclip works with your
existing subscription or API keys."
- The review step drops its checklist; the heading becomes "Let's get
started..." with "[name] is ready to work!".
- The progress strip renders three left-aligned dots at the previous
gap.
- Five e2e specs and both wizard unit suites migrate off
`#onboarding-agent-role`.

## Verification

Run the tenant suite:

```
cd ui && npx vitest run
```

- 4398 tests pass across 474 files; `npx tsc --noEmit` clean.
- Walked live in a local instance: agent step (dots, dormant pill, name
placeholder), model step (no badges/eyebrow/picker, "Advanced
settings"), review (pill alive, new copy, no checklist).
- The retargeted role test asserts the hire payload carries `role:
"general"` and the typed name — it is the test that catches the silent
failure below.

## Risks

- **A latent silent failure, now closed.** `handleGiveHeartbeat` returns
early when `agentRole` is empty. With the picker removed and no default,
Connect would have hired nobody and shown no error. The default closes
it; the guard stays for any future path that clears the role.
- **Behavioral change:** every onboarding hire is filed as `general`
rather than a chosen role. The role remains editable in the app.
- **Behavioral change:** the model is no longer chosen during
onboarding. Every adapter offered here resolves its own default in
`buildAdapterConfig`, and the model is changeable later.
- **Assets:** the pill carries its own gradient fills and does not
follow the theme. That is deliberate — the agent looks like itself on
either ground.

## Model Used

Claude Opus 5 (`claude-opus-5`) via Claude Code, with tool use and code
execution.

## 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
- [x] My branch name describes the change and contains no internal
Paperclip ticket id
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [ ] 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

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-26 21:39:55 -07:00

99 lines
4.1 KiB
TypeScript

import { test, expect } from "@playwright/test";
/**
* E2E: Onboarding wizard flow (NUX Phase 2 expanded wizard).
*
* The wizard now opens on a front door (path picker) and the "Create a new
* company" path runs:
* Step 0 — Front door (Create a new company / Level up existing)
* Step 1a — Name your organization (creates the company)
* Step 2 — Hire your team lead (adapter picker)
* Step 3+ — Launch celebration → CEO chat → hiring plan → orientation
*
* This test covers the deterministic, LLM-free core: it drives the front door
* through company naming (which creates the company) and verifies the wizard
* advances to the team-lead step without asking for a mission.
*
* The tail (CEO chat at step 4, hiring-plan generation at step 5, final
* landing) depends on a live LLM and is verified separately during manual /
* LLM-backed QA — see PAP-50. Surface-level rendering of every step is
* snapshotted by nux-phase4-screenshots.spec.ts.
*/
const COMPANY_NAME = `E2E-Test-${Date.now()}`;
test.describe("Onboarding wizard", () => {
test("create-company path: naming creates the company, and no goal is invented", async ({
page,
}) => {
const pageErrors: string[] = [];
page.on("pageerror", (err) => pageErrors.push(err.message));
// New-NUX surfaces are flag-gated default-OFF (PAP-136/137/138): turn the
// experimental flag on for this throwaway instance before driving them.
const flagRes = await page.request.patch("/api/instance/settings/experimental", {
data: { enableConferenceRoomChat: true },
});
expect(flagRes.ok()).toBe(true);
await page.goto("/onboarding");
// The wizard may open on a launcher card or directly on the capsule
// wizard; the front door (step 0) requires a click into the create path.
const startBtn = page.getByRole("button", {
name: /Start Onboarding|New Company|Add Agent/,
});
if (await startBtn.count()) {
await startBtn.first().click();
}
const createCard = page.getByRole("button", { name: /Build a new company/ });
if (await createCard.count()) {
await createCard.first().click();
}
// Step 1 — Name your organization.
await expect(
page.getByRole("heading", { name: "What is the name of your organization?" }),
).toBeVisible({ timeout: 15_000 });
await page.getByPlaceholder("e.g. Northwind Labs").fill(COMPANY_NAME);
await page.getByRole("button", { name: /^Continue/ }).click();
// Step 1's "Next" now creates the company and goes straight to the agent.
// The mission step used to sit between them and do the creating; onboarding
// no longer asks for the mission, which is collected later in the app.
await page.waitForSelector("#onboarding-agent-name", {
timeout: 30_000,
});
// Verify the company + company-level goal were persisted.
const baseUrl = page.url().split("/").slice(0, 3).join("/");
const companiesRes = await page.request.get(`${baseUrl}/api/companies`);
expect(companiesRes.ok()).toBe(true);
const companies = await companiesRes.json();
const company = companies.find(
(c: { name: string }) => c.name === COMPANY_NAME,
);
expect(company, `company ${COMPANY_NAME} should exist`).toBeTruthy();
// And no company-level goal, which is the point rather than an omission.
// Onboarding no longer asks for a mission, so writing one here would mean
// inventing a goal the customer never chose. The mission is collected later
// in the app, and the absence is what leaves room for it.
const goalsRes = await page.request.get(
`${baseUrl}/api/companies/${company.id}/goals`,
);
expect(goalsRes.ok()).toBe(true);
const goals = await goalsRes.json();
const companyGoal = (Array.isArray(goals) ? goals : []).find(
(g: { level?: string }) => g.level === "company",
);
expect(
companyGoal,
"onboarding must not invent a mission the customer never gave",
).toBeFalsy();
// The expanded wizard must not crash the app (Rules-of-Hooks regression).
expect(pageErrors, pageErrors.join("\n")).toHaveLength(0);
});
});