evOo-local/apps/web/e2e/onboarding-discovery.spec.ts
Claude 2c91a80f69
feat(onboarding): ask before discovering projects and let the user choose
The onboarding scan started on its own, discarded what it found, claimed
to read email and calendars, simulated progress for everyone who could
not scan, and its project creation always failed (wrong Space fields and
a space lookup by title across tenants).

Discovery now asks first and creates nothing without a choice:

- The scan step appears only for the deployment owner when
  ONBOARDING_SCAN_HOME is set; everyone else skips it. The simulated
  scan is gone.
- It shows the folder and asks. Only directory listings are read: git
  projects up to four levels deep, their languages and a few tools
  (Docker, GitHub Actions, Terraform). Symlinks are never followed and
  each chosen folder is resolved again before use, so nothing outside
  the folder is reached. Results stay with the account that scanned.
- Found projects are a checklist (none preselected); found languages and
  tools are facts (preselected). "Add" creates one project space per
  chosen project with createSpaceForMember and wireProjectWorkspace in
  the user's own organization, reusing the user's own space already
  linked to that folder, and saves the chosen facts as onboarding facts
  under the profile lock and fact limit.
- When a connection becomes connected (direct connect, authorization
  round trip, or catalog reconciliation), "Uses <App>" is remembered as
  a tools fact from the connector. Revoking keeps it.

The E2E harness now points ONBOARDING_SCAN_HOME at a fixture folder and
starts without a deployment owner, so owner-only onboarding steps appear
only in specs that take the seat through /__e2e/deployment-owner.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
2026-10-03 13:58:39 +00:00

77 lines
3.6 KiB
TypeScript

/**
* E2E: the deployment owner's onboarding asks before scanning the fixture home folder the
* harness configures, then adds only the projects and facts the user picks.
*/
import { expect, type Page, test } from "@playwright/test";
import type { SpaceNavigation, UserProfileState } from "@rakazo/contracts";
import { captureScreenshot, rpc, signup } from "./helpers";
const api = process.env.API_URL ?? "http://127.0.0.1:3110";
/** Advances whichever wizard step is showing until the scan step is reached. */
async function walkToScanStep(page: Page) {
const surface = page.locator('[data-rakazo-surface="onboarding"]');
await expect(async () => {
if ((await surface.getAttribute("data-onboarding-step")) === "scan") return;
const skip = page.getByRole("button", { name: /^Skip/ }).first();
const next = (await skip.isVisible())
? skip
: page.getByRole("button", { name: "Continue", exact: true });
await next.click({ timeout: 2_000 });
await expect(surface).toHaveAttribute("data-onboarding-step", "scan", { timeout: 1_000 });
}).toPass({ timeout: 60_000 });
}
test("asks before looking for projects and adds only what the user picks", async ({
page,
}, testInfo) => {
const email = `discovery-${Date.now()}@rakazo.test`;
await signup(page, email, "password12", "Discovery");
await page.waitForURL(/\/onboarding/, { timeout: 20_000 });
// The harness starts without a deployment owner; this spec takes the seat for its run.
const owner = await page.request.post(`${api}/__e2e/deployment-owner`, { data: { email } });
expect(owner.ok()).toBe(true);
const { previousOwnerUserId } = (await owner.json()) as { previousOwnerUserId: string | null };
try {
await page.goto("/onboarding", { waitUntil: "domcontentloaded" });
await walkToScanStep(page);
await expect(page.getByRole("heading", { name: "Look for projects here?" })).toBeVisible();
await expect(page.getByText(/rakazo-e2e-scan-home-/)).toBeVisible();
await captureScreenshot(page, testInfo, "onboarding-discovery-consent");
await page.getByRole("button", { name: "Look", exact: true }).click();
const projects = page.getByRole("group", { name: "Add as projects" });
await expect(projects.getByRole("checkbox")).toHaveCount(2);
const atlas = projects.getByRole("checkbox", { name: /atlas/ });
await expect(atlas).not.toBeChecked();
await expect(projects.getByRole("checkbox", { name: /garden/ })).not.toBeChecked();
await atlas.check();
const facts = page.getByRole("group", { name: "Add to About you" });
await expect(facts.getByRole("checkbox", { name: "TypeScript" })).toBeChecked();
await facts.getByRole("checkbox", { name: "Python" }).uncheck();
await captureScreenshot(page, testInfo, "onboarding-discovery-results");
await page.getByRole("button", { name: "Add", exact: true }).click();
await page.waitForURL(/\/app\/g\/[^/]+$/, { timeout: 30_000 });
const navigation = await rpc<SpaceNavigation>(page, "spaces/list", {});
const names = navigation.spaces.map((space) => space.name);
expect(names).toContain("atlas");
expect(names).not.toContain("garden");
expect(navigation.spaces.find((space) => space.name === "atlas")?.workspace?.path).toMatch(
/code\/atlas$/,
);
const values = (await rpc<UserProfileState>(page, "profile/get", {})).facts.map(
(fact) => fact.value,
);
expect(values).toEqual(
expect.arrayContaining(["Codes in TypeScript", "Uses Docker", "Uses GitHub Actions"]),
);
expect(values).not.toContain("Codes in Python");
} finally {
await page.request.post(`${api}/__e2e/deployment-owner`, {
data: { userId: previousOwnerUserId },
});
}
});