mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-05 03:51:54 +01:00
## Thinking Path > - Paperclip helps people manage AI agents and their work. > - The board checks that access data belongs to the signed-in account. > - The startup changes in #18790 added this account check. > - Two existing test fixtures did not supply the account identity. > - Cloud readiness now rejects those fixtures before their intended assertions. > - This pull request adds the matching user ID to each fixture. > - The tests can then check invitations and pending shell settings with the same access boundary as the app. ## Linked Issues or Issue Description Refs #18790, #18770. Cloud readiness [run 41665172584](https://github.com/paperclipai/paperclip/actions/runs/41665172584) at `f7ad1dc7cacedf7153a45e854ddfdd544aa87abd` failed both UI shards. The private invitation case showed an access error. Both pending-shell cases made two session reads. All three failures reproduce unchanged in the focused local run. The access query compares the response user ID with the current session. These fixtures omitted that field. The mismatch correctly starts account-change handling and prevents the tests from reaching their intended state. ## What Changed - Give the invitation fixture the same user ID as its signed-in invitee. - Give the pending-shell fixture the same user ID as its signed-in user. - Keep all assertions, account-change rejection, and production behavior unchanged. ## Verification - Before the repair: the focused App and ShellConfigurationGate run reproduced all three CI failures; 24 other cases passed. - After the repair: all 76 cases pass across App, ShellConfigurationGate, CloudAccessGate, board-access-query, and CompanyContext. These include account mismatch and revoked-access coverage. - `pnpm check:token-gates` and `git diff --check` pass. - Full `pnpm build` and `pnpm -r typecheck` pass on `fa4ec98859d573e4197bb022cedc76f36fad1cf8`. Current-head Greptile reviewed this commit at 5/5 with no findings. CI is still running. Normal squash auto-merge is enabled for this exact head and will wait for the required checks. - The full local workspace test suite is not repeated for this two-line fixture repair. The earlier full-suite chat flakiness and cleanup timeout remain recorded in #18770. This PR does not claim that those failures are repaired. - No API, visual, or operator documentation changes are needed for these fixture values. ## Risks Low risk. The change only adds existing response fields to two test fixtures. It does not relax account validation or change test expectations. Cloud readiness must still pass on the resulting master revision before rollout. ## Model Used OpenAI GPT-6 Codex, with reasoning, tool use, and code execution. The exact runtime model identifier and context-window size are not exposed in this session. ## 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 (only public GitHub `#NNN` / `github.com/paperclipai/paperclip` URLs) - [x] My branch name describes the change (e.g. `docs/...`, `fix/...`) 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 - [ ] 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>
103 lines
5.6 KiB
TypeScript
103 lines
5.6 KiB
TypeScript
// @vitest-environment jsdom
|
|
import { act } from "react";
|
|
import { createRoot } from "react-dom/client";
|
|
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
|
import { MemoryRouter, Route, Routes } from "react-router-dom";
|
|
import { afterEach, expect, it, vi } from "vitest";
|
|
import { CloudAccessGate } from "./CloudAccessGate";
|
|
import { ShellConfigurationGate } from "./ShellConfigurationGate";
|
|
import { useStreamlinedUiEnabled } from "../hooks/useStreamlinedUiEnabled";
|
|
|
|
vi.mock("@/lib/router", async () => await import("react-router-dom"));
|
|
vi.mock("./AnimatedPaperclipIcon", () => ({ PaperclipLoading: () => <div>Loading shell</div> }));
|
|
const { health, session, access, settings } = vi.hoisted(() => ({ health: vi.fn(), session: vi.fn(), access: vi.fn(), settings: vi.fn() }));
|
|
vi.mock("../api/health", () => ({ healthApi: { get: health } }));
|
|
vi.mock("../api/auth", () => ({ authApi: { getSession: session } }));
|
|
vi.mock("../api/access", () => ({ accessApi: { getCurrentBoardAccess: access } }));
|
|
vi.mock("../api/instanceSettings", () => ({ instanceSettingsApi: { getExperimental: settings } }));
|
|
(globalThis as Record<string, unknown>).IS_REACT_ACT_ENVIRONMENT = true;
|
|
|
|
afterEach(() => vi.resetAllMocks());
|
|
|
|
it.each([
|
|
["/PAP/issues/PAP-1", true],
|
|
["/PAP/issues/aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa", true],
|
|
["/PAP/issues/all", false],
|
|
["/PAP/issues", false],
|
|
["/PAP/chats/agent", false],
|
|
["/PAP/apps", false],
|
|
])("prepares only guarded task data behind the loading view while preferences load: %s", async (path, permitted) => {
|
|
const container = document.createElement("div");
|
|
const root = createRoot(container);
|
|
try {
|
|
await act(async () => root.render(<MemoryRouter initialEntries={[path]}><Routes>
|
|
<Route element={<ShellConfigurationGate loaded={false} allowPendingTaskDetail />}>
|
|
<Route path="*" element={<div>Guarded task content</div>} />
|
|
</Route>
|
|
</Routes></MemoryRouter>));
|
|
expect(container.textContent).toContain("Loading shell");
|
|
expect(container.textContent?.includes("Guarded task content")).toBe(permitted);
|
|
const prepared = container.querySelector<HTMLElement>('[data-testid="shell-prepared-outlet"]');
|
|
expect(prepared?.hidden ?? null).toBe(permitted ? true : null);
|
|
if (prepared) expect(prepared.hasAttribute("inert")).toBe(true);
|
|
} finally { await act(async () => root.unmount()); }
|
|
});
|
|
|
|
it.each([false, true])("runs access checks while shell settings are pending (membership=%s)", async allowMembershipRequest => {
|
|
let resolveSettings!: (value: unknown) => void;
|
|
health.mockResolvedValue({ status: "ok", deploymentMode: "authenticated", bootstrapStatus: "ready" });
|
|
session.mockResolvedValue({ user: { id: "user" } });
|
|
access.mockResolvedValue({ userId: "user", companyIds: ["company"] });
|
|
settings.mockReturnValue(new Promise(resolve => { resolveSettings = resolve; }));
|
|
function RoutesUnderTest() {
|
|
const { loaded, enabled } = useStreamlinedUiEnabled();
|
|
return <Routes><Route element={<CloudAccessGate allowMembershipRequest={allowMembershipRequest} />}>
|
|
<Route element={<ShellConfigurationGate loaded={loaded} />}>
|
|
<Route path="apps" element={<div>{enabled ? "Streamlined shell" : "Legacy shell"}</div>} />
|
|
</Route>
|
|
</Route></Routes>;
|
|
}
|
|
const container = document.createElement("div");
|
|
const root = createRoot(container);
|
|
const client = new QueryClient({ defaultOptions: { queries: { retry: false } } });
|
|
try {
|
|
await act(async () => root.render(<QueryClientProvider client={client}><MemoryRouter initialEntries={["/apps"]}><RoutesUnderTest /></MemoryRouter></QueryClientProvider>));
|
|
await act(async () => { await new Promise(resolve => setTimeout(resolve, 20)); });
|
|
expect(health).toHaveBeenCalledTimes(1);
|
|
expect(session).toHaveBeenCalledTimes(1);
|
|
expect(access).toHaveBeenCalledTimes(1);
|
|
expect(container.textContent).not.toContain("Streamlined shell");
|
|
expect(container.textContent).not.toContain("Legacy shell");
|
|
await act(async () => { resolveSettings({ enableStreamlinedUi: false }); });
|
|
await act(async () => { await new Promise(resolve => setTimeout(resolve, 20)); });
|
|
expect(container.textContent).toBe("Legacy shell");
|
|
} finally {
|
|
await act(async () => root.unmount());
|
|
client.clear();
|
|
}
|
|
});
|
|
|
|
it("keeps prepared task content mounted without taking existing focus when the shell becomes visible", async () => {
|
|
const container = document.createElement("div");
|
|
const root = createRoot(container);
|
|
const input = document.createElement("input");
|
|
document.body.append(container, input);
|
|
const view = (loaded: boolean) => <MemoryRouter initialEntries={["/PAP/issues/PAP-1"]}><Routes>
|
|
<Route element={<ShellConfigurationGate loaded={loaded} allowPendingTaskDetail />}>
|
|
<Route path="*" element={<main id="main-content" tabIndex={-1} data-testid="prepared-task">Task data</main>} />
|
|
</Route>
|
|
</Routes></MemoryRouter>;
|
|
try {
|
|
await act(async () => root.render(view(false)));
|
|
const task = container.querySelector('[data-testid="prepared-task"]');
|
|
expect(task).not.toBeNull();
|
|
input.focus();
|
|
await act(async () => root.render(view(true)));
|
|
await act(async () => { await new Promise(resolve => requestAnimationFrame(resolve)); });
|
|
expect(document.activeElement).toBe(input);
|
|
expect(container.querySelector('[data-testid="prepared-task"]')).toBe(task);
|
|
expect(container.querySelector<HTMLElement>('[data-testid="shell-prepared-outlet"]')?.hidden).toBe(false);
|
|
expect(container.querySelector('[data-testid="shell-configuration-loading"]')).toBeNull();
|
|
} finally { await act(async () => root.unmount()); container.remove(); input.remove(); }
|
|
});
|