mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-09 16:02:36 +01:00
## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work. > - A new user's first session is the onboarding first task. The agent asks its onboarding questions in that task thread. > - The app also shows an optional in-app announcement. The live feed now serves "Introducing Connectors" (`2026-09-14-connectors`). > - The announcement well only waits while the onboarding wizard dialog is open. It shows 3 seconds after the user lands on the first task. > - In the Nov 5 cloud onboarding test (Claude API path), the tester said: "Don't show the connectors upsell until the second session. It's blocking the user flyout and potentially the onboarding questions." > - This pull request holds announcements until the company's onboarding first task is done or cancelled. > - The benefit is that a new user sees the onboarding questions and the account menu without an upsell on top of them. ## Linked Issues or Issue Description Refs #19511 (places the card in the desktop sidebar, above the account menu. That PR keeps the card off the account menu after onboarding. This PR adds the first-session gate that #19511 leaves open.) **What happened?** On a new company's onboarding first task, the "Introducing Connectors" card shows 3 seconds after the page loads. At 1440x900 the card (352x346 px, bottom-left, z-40) covers the sidebar account menu trigger and the left edge of the task composer. **Expected behavior** Optional announcements do not show in the first session, while the onboarding first task is open. **Steps to reproduce** 1. Create a new company through onboarding. The wizard creates the onboarding first task. 2. Open the first task at 1440x900 and wait 3 seconds. 3. See the announcement card on top of the account menu and the composer. **Paperclip version or commit** `master` at819420a60a. ## What Changed - `AnnouncementWell` reads the company's onboarding first task (`originKind=onboarding_first_task`, `limit=1`) with React Query under `["issues", companyId, "onboarding-first-task"]`. Issue live-update invalidation of `["issues", companyId]` refreshes it. - The well stays disabled while that task is not `done` or `cancelled`. While the read is loading or after it fails, the optional card stays hidden. A company that never had an onboarding first task is not gated. - "First session" uses the existing `origin_kind` marker. There is no schema change. - New e2e spec `tests/e2e/announcement-first-session.spec.ts` with a mocked feed that copies the live connectors entry. It checks that the card is absent while the onboarding first task is open. Then it marks the task done on the same page and checks that the card appears without a reload (a positive control for the feed mock and the live refresh path). - New unit test `AnnouncementWell.onboarding.test.tsx`. It covers open, finished, no first task, read failure, and the open-to-done transition after `["issues", companyId]` is invalidated. `AnnouncementWell.test.tsx` now gives the well a `QueryClient` with a finished onboarding state. ## Verification Red on `master` (819420a60a), before the fix. Vitest: ``` cd ui && npx vitest run src/components/AnnouncementWell.onboarding.test.tsx × stays hidden while the onboarding first task is open × stays hidden when onboarding state cannot be read Tests 2 failed | 1 passed (3) AssertionError: expected true to be false ``` Red on `master`, Playwright at 1440x900 (an earlier draft of the spec also had a placement check, recorded here as evidence for the flyout part of the report): ``` ✘ announcement-first-session.spec.ts › a brand-new company with its onboarding task open does not show the upsell Error: expect(locator).toHaveCount(expected) failed Locator: getByRole('complementary', { name: 'Paperclip announcements' }) Expected: 0 Received: 1 ✘ … where the upsell shows, it does not cover the account menu or its flyout Error: well {"x":12,"y":542,"width":352,"height":346} covers account trigger {"x":20,"y":856,"width":166,"height":36} ``` Green after the fix, merged with current `master` (f7070d68b5), with the in-page positive control: ``` ✓ announcement-first-session.spec.ts › a brand-new company with its onboarding task open does not show the upsell (10.1s) 1 passed cd ui && npx vitest run src/components/AnnouncementWell.onboarding.test.tsx src/components/AnnouncementWell.test.tsx Tests 6 passed (6) cd ui && npx vitest run src/components/AnnouncementWell.onboarding.test.tsx src/components/AnnouncementWell.test.tsx \ src/components/AnnouncementCard.test.tsx src/hooks/useAnnouncement.test.tsx src/components/SidebarAccountMenu.test.tsx src/components/Layout.test.tsx Test Files 6 passed (6) Tests 95 passed (95) ``` - `pnpm check:token-gates`: All gates clean. - UI `tsc --noEmit`: no errors in app code. The only errors are the git-ignored, generated Storybook catalog files. - CI note: the first CI run had three jobs fail with "The self-hosted runner lost communication with the server" (server tests 12/12, Runner vitest package, e2e shard 5/8). These are infra flakes. I reran them. - Local e2e note: two runs could not start the server because embedded PostgreSQL ports were in use by parallel e2e runs on the same machine. A rerun passed. This is not related to the change. Before (master): the card covers the account menu and the composer on the onboarding first task.  After: no card on the onboarding first task. The account menu and the composer are clear.  ## Risks - Low risk. The change only hides optional content. - A user who never finishes or cancels the onboarding first task does not see announcements in that company. This is the intended trade-off for "not in the first session". Closing the task shows them again. - The well makes one extra small issues read (`limit=1`) per company. It is cached for 60 seconds. - `AnnouncementWell.tsx` also changes in #19511. Whichever PR lands second resolves a small conflict in the `enabled` condition and the `<aside>` placement. ## Model Used - Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, used through Claude Code with tool use (shell, file edits, Playwright, Vitest). ## 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 - [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups - [x] I will address all Greptile and reviewer comments before requesting merge 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
106 lines
5.3 KiB
TypeScript
106 lines
5.3 KiB
TypeScript
import { test, expect, request as pwRequest, type APIRequestContext, type Page } from "@playwright/test";
|
|
|
|
/**
|
|
* E2E (N-13): the in-app announcement well ("Introducing Connectors" upsell)
|
|
*
|
|
* stays hidden while a new company's onboarding first task is open, so it
|
|
* cannot cover the onboarding questions or the account menu in the user's
|
|
* first session. Placement (never covering the account menu once shown) is
|
|
* covered by announcement-placement.spec.ts.
|
|
*
|
|
* The public feed is mocked so the spec does not depend on what the live feed
|
|
* is publishing; the card mirrors the real 2026-09-14-connectors entry.
|
|
*/
|
|
|
|
const PORT = Number(process.env.PAPERCLIP_E2E_PORT ?? 3199);
|
|
const BASE_URL = `http://127.0.0.1:${PORT}`;
|
|
const ANNOUNCEMENT_ID = "e2e-n13-connectors";
|
|
const SETTLE_WAIT_MS = 4_500; // ANNOUNCEMENT_SETTLE_MS (3 s) plus slack.
|
|
const ONE_PIXEL_PNG = Buffer.from(
|
|
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=",
|
|
"base64",
|
|
);
|
|
|
|
async function createCompany(board: APIRequestContext, name: string) {
|
|
const res = await board.post(`${BASE_URL}/api/companies`, { data: { name: `${name}-${Date.now()}` } });
|
|
if (!res.ok()) throw new Error(`POST /api/companies → ${res.status()}: ${await res.text()}`);
|
|
const company = await res.json();
|
|
// A company without agents lands on "Create your first agent"; add one so the normal shell renders.
|
|
const agentRes = await board.post(`${BASE_URL}/api/companies/${company.id}/agents`, {
|
|
data: { name: "Chief of staff", role: "ceo", adapterType: "process", adapterConfig: { command: "true" } },
|
|
});
|
|
if (!agentRes.ok()) throw new Error(`POST agents → ${agentRes.status()}: ${await agentRes.text()}`);
|
|
return { id: company.id as string, prefix: (company.issuePrefix ?? company.prefix) as string };
|
|
}
|
|
|
|
async function createIssue(board: APIRequestContext, companyId: string, data: Record<string, unknown>) {
|
|
const res = await board.post(`${BASE_URL}/api/companies/${companyId}/issues`, { data });
|
|
if (!res.ok()) throw new Error(`POST issues → ${res.status()}: ${await res.text()}`);
|
|
return res.json() as Promise<{ id: string; identifier: string; originKind: string | null }>;
|
|
}
|
|
|
|
async function mockAnnouncementFeed(page: Page) {
|
|
await page.route("**/api/announcements/current", (route) => route.fulfill({ json: {
|
|
id: ANNOUNCEMENT_ID,
|
|
eyebrow: "Introducing Connectors",
|
|
title: "Give your agents the access they need",
|
|
description: "Connect Gmail, GitHub, Notion, and more. Choose which agents get access, which accounts they use, and which actions they can take.",
|
|
image: { path: `assets/${"a".repeat(64)}.png`, alt: "Paperclip agents with their own sets of connected apps." },
|
|
primaryAction: { kind: "route", label: "Explore connectors", path: "/apps" },
|
|
} }));
|
|
await page.route(`**/api/announcements/${ANNOUNCEMENT_ID}/image`, (route) =>
|
|
route.fulfill({ contentType: "image/png", body: ONE_PIXEL_PNG }));
|
|
await page.route(`**/api/announcements/${ANNOUNCEMENT_ID}/dismiss`, (route) => route.fulfill({ status: 204 }));
|
|
await page.route("**/api/workspace-preferences/me", (route) => route.fulfill({ json: {
|
|
userId: "local-board", coordinatorWorkspaceEnabled: false, updatedAt: null,
|
|
} }));
|
|
await page.addInitScript(() => {
|
|
for (const key of Object.keys(window.localStorage)) {
|
|
if (key.includes("announcement")) window.localStorage.removeItem(key);
|
|
}
|
|
});
|
|
}
|
|
|
|
test.describe("Announcement well (N-13)", () => {
|
|
let board: APIRequestContext;
|
|
const companyIds: string[] = [];
|
|
|
|
test.beforeAll(async () => {
|
|
board = await pwRequest.newContext({ baseURL: BASE_URL });
|
|
});
|
|
|
|
test.afterAll(async () => {
|
|
for (const id of companyIds) await board.delete(`${BASE_URL}/api/companies/${id}`).catch(() => {});
|
|
await board.dispose();
|
|
});
|
|
|
|
test.beforeEach(async ({ page }) => {
|
|
await page.setViewportSize({ width: 1440, height: 900 });
|
|
await mockAnnouncementFeed(page);
|
|
});
|
|
|
|
test("a brand-new company with its onboarding task open does not show the upsell", async ({ page }) => {
|
|
const company = await createCompany(board, "E2E-N13-Onboarding");
|
|
companyIds.push(company.id);
|
|
const firstTask = await createIssue(board, company.id, {
|
|
title: "Plan our launch", status: "todo", onboardingFirstTask: true,
|
|
});
|
|
expect(firstTask.originKind).toBe("onboarding_first_task");
|
|
|
|
await page.goto(`/${company.prefix}/issues/${firstTask.identifier}`);
|
|
await expect(page.getByRole("button", { name: "Open account menu" })).toBeVisible();
|
|
await page.waitForTimeout(SETTLE_WAIT_MS);
|
|
await page.screenshot({ path: test.info().outputPath("onboarding-task.png") });
|
|
const well = page.getByRole("complementary", { name: "Paperclip announcements" });
|
|
await expect(well).toHaveCount(0);
|
|
|
|
// Positive control on the same page: once onboarding finishes, the same
|
|
// mocked announcement renders without a reload. This proves the absence
|
|
// above comes from the onboarding gate, not from a broken feed mock.
|
|
const finish = await board.patch(`${BASE_URL}/api/issues/${firstTask.id}`, { data: { status: "done" } });
|
|
if (!finish.ok()) throw new Error(`PATCH issue → ${finish.status()}: ${await finish.text()}`);
|
|
await expect(well).toBeVisible({ timeout: 20_000 });
|
|
await expect(well.getByRole("link", { name: "Explore connectors" })).toBeVisible();
|
|
});
|
|
});
|