// Cloud-specific (billing): the free plan allows 3 organizations per user.// Driven ENTIRELY through the real web UI as a fresh user — the onboarding// create-org page for the first org, then the in-app account-menu →// org-switcher → "Create organization" modal for the rest. The run's// Playwright trace + video + step screenshots are the debugging artifacts.import { expect } from "@effect/vitest";import { Effect } from "effect"; import { scenario } from "../src/scenario";import { Billing, Browser, Target } from "../src/services"; const FREE_LIMIT = 3; scenario( "Billing · the free plan stops organization creation after 3", {}, Effect.gen(function* () { // Gate: billing limits are enforced on this target. yield* Billing; const target = yield* Target; const browser = yield* Browser; const identity = yield* target.newIdentity({ org: false }); yield* browser.session(identity, async ({ page, step }) => { await step("A fresh user lands on onboarding (no organization yet)", async () => { await page.goto("/", { waitUntil: "networkidle" }); await page.getByPlaceholder("Northwind Labs").waitFor(); }); await step(`Create "Acme 1" (1 of ${FREE_LIMIT} allowed on the free plan)`, async () => { await page.getByPlaceholder("Northwind Labs").fill("Acme 1"); await page.getByRole("button", { name: "Create organization" }).click(); // Onboarding step 2 — proves the first org was created. await page.getByText("Connect your MCP client").waitFor(); }); await step("Continue into the app", async () => { await page.getByRole("button", { name: "Continue to app" }).click(); await page.getByText("Integrations").first().waitFor(); // Let the router navigation fully settle (slow on a cold dev server) // before opening menus — a late remount closes them mid-interaction. // The console canonicalizes onto the org's URL slug (/acme-1). await page.waitForURL((url) => /^\/[a-z0-9-]+\/?$/.test(url.pathname), { timeout: 30_000, }); await page.waitForLoadState("networkidle"); }); const openCreateOrgModal = async (currentOrg: string) => { // Under parallel-suite load the radix menu re-renders while the org // list loads; a click can land on a closing menu. Bounded retry with // a clean reopen keeps the journey honest without 30s hangs. for (let attempt = 1; ; attempt++) { try { await page.keyboard.press("Escape"); await page.getByRole("button", { name: /Test User/ }).click(); await page.getByRole("menuitem", { name: currentOrg }).click({ timeout: 5_000 }); const subContent = page.locator('[data-slot="dropdown-menu-sub-content"]'); await subContent.waitFor({ state: "visible", timeout: 5_000 }); await subContent .getByText("Create organization", { exact: true }) .click({ timeout: 5_000 }); await page.getByText("Add another organization").waitFor({ timeout: 5_000 }); return; } catch (error) { if (attempt >= 3) throw error; } } }; for (let i = 2; i <= FREE_LIMIT; i++) { await step(`Open the org switcher and choose "Create organization"`, async () => { await openCreateOrgModal(`Acme ${i - 1}`); }); await step(`Create "Acme ${i}" (${i} of ${FREE_LIMIT})`, async () => { await page.getByPlaceholder("Northwind Labs").fill(`Acme ${i}`); await page.getByRole("button", { name: "Create organization" }).click(); // The modal closes and the session switches into the new org. await page.getByText("Add another organization").waitFor({ state: "hidden" }); await page.getByRole("button", { name: new RegExp(`Acme ${i}`) }).waitFor(); }); } await step("Attempt a 4th organization (over the free limit)", async () => { await openCreateOrgModal(`Acme ${FREE_LIMIT}`); await page.getByPlaceholder("Northwind Labs").fill("Acme 4"); await page.getByRole("button", { name: "Create organization" }).click(); await page.locator("p.text-destructive").first().waitFor(); }); const errorText = await page.locator("p.text-destructive").first().innerText(); expect(errorText.length, "the UI shows a visible refusal").toBeGreaterThan(0); // Cross-check through the session API, with the browser's own session // cookie (fetched explicitly — the Secure cookie isn't replayed by // page.request over plain http). const cookie = (await page.context().cookies()).map((c) => `${c.name}=${c.value}`).join("; "); const response = await fetch(new URL("/api/auth/organizations", target.baseUrl), { headers: { cookie }, }); const body = (await response.json()) as { organizations: ReadonlyArray<{ name: string }> }; expect(body.organizations.length, "exactly the free-plan allowance exists").toBe(FREE_LIMIT); }); }),);
// Cloud-specific (billing): the free plan allows 3 organizations per user.
// Driven ENTIRELY through the real web UI as a fresh user — the onboarding
// create-org page for the first org, then the in-app account-menu →
// org-switcher → "Create organization" modal for the rest. The run's
// Playwright trace + video + step screenshots are the debugging artifacts.
import { expect } from "@effect/vitest";
import { Effect } from "effect";
import { scenario } from "../src/scenario";
import { Billing, Browser, Target } from "../src/services";
const FREE_LIMIT = 3;
scenario(
"Billing · the free plan stops organization creation after 3",
{},
Effect.gen(function* () {
// Gate: billing limits are enforced on this target.
yield* Billing;
const target = yield* Target;
const browser = yield* Browser;
const identity = yield* target.newIdentity({ org: false });
yield* browser.session(identity, async ({ page, step }) => {
await step("A fresh user lands on onboarding (no organization yet)", async () => {
await page.goto("/", { waitUntil: "networkidle" });
await page.getByPlaceholder("Northwind Labs").waitFor();
});
await step(`Create "Acme 1" (1 of ${FREE_LIMIT} allowed on the free plan)`, async () => {
await page.getByPlaceholder("Northwind Labs").fill("Acme 1");
await page.getByRole("button", { name: "Create organization" }).click();
// Onboarding step 2 — proves the first org was created.
await page.getByText("Connect your MCP client").waitFor();
});
await step("Continue into the app", async () => {
await page.getByRole("button", { name: "Continue to app" }).click();
await page.getByText("Integrations").first().waitFor();
// Let the router navigation fully settle (slow on a cold dev server)
// before opening menus — a late remount closes them mid-interaction.
// The console canonicalizes onto the org's URL slug (/acme-1).
await page.waitForURL((url) => /^\/[a-z0-9-]+\/?$/.test(url.pathname), {
timeout: 30_000,
});
await page.waitForLoadState("networkidle");
});
const openCreateOrgModal = async (currentOrg: string) => {
// Under parallel-suite load the radix menu re-renders while the org
// list loads; a click can land on a closing menu. Bounded retry with
// a clean reopen keeps the journey honest without 30s hangs.
for (let attempt = 1; ; attempt++) {
try {
await page.keyboard.press("Escape");
await page.getByRole("button", { name: /Test User/ }).click();
await page.getByRole("menuitem", { name: currentOrg }).click({ timeout: 5_000 });
const subContent = page.locator('[data-slot="dropdown-menu-sub-content"]');
await subContent.waitFor({ state: "visible", timeout: 5_000 });
await subContent
.getByText("Create organization", { exact: true })
.click({ timeout: 5_000 });
await page.getByText("Add another organization").waitFor({ timeout: 5_000 });
return;
} catch (error) {
if (attempt >= 3) throw error;
}
}
};
for (let i = 2; i <= FREE_LIMIT; i++) {
await step(`Open the org switcher and choose "Create organization"`, async () => {
await openCreateOrgModal(`Acme ${i - 1}`);
});
await step(`Create "Acme ${i}" (${i} of ${FREE_LIMIT})`, async () => {
await page.getByPlaceholder("Northwind Labs").fill(`Acme ${i}`);
await page.getByRole("button", { name: "Create organization" }).click();
// The modal closes and the session switches into the new org.
await page.getByText("Add another organization").waitFor({ state: "hidden" });
await page.getByRole("button", { name: new RegExp(`Acme ${i}`) }).waitFor();
});
}
await step("Attempt a 4th organization (over the free limit)", async () => {
await openCreateOrgModal(`Acme ${FREE_LIMIT}`);
await page.getByPlaceholder("Northwind Labs").fill("Acme 4");
await page.getByRole("button", { name: "Create organization" }).click();
await page.locator("p.text-destructive").first().waitFor();
});
const errorText = await page.locator("p.text-destructive").first().innerText();
expect(errorText.length, "the UI shows a visible refusal").toBeGreaterThan(0);
// Cross-check through the session API, with the browser's own session
// cookie (fetched explicitly — the Secure cookie isn't replayed by
// page.request over plain http).
const cookie = (await page.context().cookies()).map((c) => `${c.name}=${c.value}`).join("; ");
const response = await fetch(new URL("/api/auth/organizations", target.baseUrl), {
headers: { cookie },
});
const body = (await response.json()) as { organizations: ReadonlyArray<{ name: string }> };
expect(body.organizations.length, "exactly the free-plan allowance exists").toBe(FREE_LIMIT);
});
}),
);