mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-06 10:48:12 +02: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>
156 lines
5.9 KiB
TypeScript
156 lines
5.9 KiB
TypeScript
import { test, expect, type Page } from "@playwright/test";
|
|
import {
|
|
expectLandsOnFirstTaskWithoutDashboardBounce,
|
|
instrumentNavLog,
|
|
} from "./helpers/onboarding-landing";
|
|
|
|
/**
|
|
* E2E: post-wizard onboarding launch.
|
|
*
|
|
* Completing the onboarding wizard now creates the first assigned task and
|
|
* drops the user straight onto that task's detail page (not the dashboard),
|
|
* so they land in the conversation the agent will start in. The chat intro
|
|
* still has unit coverage in BoardChat tests.
|
|
*
|
|
* PAP-404: onboarding used to intermittently bounce to the company dashboard.
|
|
* The bounce only reproduces when the instance already has ≥1 company (the
|
|
* board's test ports), so the second test seeds a company first to exercise
|
|
* exactly that failing condition.
|
|
*/
|
|
|
|
const FIRST_TASK_TITLE = "Paperclip onboarding";
|
|
|
|
/**
|
|
* Intercept the two side-effecting calls the wizard makes so no real CLI check
|
|
* runs and no real agent process spawns (the hire still happens server-side
|
|
* with an inert http adapter).
|
|
*/
|
|
async function installLaunchIntercepts(page: Page, baseURL?: string) {
|
|
await page.route("**/test-environment", (route) =>
|
|
route.fulfill({
|
|
contentType: "application/json",
|
|
body: JSON.stringify({ status: "pass", checks: [] }),
|
|
}),
|
|
);
|
|
|
|
await page.route("**/agent-hires", async (route) => {
|
|
const req = route.request();
|
|
const body = JSON.parse(req.postData() || "{}");
|
|
const auth = req.headers().authorization;
|
|
const real = await fetch(new URL(req.url(), baseURL).toString(), {
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
...(auth ? { Authorization: auth } : {}),
|
|
},
|
|
body: JSON.stringify({
|
|
name: body.name,
|
|
role: body.role,
|
|
adapterType: "http",
|
|
adapterConfig: { url: "http://127.0.0.1:1/dead" },
|
|
runtimeConfig: { heartbeat: { enabled: false } },
|
|
}),
|
|
});
|
|
await route.fulfill({
|
|
status: real.status,
|
|
contentType: "application/json",
|
|
body: await real.text(),
|
|
});
|
|
});
|
|
}
|
|
|
|
/** Drive the wizard from the /onboarding route through to "Get started". */
|
|
async function runOnboardingWizard(page: Page, companyName: string) {
|
|
await page.goto("/onboarding");
|
|
|
|
// Launcher card path (existing companies) — enter the wizard if the
|
|
// route shows a launcher instead of opening the wizard directly.
|
|
const startBtn = page.getByRole("button", { name: /Start Onboarding/i });
|
|
if (await startBtn.count()) await startBtn.first().click();
|
|
|
|
// Step 0: front door (skipped when the wizard opens on the create path).
|
|
const frontDoor = page.getByText("Build a new company");
|
|
if (await frontDoor.count()) await frontDoor.first().click();
|
|
|
|
// Step 1: company name.
|
|
await page.getByPlaceholder("Acme Corp").fill(companyName);
|
|
await page.getByRole("button", { name: /^Next/ }).click();
|
|
|
|
// Step 1's "Next" creates the company; the mission step no longer runs.
|
|
|
|
// Step 3: the lead's role, then its name. The role gates "Next", and
|
|
// choosing one fills the name — so the walk only types here to override it.
|
|
await page.waitForSelector("#onboarding-agent-role", { timeout: 15_000 });
|
|
await page.locator("#onboarding-agent-role").click();
|
|
await page.getByRole("option", { name: "CEO", exact: true }).click();
|
|
await page.getByRole("button", { name: /^Next/ }).click();
|
|
|
|
// Step 4: adapter (claude_local default); heartbeat is intercepted.
|
|
await page.getByRole("button", { name: /^Connect$/ }).click();
|
|
|
|
// Step 5: review → Get started creates the first task and opens its
|
|
// detail page.
|
|
const getStarted = page.getByRole("button", { name: /Get started/ });
|
|
await getStarted.waitFor({ timeout: 20_000 });
|
|
await getStarted.click();
|
|
}
|
|
|
|
async function assertFirstTaskExists(page: Page, companyName: string) {
|
|
const companiesRes = await page.request.get("/api/companies");
|
|
expect(companiesRes.ok()).toBe(true);
|
|
const companies = await companiesRes.json();
|
|
const company = companies.find(
|
|
(candidate: { name: string }) => candidate.name === companyName,
|
|
);
|
|
expect(company).toBeTruthy();
|
|
|
|
const issuesRes = await page.request.get(`/api/companies/${company.id}/issues`);
|
|
expect(issuesRes.ok()).toBe(true);
|
|
const issues = await issuesRes.json();
|
|
const firstTask = issues.find(
|
|
(candidate: { title: string }) => candidate.title === FIRST_TASK_TITLE,
|
|
);
|
|
expect(firstTask).toBeTruthy();
|
|
await expect(page.getByText(FIRST_TASK_TITLE).first()).toBeVisible({
|
|
timeout: 15_000,
|
|
});
|
|
}
|
|
|
|
test.describe("First-task launch after onboarding wizard", () => {
|
|
test("creates the first task and opens its detail page", async ({ page, baseURL }) => {
|
|
await instrumentNavLog(page);
|
|
await installLaunchIntercepts(page, baseURL);
|
|
|
|
const companyName = `E2E-TypingIntro-${Date.now()}`;
|
|
await runOnboardingWizard(page, companyName);
|
|
|
|
await expectLandsOnFirstTaskWithoutDashboardBounce(page);
|
|
await assertFirstTaskExists(page, companyName);
|
|
});
|
|
|
|
// PAP-404 regression: the dashboard bounce only fires when the instance
|
|
// already has a company for the route-sync effect to reset selection to.
|
|
// Seed one first, then onboard a brand-new company and assert we still land
|
|
// on the first task without a dashboard bounce.
|
|
test("lands on the first task even when a company already exists", async ({
|
|
page,
|
|
baseURL,
|
|
}) => {
|
|
await instrumentNavLog(page);
|
|
await installLaunchIntercepts(page, baseURL);
|
|
|
|
// Seed a pre-existing company so the companies list is non-empty when the
|
|
// wizard launches — the exact condition that reproduced the bounce.
|
|
const seedRes = await page.request.post("/api/companies", {
|
|
data: { name: `E2E-Seed-${Date.now()}` },
|
|
});
|
|
expect(seedRes.ok()).toBe(true);
|
|
|
|
const companyName = `E2E-TypingIntro-Existing-${Date.now()}`;
|
|
await runOnboardingWizard(page, companyName);
|
|
|
|
await expectLandsOnFirstTaskWithoutDashboardBounce(page);
|
|
await assertFirstTaskExists(page, companyName);
|
|
});
|
|
});
|