Files
PaperClipAI/tests/canary-onboarding/canary-onboarding.spec.ts
DottaandPaperclip 8c89340444 fix(onboarding): preserve draft through company refetch (#12735)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Onboarding creates an organization in the browser.
> - The browser keeps onboarding drafts for the same origin.
> - A new data directory does not clear that browser data.
> - The organization create request refreshes the company list.
> - The old gate unmounted the live wizard during that refresh.
> - This pull request keeps the wizard mounted after its first draft
check.
> - The customer can continue to the agent step after the organization
is created.

## Linked Issues or Issue Description

No matching public issue was found. Related earlier fix: Refs #12667.

**What happened?**

A local canary install could create an organization through the API and
then return the browser to an empty organization-name screen.

**Expected behavior**

The wizard must continue to the agent step after it creates the
organization.

**Steps to reproduce**

1. Keep a Paperclip onboarding draft in the browser.
2. Run npx paperclipai@canary onboard with a new data directory.
3. Open /onboarding.
4. Enter an organization name and select Continue.

**Paperclip version or commit**

2026.902.0-canary.7. The fix is based on current master.

**Deployment mode**

Local trusted mode through the Paperclip CLI.

**Install method**

npx package install.

**Agent adapter(s) involved**

Not adapter-specific.

**Database mode**

Embedded PostgreSQL.

## What Changed

- Keep the onboarding wizard mounted after its first successful draft
ownership check.
- Keep a failed ownership check retryable, so a later verified fetch
restores the saved draft.
- Add component, source E2E, and published-canary coverage for the
retained-draft refetch case.

## Verification

- Confirmed that the new canary scenario fails against
2026.902.0-canary.7 before this fix.
- pnpm exec vitest run ui/src/components/OnboardingWizard.test.tsx
- PAPERCLIP_E2E_PORT=3245 pnpm exec playwright test --config
tests/e2e/playwright.config.ts tests/e2e/onboarding.spec.ts
--reporter=line
- pnpm --filter @paperclipai/ui typecheck
- pnpm check:token-gates

## Risks

Low risk. The initial ownership check still waits for a fresh company
list. A later successful retry can restore a retained draft. Later
background refetches preserve live wizard state.

## Model Used

OpenAI Codex, GPT-5. Reasoning, tool use, code editing, terminal
execution, and browser testing were used. The execution environment does
not expose a context-window size.

## 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 described the issue in-PR following the bug issue template
- [x] I have not referenced internal or instance-local Paperclip issues
or links
- [x] My branch name describes the change 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-09-02 16:50:15 -05:00

102 lines
3.5 KiB
TypeScript

import { expect, test, type APIRequestContext } from "@playwright/test";
const expectedVersion = process.env.PAPERCLIPAI_VERSION!;
const companyName = `Canary Smoke ${Date.now()}`;
const agentName = "Canary Smoke Lead";
async function getJson<T>(request: APIRequestContext, url: string): Promise<T> {
const response = await request.get(url);
expect(response.ok()).toBe(true);
return (await response.json()) as T;
}
test("the exact published canary installs and reaches Connect a model", async ({
page,
}) => {
const pageErrors: string[] = [];
page.on("pageerror", (error) => pageErrors.push(error.message));
// Browser storage is scoped to the Paperclip origin, not to a data directory.
// A customer can therefore start a freshly installed server with an existing
// onboarding draft. Creating the organization invalidates the company list;
// this release check must prove that a refetch does not remount the wizard
// from that old draft and leave the customer on the name screen.
await page.addInitScript(() => {
localStorage.setItem("paperclip-onboarding-state", JSON.stringify({
step: 1,
companyName: "",
createdCompanyId: null,
}));
});
let delayCompanyListRefetch = false;
await page.route("**/api/companies", async (route) => {
if (delayCompanyListRefetch && route.request().method() === "GET") {
// Make the invalidation window observable. The previous implementation
// unmounted the live wizard for this entire request.
await new Promise((resolve) => setTimeout(resolve, 250));
}
await route.continue();
});
const health = await getJson<{
status: string;
version: string;
serverVersion: string;
}>(page.request, "/api/health");
expect(health.status).toBe("ok");
expect(health.version).toBe(expectedVersion);
expect(health.serverVersion).toBe(expectedVersion);
await page.goto("/onboarding");
await expect(
page.getByRole("heading", { name: "What is the name of your organization?" }),
).toBeVisible();
await page.getByRole("textbox").fill(companyName);
delayCompanyListRefetch = true;
await page.getByRole("button", { name: "Continue", exact: true }).click();
const agentNameField = page.locator("#onboarding-agent-name");
await expect(agentNameField).toBeVisible({ timeout: 30_000 });
const companies = await getJson<Array<{ id: string; name: string }>>(
page.request,
"/api/companies",
);
const company = companies.find((candidate) => candidate.name === companyName);
expect(company, `organization ${companyName} should exist through the API`).toBeTruthy();
expect(
await getJson<Array<{ id: string }>>(
page.request,
`/api/companies/${company!.id}/agents`,
),
).toEqual([]);
expect(
await getJson<Array<{ id: string }>>(
page.request,
`/api/companies/${company!.id}/issues`,
),
).toEqual([]);
await agentNameField.fill(agentName);
await page.getByRole("button", { name: "Next", exact: true }).click();
await expect(page.getByRole("heading", { name: "Connect a model" })).toBeVisible();
// Stop before Connect: a public-repository gate must not require a Claude or
// Codex credential, and reaching this screen has not hired an agent or made
// a first task.
expect(
await getJson<Array<{ id: string }>>(
page.request,
`/api/companies/${company!.id}/agents`,
),
).toEqual([]);
expect(
await getJson<Array<{ id: string }>>(
page.request,
`/api/companies/${company!.id}/issues`,
),
).toEqual([]);
expect(pageErrors, pageErrors.join("\n")).toEqual([]);
});