mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-28 12:04:41 +01:00
## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work > - New customers arrive through an onboarding arc that spans Paperclip Cloud and the tenant app > - Cloud's naming screen stopped asking for the company mission, but the tenant wizard still decided its first step by asking whether the company had one > - Every Cloud-created company therefore looked mission-less on arrival, so every walk detoured through a "Define your mission" screen the design had already removed > - This pull request removes that step from the arc, and makes step 1 create the company itself > - The benefit is a shorter arc that matches the design, and a three-step progress strip that now counts the steps that exist ## Linked Issues or Issue Description No public issue exists. The problem was found by walking staging end to end. **What happened:** A new customer signs in, names their organization, and waits for it to build. The tenant wizard then asks "Define your mission" before it asks for the first agent. Cloud no longer collects a mission, so this screen appears for every new customer. **Expected behavior:** The wizard asks for the first agent, the model, and a review. The progress strip counts three steps. **Actual behavior:** The wizard asks for the mission first. The progress strip counts five segments, because the run does not enter on the agent arc. **Additional context:** Three merged pull requests built the mission-based step choice this change removes: #11352, #11416 and #11429. The mission is now collected later, inside the tenant app, so onboarding does not ask for it at all. ## What Changed - `onboardingStepForCompany` always returns the agent step. The `companyHasMission` parameter is removed, because it cannot change the answer. - `resolveRouteOnboardingOptions` no longer accepts `companyHasMission`. - The dashboard no longer waits for the goal lookup before it opens the wizard. That wait only chose a step, and the step is now fixed. - Step 1 creates the company in a new `handleCreateCompany`. Company creation used to sit at the end of `handleConfirmMission`. - No company goal is written during onboarding. - The three-step strip now shows on the agent, model and review steps, because every Cloud-first run enters on the arc. - The full-length bar drops its second segment. No run can fill it. - The grow path keeps its step 2 questionnaire. Only the create path skips ahead. - Back from the agent step goes to the screen the run came from. - Four end-to-end specs no longer drive the wizard through the mission step. ## Verification Run the tenant test suite: ``` cd ui && npx vitest run ``` - 4356 tests pass. 471 files pass. - `npx tsc --noEmit` reports no errors. - Fault injection: forcing `skipsMissionStep` to `true` fails the grow questionnaire test. Removing the Back rule fails the Back test. Both tests fail on the exact defect they guard. - The three-step strip is asserted by an existing test. It checks `Step 1 of 3` and `aria-label="Create your first agent"`. Manual check on staging after the paired Cloud change: 1. Open a new incognito window. 2. Sign in with a new account. 3. Name the organization. 4. Confirm the wizard shows "Create your first agent" and "Step 1 of 3". ## Risks - **Behavioral change.** Onboarding no longer writes a company goal. An agent hired during onboarding starts without a seeded mission. This is intended. The mission moves to the tenant app. - **Dead code.** `ONBOARDING_MISSION_STEP` and the mission screen stay in the codebase, but nothing in the app opens them. They wait for the surface that collects the mission later. - **Grow path.** The grow path is unchanged, but it shares step 2 with the removed screen. New tests cover it. - **Superseded work.** #11352, #11416 and #11429 tuned the mission-based step choice. This change removes the branch they tuned. ## Model Used Claude Opus 5 (`claude-opus-5`), extended thinking, with tool use and code execution through Claude Code. ## 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>
166 lines
7.4 KiB
TypeScript
166 lines
7.4 KiB
TypeScript
import { test, expect } from "@playwright/test";
|
||
import fs from "node:fs";
|
||
import path from "node:path";
|
||
import { fileURLToPath } from "node:url";
|
||
|
||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||
|
||
/**
|
||
* NUX Phase 4 — visual QA screenshot capture.
|
||
*
|
||
* Boots a throwaway local_trusted instance (see playwright.config.ts webServer)
|
||
* and captures screenshots of every surface integrated by NUX Phases 1–3:
|
||
* - "Build a new company" step 1 (company name) + step 2 (mission)
|
||
* - Team-lead hire step (capsule wizard, PAP-125)
|
||
* - Onboarding front door (path picker)
|
||
* - "Add agents to your org" growth intake
|
||
* - Conference Room (BoardChat) shell + composer + activity feed
|
||
* - Artifacts page
|
||
*
|
||
* These are structural/rendering checks — LLM-dependent streaming (CEO chat
|
||
* responses, hiring-plan generation) is verified separately on an LLM-backed
|
||
* instance. Screenshots land in ./nux-phase4-shots for upload as evidence.
|
||
*/
|
||
|
||
// Write under the gitignored test-results dir so re-runs leave no untracked
|
||
// noise; screenshots are uploaded to the issue as QA evidence, not committed.
|
||
const SHOT_DIR = path.join(__dirname, "test-results", "nux-phase4-shots");
|
||
|
||
function shot(name: string) {
|
||
fs.mkdirSync(SHOT_DIR, { recursive: true });
|
||
return path.join(SHOT_DIR, name);
|
||
}
|
||
|
||
async function openWizard(page: import("@playwright/test").Page) {
|
||
await page.goto("/onboarding");
|
||
const startBtn = page.getByRole("button", { name: /Start Onboarding|New Company|Add Agent/ });
|
||
if (await startBtn.count()) {
|
||
await startBtn.first().click();
|
||
}
|
||
}
|
||
|
||
test.describe("NUX Phase 4 visual QA", () => {
|
||
test("captures every integrated surface", async ({ page }) => {
|
||
// 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);
|
||
|
||
const consoleErrors: string[] = [];
|
||
page.on("console", (msg) => {
|
||
if (msg.type() === "error") consoleErrors.push(msg.text());
|
||
});
|
||
page.on("pageerror", (err) => consoleErrors.push("PAGEERROR: " + err.message));
|
||
|
||
const baseUrl =
|
||
"http://127.0.0.1:" + (process.env.PAPERCLIP_E2E_PORT ?? "3199");
|
||
|
||
// ── Section A: create-company path (name → mission → hire) ────────────
|
||
await openWizard(page);
|
||
// Front door shows when the wizard doesn't open directly on the create
|
||
// path (e.g. another spec already created a company on this instance).
|
||
const createCard = page.getByRole("button", { name: /Build a new company/ });
|
||
if (await createCard.count()) {
|
||
await createCard.first().click();
|
||
}
|
||
await expect(
|
||
page.getByRole("heading", { name: "Name your organization" }),
|
||
).toBeVisible({ timeout: 15_000 });
|
||
await page.getByPlaceholder("Acme Corp").fill("QA Robotics");
|
||
await page.screenshot({ path: shot("02-create-name.png") });
|
||
|
||
await page.getByRole("button", { name: /^Next/ }).click();
|
||
// Step 1's "Next" creates the company and goes straight to the team lead.
|
||
// The mission screenshot that sat here is gone with the step it captured.
|
||
await page.waitForSelector("#onboarding-agent-role", {
|
||
timeout: 30_000,
|
||
});
|
||
await page.screenshot({ path: shot("04-hire-team-lead.png") });
|
||
|
||
// The company just created anchors the route-scoped sections below.
|
||
const companiesRes = await page.request.get(`${baseUrl}/api/companies`);
|
||
expect(companiesRes.ok()).toBe(true);
|
||
const companies = await companiesRes.json();
|
||
const qaCompany = (Array.isArray(companies) ? companies : []).find(
|
||
(c: { name: string }) => c.name === "QA Robotics",
|
||
);
|
||
expect(qaCompany, "wizard should have created QA Robotics").toBeTruthy();
|
||
const prefix: string = qaCompany.issuePrefix;
|
||
|
||
// ── Section B: front door + growth intake ─────────────────────────────
|
||
await page.evaluate(() => window.localStorage.clear());
|
||
await openWizard(page);
|
||
// Reach the full-screen front door (step 0): either it shows directly or
|
||
// "← Back to start" returns to it from the create step.
|
||
if (!(await page.getByRole("heading", { name: "Welcome to Paperclip" }).count())) {
|
||
await page.getByRole("button", { name: /Back to start/ }).click();
|
||
}
|
||
await expect(
|
||
page.getByRole("heading", { name: "Welcome to Paperclip" }),
|
||
).toBeVisible({ timeout: 10_000 });
|
||
await expect(
|
||
page.getByRole("heading", { name: "Build a new company" }),
|
||
).toBeVisible();
|
||
await expect(
|
||
page.getByRole("heading", { name: "Add agents to your org" }),
|
||
).toBeVisible();
|
||
await page.screenshot({ path: shot("01-front-door.png") });
|
||
|
||
await page.getByRole("button", { name: /Add agents to your org/ }).click();
|
||
// The grow path shares step 1 (company name) before its step-2 intake.
|
||
await expect(
|
||
page.getByRole("heading", { name: "Name your organization" }),
|
||
).toBeVisible({ timeout: 10_000 });
|
||
await page.getByPlaceholder("Acme Corp").fill("QA Robotics Grow");
|
||
await page.getByRole("button", { name: /^Next/ }).click();
|
||
await expect(
|
||
page.getByRole("heading", { name: /Tell us about your team/ }),
|
||
).toBeVisible({ timeout: 10_000 });
|
||
await page.screenshot({ path: shot("05-growth-intake.png") });
|
||
|
||
// ── Section C: Conference Room (BoardChat) ────────────────────────────
|
||
// Visit the company dashboard first so CompanyContext selects the company
|
||
// from the route before we land on the board-chat surface.
|
||
await page.evaluate(() => window.localStorage.clear());
|
||
await page.goto(`/${prefix}/dashboard`);
|
||
await page.waitForLoadState("networkidle");
|
||
await page.goto(`/${prefix}/board-chat`);
|
||
await expect(page).toHaveURL(new RegExp(`/${prefix}/board-chat`));
|
||
// Composer renders once a company is selected. (Regression guard for the
|
||
// Rules-of-Hooks crash that previously blanked this page — see PAP-50.)
|
||
await expect(
|
||
page.getByPlaceholder("Ask anything about your company..."),
|
||
).toBeVisible({ timeout: 20_000 });
|
||
await page.waitForTimeout(2_000); // let welcome bubble + suggestion chips stage in
|
||
await page.screenshot({ path: shot("06-board-chat.png") });
|
||
|
||
// ── Section D: Artifacts ──────────────────────────────────────────────
|
||
await page.goto(`/${prefix}/artifacts`);
|
||
await expect(page).toHaveURL(new RegExp(`/${prefix}/artifacts`));
|
||
await page.waitForLoadState("networkidle");
|
||
await page.waitForTimeout(1_000);
|
||
await page.screenshot({ path: shot("07-artifacts.png") });
|
||
|
||
for (const f of [
|
||
"01-front-door.png",
|
||
"02-create-name.png",
|
||
"04-hire-team-lead.png",
|
||
"05-growth-intake.png",
|
||
"06-board-chat.png",
|
||
"07-artifacts.png",
|
||
]) {
|
||
const p = shot(f);
|
||
expect(fs.existsSync(p), `missing ${f}`).toBe(true);
|
||
expect(fs.statSync(p).size, `empty ${f}`).toBeGreaterThan(1_000);
|
||
}
|
||
|
||
// No React Rules-of-Hooks / render crashes on any surface we visited.
|
||
const hookErrors = consoleErrors.filter(
|
||
(e) => /Rendered more hooks|change in the order of Hooks/i.test(e),
|
||
);
|
||
expect(hookErrors, hookErrors.join("\n")).toHaveLength(0);
|
||
});
|
||
});
|