Files
PaperClipAI/tests/e2e/conference-room-typing-intro.spec.ts
DottaandPaperclip 8d6232e7b0 feat: reuse provider sign-in across AI connection workflows (#13248)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Users connect provider accounts during onboarding and agent setup.
> - They should reuse and manage those accounts through the existing
Connectors interface.
> - A second login wizard would diverge from the established provider
workflows.
> - This pull request composes the existing sign-in components into
Connections and agent configuration.
> - Users can select accounts without changing their agent's harness or
model.

## Linked Issues or Issue Description

**Problem or motivation**
AI credentials are configured separately from Connections. Agents cannot
consistently reuse a responsible user's account or a permitted shared
account.

**Proposed solution**
Manage AI accounts with the existing Connections grants and permissions.
Keep model and harness selection independent from credential selection.
Preserve legacy authentication until validated adoption.

**Alternatives considered**
A separate credential registry would duplicate ownership and access
policy. Automatic fallback would risk using the wrong account.

**Roadmap alignment**
This extends the shipped Apps, multi-user, secrets, and agent-runtime
capabilities. The maintainer requested the feature and reviewed the UI.
Related groundwork: #11899 (connection permissions), #10910 (connection
wizard), #11692 (Claude subscription profiles), and #11854 (Codex
account rotation).

## What Changed

- Add compact AI-account management to the existing Connectors pages.
- Reuse AgentProviderConnection, AdapterLoginPanel, AdapterLoginChrome,
and authentication controllers.
- Add the shared connection picker to agent setup/settings and task
requests.
- Preserve onboarding's sequence and reuse existing accounts.
- Add local-login recovery, retry, cancellation, and React StrictMode
handling.
- Add interactive Storybook scenarios, design-guide examples, and app
acceptance checks.

This is part 2 of the AI Connections change. The runtime foundation in
#13247 is merged. This PR now targets master.

## Verification

- Updated against master `47ded8bf9`, including the landed runtime
foundation and upstream task-search changes.
- Full workspace typecheck, production build, Storybook build, and token
gates passed on the integrated branch. Final local-login changes passed
59 focused tests; new-agent and inbox regression suites passed 63 tests.
- Browser checks verified automatic local Claude account detection,
resumable Codex login commands, retry, focus restoration, and
desktop/phone layouts. Commands create their isolated directory before
invoking the CLI.
- All CI test, browser, build, packaging, and runner jobs passed on
final head `dd17d3211931dd70aaa6ea619d83a7f9966dd18e`. The fresh
Greptile review is 5/5, the security scan passed, and there are no
unresolved review threads. The final CI aggregate gates passed.
- Local general-server coverage passed 11,804 tests; three
port-collision failures passed in an isolated 25-test rerun. All 6,111
UI tests passed. CLI coverage passed 484 tests; its remaining doctor
test requires port 3199, which is occupied by an unrelated report server
on this Mac. The complete CLI suite passed in CI.
- Live browser testing verified Codex API-key reconnect inside a task
card on desktop and phone. Real provider runs resumed and completed with
unchanged connection/grant identity and agent routing.
- Tested opening, cancelling, reopening, and completing connection
creation. A regression confirms Connect another account cannot submit
the new-agent form or copy provider keys into agent settings.
- Added shared inline repair, automatic local sign-in checks, and
responsive connection dialogs. Standalone Daytona installation ignores
workspace configuration and suppresses dependency scripts. Its
standalone build also passed with CI's exact pnpm 9.15.4.
- Destructive live tests are excluded by default. Explicit opt-in, local
deployment checks, and matching disposable fixture identities are
required before any mutation.

## Risks

- Local Codex/Grok creation requires the connection-specific terminal
login command.
- Browser sign-in uses the existing supported-environment controllers.
- This update verifies live local Claude detection and Codex API-key
task repair. New subscription authorization/refresh and
independent-human/native-runner isolation were not reverified in this
update.
- No agent automatically adopts managed Connections.

## Model Used

OpenAI GPT-6 through Codex, with reasoning, repository tools, code
execution, and browser testing. 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
- [x] 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>
2026-09-12 16:51:26 -05:00

172 lines
6.8 KiB
TypeScript

import { test, expect, type Page } from "@playwright/test";
import { mockOnboardingLocalAiConnection } from "./helpers/onboarding-ai-connection";
import {
expectLandsOnFirstTaskWithoutDashboardBounce,
instrumentNavLog,
} from "./helpers/onboarding-landing";
/**
* E2E: post-wizard onboarding launch.
*
* Completing the onboarding wizard now creates the first assigned task and
* drops the user straight onto that task's detail page (not the dashboard),
* so they land in the conversation the agent will start in. The chat intro
* still has unit coverage in BoardChat tests.
*
* PAP-404: onboarding used to intermittently bounce to the company dashboard.
* The bounce only reproduces when the instance already has ≥1 company (the
* board's test ports), so the second test seeds a company first to exercise
* exactly that failing condition.
*/
const FIRST_TASK_TITLE = "Paperclip onboarding";
/**
* Intercept authentication, environment checks, and hiring so no real CLI check
* runs and no real agent process spawns (the hire still happens server-side
* with an inert http adapter).
*/
async function installLaunchIntercepts(page: Page, baseURL?: string) {
await mockOnboardingLocalAiConnection(page);
await page.route("**/test-environment", (route) =>
route.fulfill({
contentType: "application/json",
body: JSON.stringify({ status: "pass", checks: [] }),
}),
);
await page.route("**/agent-hires", async (route) => {
const req = route.request();
const body = JSON.parse(req.postData() || "{}");
const auth = req.headers().authorization;
const real = await fetch(new URL(req.url(), baseURL).toString(), {
method: "POST",
headers: {
"Content-Type": "application/json",
...(auth ? { Authorization: auth } : {}),
},
body: JSON.stringify({
name: body.name,
role: body.role,
adapterType: "http",
adapterConfig: { url: "http://127.0.0.1:1/dead" },
runtimeConfig: { heartbeat: { enabled: false } },
}),
});
await route.fulfill({
status: real.status,
contentType: "application/json",
body: await real.text(),
});
});
}
/** Drive the wizard from the /onboarding route through to "Get started". */
async function runOnboardingWizard(page: Page, companyName: string) {
await page.goto("/onboarding");
// Launcher card path (existing companies) — enter the wizard if the
// route shows a launcher instead of opening the wizard directly.
const startBtn = page.getByRole("button", { name: /Start Onboarding/i });
if (await startBtn.count()) await startBtn.first().click();
// Step 0: front door (skipped when the wizard opens on the create path).
const frontDoor = page.getByText("Build a new organization");
if (await frontDoor.count()) await frontDoor.first().click();
// Step 1: company name.
await page.getByPlaceholder("e.g. Northwind Labs").fill(companyName);
await page.getByRole("button", { name: /^Continue/ }).click();
// Step 1's "Next" creates the company; the mission step no longer runs.
// Step 3: name the agent. The role picker is gone — the arc asks for a
// name and hires under the neutral `general` role.
await page.waitForSelector("#onboarding-agent-name", { timeout: 30_000 });
await page.locator("#onboarding-agent-name").fill("Ada");
await page.getByRole("button", { name: /^Next$/ }).click();
// Step 4: pick a model source, then advance. Nothing is selected on arrival
// — the row is a question, not a confirmation — so the CTA is disabled until
// a tile is pressed. By role rather than by label: which adapters the tiles
// offer depends on the registry this environment reports.
const source = page.getByRole("radio").first();
await source.waitFor({ timeout: 30_000 });
await source.click();
// "Connect", not "Next": this step's button starts the sign-in where there
// is one to start, so it is named for what it does. This test simulates
// successful local account connection before the environment check and hire.
//
// Waited on for enabled rather than for visible: it is already on screen,
// disabled, and clicking a disabled button raises nothing and does nothing.
const connectNext = page.getByRole("button", { name: /^Connect$/ });
await expect(connectNext).toBeEnabled({ timeout: 30_000 });
await connectNext.click();
// Step 5: review → Get started creates the first task and opens its
// detail page.
const getStarted = page.getByRole("button", { name: /Get started/ });
await getStarted.waitFor({ timeout: 20_000 });
await getStarted.click();
}
async function assertFirstTaskExists(page: Page, companyName: string) {
const companiesRes = await page.request.get("/api/companies");
expect(companiesRes.ok()).toBe(true);
const companies = await companiesRes.json();
const company = companies.find(
(candidate: { name: string }) => candidate.name === companyName,
);
expect(company).toBeTruthy();
const issuesRes = await page.request.get(`/api/companies/${company.id}/issues`);
expect(issuesRes.ok()).toBe(true);
const issues = await issuesRes.json();
const firstTask = issues.find(
(candidate: { title: string }) => candidate.title === FIRST_TASK_TITLE,
);
expect(firstTask).toBeTruthy();
await expect(page.getByText(FIRST_TASK_TITLE).first()).toBeVisible({
timeout: 15_000,
});
}
test.describe("First-task launch after onboarding wizard", () => {
test("creates the first task and opens its detail page", async ({ page, baseURL }) => {
await instrumentNavLog(page);
await installLaunchIntercepts(page, baseURL);
const companyName = `E2E-TypingIntro-${Date.now()}`;
await runOnboardingWizard(page, companyName);
await expectLandsOnFirstTaskWithoutDashboardBounce(page);
await assertFirstTaskExists(page, companyName);
});
// PAP-404 regression: the dashboard bounce only fires when the instance
// already has a company for the route-sync effect to reset selection to.
// Seed one first, then onboard a brand-new company and assert we still land
// on the first task without a dashboard bounce.
test("lands on the first task even when a company already exists", async ({
page,
baseURL,
}) => {
await instrumentNavLog(page);
await installLaunchIntercepts(page, baseURL);
// Seed a pre-existing company so the companies list is non-empty when the
// wizard launches — the exact condition that reproduced the bounce.
const seedRes = await page.request.post("/api/companies", {
data: { name: `E2E-Seed-${Date.now()}` },
});
expect(seedRes.ok()).toBe(true);
const companyName = `E2E-TypingIntro-Existing-${Date.now()}`;
await runOnboardingWizard(page, companyName);
await expectLandsOnFirstTaskWithoutDashboardBounce(page);
await assertFirstTaskExists(page, companyName);
});
});