Files
PaperClipAI/ui/src/lib/saved-provider-credentials.test.ts
T
DottaandPaperclip e9828f8bf4 fix: reuse saved model connections during agent setup (#13161)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Agent setup connects each agent to a model provider.
> - The organization can already hold subscription logins and API keys.
> - The simplified setup flow did not consistently offer those saved
credentials.
> - This pull request restores reuse and selects a saved connection by
default.
> - Agents keep secret references, so reuse does not copy or rotate
credentials.

## Linked Issues or Issue Description

Related change: #13011. Searched public issues and PRs; no duplicate fix
found.

**What happened?**

Onboarding and new-agent setup could ask for a new API key or sign-in
despite an existing saved connection. A general environment auth signal
could also be mistaken for the owner's saved Claude subscription.

**Expected behavior**

Offer saved credentials from the selected organization. Default to a
saved subscription when one exists. Otherwise select a saved API key.
Keep the option to enter a new key or sign in to another account.

**Steps to reproduce**

1. Save a Claude or OpenAI API key, or complete a supported subscription
login.
2. Add another agent with the same provider.
3. Open the provider connection step.
4. Check whether the saved credential is available and selected.

**Paperclip version or commit**

Reproduced on 5cb4f061d after #13011. This branch is rebased onto
current master.

**Deployment mode**

Built from source. Tested in an isolated local test drive with embedded
storage and board access.

## What Changed

- Add a shared saved-credential lookup and picker for active personal
and organization keys.
- Reuse saved Claude subscriptions and saved Codex account homes. Select
an existing connection by default.
- Preserve secret references through connection tests and agent
creation, including the native Claude and Codex runner setup paths.
- Store newly entered onboarding keys separately. Do not rotate another
agent's key.
- Keep explicit choices during metadata refresh. Prevent refreshes from
remounting an active login panel.
- Add integration tests and production-component Storybook stories.
Document connection reuse.

## Verification

- All 5,628 UI tests passed before rebase.
- Twenty targeted server credential tests passed.
- UI typecheck, UI build, token gates, and diff whitespace checks
passed.
- Browser walkthroughs covered onboarding and new-agent setup, saved
keys, saved subscription fixtures, and new sign-in screens.
- Live Claude and Codex API-key probes succeeded. Created both agents
and confirmed that each retained its saved-secret reference. Both secret
versions remained unchanged. Codex passed after one retry.
- Live subscription authentication was not repeated. Subscription flows
use fixture browser tests and integration tests.
- After rebase and the cache fix, all 109 focused onboarding and
agent-creation tests passed.
- Full repository `pnpm build` and `pnpm -r typecheck` passed.
- The full local test attempt encountered timeouts and embedded
PostgreSQL startup failures under parallel load. All four affected
suites passed in isolation: 20 tests, with no code changes. The complete
CI matrix passed, including all workspace, general server, serialized
server, browser end-to-end, build, typecheck, and canary dry-run checks.
- Greptile reviewed commit d53ddf6b82:
5/5, successful check, no review threads.

## Risks

- The default connection mode changes when saved credentials exist. A
saved subscription takes priority over saved API keys; personal keys
appear before organization keys.
- A listed credential can be expired or unavailable in the selected
environment. The existing connection test still checks it.
- No database migration or API contract change is required.

## Model Used

OpenAI Codex, GPT-6. The exact runtime model identifier and
context-window size are not exposed in this session. Used reasoning,
code execution, repository tools, and browser automation.

## 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-10 12:57:53 -05:00

96 lines
2.5 KiB
TypeScript

import { describe, expect, it } from "vitest";
import type { CompanySecret } from "@paperclipai/shared";
import type { MyUserSecretEntry } from "../api/secrets";
import {
savedProviderKeys,
savedCodexSubscriptions,
} from "./saved-provider-credentials";
const secret = (overrides = {}) =>
({
id: "s1",
companyId: "c1",
key: "ANTHROPIC_API_KEY",
name: "Claude",
scope: "company",
status: "active",
...overrides,
}) as CompanySecret;
const personal = (key = "ANTHROPIC_API_KEY.setup.abc", overrides = {}) =>
({
definition: {
id: "d1",
companyId: "c1",
key,
name: "My Claude",
status: "active",
...overrides,
},
secret: secret({ scope: "user" }),
}) as MyUserSecretEntry;
describe("saved provider keys", () => {
it("reuses canonical and setup keys with references, including normalized organization keys", () => {
expect(
savedProviderKeys(
"c1",
"ANTHROPIC_API_KEY",
[personal()],
[secret({ key: "anthropic_api_key" })],
),
).toEqual([
{
id: "user:d1",
label: "My Claude (Your key)",
binding: {
type: "user_secret_ref",
key: "ANTHROPIC_API_KEY.setup.abc",
version: "latest",
},
},
{
id: "company:s1",
label: "Claude (Organization key)",
binding: { type: "secret_ref", secretId: "s1", version: "latest" },
},
]);
});
it("excludes unavailable, wrong-provider, and foreign-company credentials", () => {
expect(
savedProviderKeys(
"c1",
"ANTHROPIC_API_KEY",
[
personal("OPENAI_API_KEY"),
personal(undefined, { status: "disabled" }),
{ ...personal(), secret: null },
{ ...personal(), secret: secret({ status: "archived" }) },
personal(undefined, { companyId: "c2" }),
],
[
secret({ companyId: "c2" }),
secret({ status: "disabled" }),
secret({ scope: "user" }),
secret({ key: "ANTHROPIC_API_KEY_OTHER" }),
],
),
).toEqual([]);
});
});
it("lists only active company Codex account connections", () => {
const account = secret({ name: "CODEX_HOME_team" });
expect(
savedCodexSubscriptions("c1", [
account,
{ ...account, status: "disabled" },
{ ...account, companyId: "c2" },
secret(),
]),
).toEqual([
{
id: "company:s1",
label: "ChatGPT account · team",
binding: { type: "secret_ref", secretId: "s1", version: "latest" },
},
]);
});