// @vitest-environment jsdom import type { ReactNode } from "react"; import { flushSync } from "react-dom"; import { createRoot } from "react-dom/client"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { CloudAccessGate } from "./components/CloudAccessGate"; import { queryKeys } from "./lib/queryKeys"; import appSource from "./App.tsx?raw"; const mockHealthApi = vi.hoisted(() => ({ get: vi.fn(), })); const mockAuthApi = vi.hoisted(() => ({ getSession: vi.fn(), })); const mockAccessApi = vi.hoisted(() => ({ getCurrentBoardAccess: vi.fn(), claimBootstrapAdmin: vi.fn(), })); const beginCloudSignInMock = vi.hoisted(() => vi.fn()); vi.mock("@/lib/cloud-sign-in", () => ({ beginCloudSignIn: (url: string) => beginCloudSignInMock(url), clearCloudSignInAttempt: vi.fn(), })); vi.mock("./api/health", () => ({ healthApi: mockHealthApi, })); vi.mock("./api/auth", () => ({ authApi: mockAuthApi, })); vi.mock("./api/access", () => ({ accessApi: mockAccessApi, })); vi.mock("@/lib/router", () => ({ Link: ({ to, children }: { to: string; children?: ReactNode }) => {children}, Navigate: ({ to }: { to: string }) =>
Navigate:{to}
, Outlet: () =>
Outlet content
, Route: ({ children }: { children?: ReactNode }) => <>{children}, Routes: ({ children }: { children?: ReactNode }) => <>{children}, useLocation: () => ({ pathname: "/instance/settings/general", search: "", hash: "" }), useParams: () => ({}), })); /** * Waits on the condition, not on a fixed number of turns. A hand-rolled retry * loop is ample on an idle machine and not when the suite runs many workers in * parallel: it gives up after N turns and reports a failure on behaviour that * works. `vi.waitFor` retries against a time budget, so a loaded worker gets * more turns instead. * * Same replacement as #11499 and #11521, which fixed the shorter-budget * instances of this in the routing tests. */ async function waitForText(container: HTMLElement, text: string) { await vi.waitFor(() => expect(container.textContent).toContain(text)); } function renderGate(container: HTMLElement, allowMembershipRequest = false, client?: QueryClient) { const root = createRoot(container); const queryClient = client ?? new QueryClient({ defaultOptions: { queries: { retry: false } }, }); flushSync(() => { root.render( , ); }); return root; } function unmountRoot(root: ReturnType) { flushSync(() => { root.unmount(); }); } describe("CloudAccessGate", () => { let container: HTMLDivElement; beforeEach(() => { container = document.createElement("div"); document.body.appendChild(container); mockHealthApi.get.mockResolvedValue({ status: "ok", deploymentMode: "authenticated", deploymentExposure: "private", bootstrapStatus: "ready", }); }); afterEach(() => { container.remove(); document.body.innerHTML = ""; vi.clearAllMocks(); }); it("renews a missing Cloud instance session without opening local auth", async () => { mockHealthApi.get.mockResolvedValue({ deploymentMode: "authenticated", cloud: { managed: true, managedBy: "paperclip-cloud", cloudBaseUrl: "https://my-staging.paperclip.app", stackSlug: "team" } }); mockAuthApi.getSession.mockResolvedValue(null); beginCloudSignInMock.mockReturnValue(true); const root = renderGate(container); await vi.waitFor(() => expect(beginCloudSignInMock).toHaveBeenCalledTimes(1)); expect(beginCloudSignInMock).toHaveBeenCalledWith("https://my-staging.paperclip.app/v1/stacks/team/entry-redirect?returnTo=%2Finstance%2Fsettings%2Fgeneral"); expect(container.textContent).not.toContain("Navigate:/auth"); expect(container.textContent).not.toContain("Outlet content"); unmountRoot(root); }); it("does not mistake a session service failure for a signed-out user", async () => { mockAuthApi.getSession.mockRejectedValue(new Error("Session service unavailable")); const root = renderGate(container); await waitForText(container, "Unable to load Paperclip"); expect(container.querySelector("button")?.textContent).toBe("Try again"); expect(container.textContent).not.toContain("Outlet content"); expect(container.textContent).not.toContain("Navigate:/auth"); expect(beginCloudSignInMock).not.toHaveBeenCalled(); unmountRoot(root); }); it.each([undefined, { managed: true, managedBy: "paperclip-cloud", cloudBaseUrl: "https://my.paperclip.app", stackSlug: "team" }])( "does not require a session in local trusted mode, including with Cloud metadata %j", async (cloud) => { mockHealthApi.get.mockResolvedValue({ deploymentMode: "local_trusted", cloud }); const client = new QueryClient({ defaultOptions: { queries: { retry: false } } }); await client.fetchQuery({ queryKey: queryKeys.auth.session, queryFn: () => Promise.reject(new Error("Session service unavailable")), }).catch(() => {}); const root = renderGate(container, false, client); await waitForText(container, "Outlet content"); expect(beginCloudSignInMock).not.toHaveBeenCalled(); unmountRoot(root); }); it("shows a no-access message for signed-in users without org access", async () => { mockAuthApi.getSession.mockResolvedValue({ session: { id: "session-1", userId: "user-1" }, user: { id: "user-1", email: "user@example.com", name: "User", image: null }, }); mockAccessApi.getCurrentBoardAccess.mockResolvedValue({ user: { id: "user-1", email: "user@example.com", name: "User", image: null }, userId: "user-1", isInstanceAdmin: false, companyIds: [], source: "session", keyId: null, }); const root = renderGate(container); await waitForText(container, "No organization access"); expect(container.textContent).toContain("No organization access"); expect(container.textContent).not.toContain("Outlet content"); unmountRoot(root); }); it("admits signed-in nonmembers only for the private invitation landing page", async () => { mockAuthApi.getSession.mockResolvedValue({ user: { id: "invitee" } }); mockAccessApi.getCurrentBoardAccess.mockResolvedValue({ isInstanceAdmin: false, companyIds: [] }); const root = renderGate(container, true); await waitForText(container, "Outlet content"); unmountRoot(root); }); it("still requires sign-in for the invitation landing page", async () => { mockAuthApi.getSession.mockResolvedValue(null); const root = renderGate(container, true); await waitForText(container, "Navigate:/auth?next="); expect(container.textContent).not.toContain("Outlet content"); unmountRoot(root); }); it("still blocks invitation pages while cloud bootstrap is pending", async () => { mockHealthApi.get.mockResolvedValue({ deploymentMode: "authenticated", deploymentExposure: "public", bootstrapStatus: "bootstrap_pending" }); mockAuthApi.getSession.mockResolvedValue({ user: { id: "invitee" } }); const root = renderGate(container, true); await waitForText(container, "This Paperclip is waiting on its first admin"); expect(container.textContent).not.toContain("Outlet content"); unmountRoot(root); }); it("keeps invitation pages closed when cloud access checks fail", async () => { mockAuthApi.getSession.mockResolvedValue({ user: { id: "invitee" } }); mockAccessApi.getCurrentBoardAccess.mockRejectedValueOnce(new Error("Access check unavailable")); const root = renderGate(container, true); await waitForText(container, "Unable to load Paperclip"); expect(container.querySelector("button")?.textContent).toBe("Try again"); expect(container.textContent).not.toContain("Outlet content"); unmountRoot(root); }); it("allows authenticated users with company access through to the board", async () => { mockAuthApi.getSession.mockResolvedValue({ session: { id: "session-1", userId: "user-1" }, user: { id: "user-1", email: "user@example.com", name: "User", image: null }, }); mockAccessApi.getCurrentBoardAccess.mockResolvedValue({ user: { id: "user-1", email: "user@example.com", name: "User", image: null }, userId: "user-1", isInstanceAdmin: false, companyIds: ["company-1"], source: "session", keyId: null, }); const root = renderGate(container); await waitForText(container, "Outlet content"); expect(container.textContent).toContain("Outlet content"); expect(container.textContent).not.toContain("No organization access"); unmountRoot(root); }); it("shows browser sign-in setup for signed-out private bootstrap-pending instances", async () => { mockHealthApi.get.mockResolvedValue({ status: "ok", deploymentMode: "authenticated", deploymentExposure: "private", bootstrapStatus: "bootstrap_pending", bootstrapInviteActive: false, }); mockAuthApi.getSession.mockResolvedValue(null); const root = renderGate(container); await waitForText(container, "Finish setting up this Paperclip"); expect(container.textContent).toContain("Finish setting up this Paperclip"); expect(container.textContent).toContain("Sign in / Create account"); expect(container.textContent).toContain("npx paperclipai auth bootstrap-ceo"); expect(mockAccessApi.getCurrentBoardAccess).not.toHaveBeenCalled(); unmountRoot(root); }); it("shows the claim action for signed-in private bootstrap-pending instances", async () => { mockHealthApi.get.mockResolvedValue({ status: "ok", deploymentMode: "authenticated", deploymentExposure: "private", bootstrapStatus: "bootstrap_pending", bootstrapInviteActive: false, }); mockAuthApi.getSession.mockResolvedValue({ session: { id: "session-1", userId: "user-1" }, user: { id: "user-1", email: "user@example.com", name: "User", image: null }, }); mockAccessApi.claimBootstrapAdmin.mockResolvedValue({ claimed: true, userId: "user-1" }); const root = renderGate(container); await waitForText(container, "Claim this instance"); expect(container.textContent).toContain("Claim this instance"); expect(container.textContent).toContain("Signed in as user@example.com"); expect(mockAccessApi.getCurrentBoardAccess).not.toHaveBeenCalled(); const button = Array.from(container.querySelectorAll("button")).find((candidate) => candidate.textContent?.includes("Claim this instance"), ); expect(button).toBeTruthy(); flushSync(() => { button?.dispatchEvent(new MouseEvent("click", { bubbles: true })); }); await waitForText(container, "You're the instance admin"); expect(mockAccessApi.claimBootstrapAdmin).toHaveBeenCalledTimes(1); expect(container.textContent).toContain("You're the instance admin"); expect(container.textContent).toContain("Continue to dashboard"); unmountRoot(root); }); it("keeps public bootstrap-pending instances invite-only", async () => { mockHealthApi.get.mockResolvedValue({ status: "ok", deploymentMode: "authenticated", deploymentExposure: "public", bootstrapStatus: "bootstrap_pending", bootstrapInviteActive: true, }); mockAuthApi.getSession.mockResolvedValue({ session: { id: "session-1", userId: "user-1" }, user: { id: "user-1", email: "user@example.com", name: "User", image: null }, }); const root = renderGate(container); await waitForText(container, "This Paperclip is waiting on its first admin"); expect(container.textContent).toContain("This Paperclip is waiting on its first admin"); expect(container.textContent).toContain("invite-only mode"); expect(container.textContent).not.toContain("Claim this instance"); expect(container.textContent).not.toContain("Sign in / Create account"); expect(mockAccessApi.claimBootstrapAdmin).not.toHaveBeenCalled(); unmountRoot(root); }); }); describe("Skill Studio routes", () => { it("registers create mode before the skillId route in prefixed and unprefixed routing", () => { const createRoute = 'path="skills/studio/new"'; const detailRoute = 'path="skills/studio/:skillId"'; const createIndexes = [...appSource.matchAll(new RegExp(createRoute, "g"))].map((match) => match.index ?? -1); const detailIndexes = [...appSource.matchAll(new RegExp(detailRoute, "g"))].map((match) => match.index ?? -1); expect(createIndexes).toHaveLength(2); expect(detailIndexes).toHaveLength(2); expect(createIndexes[0]).toBeLessThan(detailIndexes[0]!); expect(createIndexes[1]).toBeLessThan(detailIndexes[1]!); }); }); describe("Apps routes", () => { it("uses one connector landing page and redirects retired browse, connections, and audit URLs", () => { expect(appSource).toContain('} />'); expect(appSource).toContain('} />'); expect(appSource).toContain('} />'); expect(appSource).toContain('} />'); expect(appSource).toContain('} />'); expect(appSource).not.toContain('import { Connections }'); expect(appSource).toContain('} />'); expect(appSource).toContain('path="apps/vercel-connect"'); expect(appSource).toContain(''); expect(appSource).toContain(''); expect(appSource).toContain('} />'); expect(appSource).toContain('} />'); expect(appSource).toContain('} />'); }); it("redirects legacy Rules and Health links to the remaining developer surfaces", () => { expect(appSource).toContain('if (tab === "runtime" || tab === "audit") return "/apps";'); expect(appSource).toContain('if (tab === "policies") return "/apps/advanced/profiles";'); }); }); describe("Retired settings routes", () => { it("redirects the removed heartbeats page to the settings root instead of dropping the route", () => { expect(appSource).toContain( '} />', ); expect(appSource).not.toContain(" { it("does not register decision-training views", () => { expect(appSource).not.toContain('path="decisions/training"'); expect(appSource).not.toContain('path="decisions/training/:id"'); expect(appSource).not.toContain('path="training"'); expect(appSource).not.toContain('path="training/:id"'); }); });