mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-27 11:20:42 +01:00
fix(ui): identify accounts in AI connection choices (#17738)
## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work. > - Agent settings select an authorized AI account independently of the harness and model. > - Multiple accounts can have the same provider name. > - The account picker and legacy adoption dialog showed only that name. > - Operators could not distinguish a personal API key from a shared subscription during credential repair. > - This pull request shows the existing sign-in method, sharing scope, and available account identity before selection and adoption. > - Operators can select the intended account while the existing access and validation checks remain effective. ## Linked Issues or Issue Description Refs #14970 for the shared account selection control. No duplicate account-identity repair was found. **What happened?** Two authorized OpenAI accounts appeared as identical `OpenAI` choices. The adoption dialog repeated that ambiguous name. The list response already contained the account method, scope, and identity, but the UI omitted them. **Expected behavior** An operator can distinguish a personal API key from a company-shared subscription and confirm the selected account before saving. **Steps to reproduce** 1. Make a personal OpenAI API-key account and a shared OpenAI subscription available to an agent. Give both the name `OpenAI`. 2. Open Harness / Runtime and choose a managed connection for a legacy agent. 3. Inspect the two choices and the adoption confirmation. **Paperclip version or commit** Reproduced on master `a1f77821b9cd5c9c846fdf7a0fa5cc620ad2b6bb`. **Deployment mode** Hosted staging and the source UI regression. ## What Changed - Show the sign-in method, personal/shared scope, and available account or owner identity in connection labels. - Use the same label in the selected value and legacy adoption confirmation. Match the confirmation to both the connection and grant. Wrap dropdown choices so narrow screens retain the account identity, and expose the full selected label in the trigger title. - Add a behavioral regression with duplicate provider names, exact grant selection, and disabled unavailable accounts. - Update affected Storybook interaction selectors, the account guide, and the agent verification recipe. ## Verification - Before the fix, the new regression fails because duplicate accounts have no distinguishing metadata. The other 13 field tests pass. - After the fix, all 67 tests in `ui/src/components/ai-connections` pass. The regression selects the exact personal grant and keeps the unavailable option disabled. - `pnpm -r typecheck` and `pnpm build` pass locally. The full `pnpm test:run` command was stopped after embedded PostgreSQL failed to initialize after five attempts in an unrelated native-runtime suite. Its log is retained. Current-head CI partitions must establish full test coverage. - Token gates, map checks, Storybook catalog checks, and all 30 map/catalog contract tests pass. - In local Chrome, the existing production-component Storybook dropdown shows method and scope. Its nine interaction steps pass, as do all 19 steps in the new-agent connection-modes story. At a 320-pixel preview width, two shared OpenAI subscriptions show their distinct account identities in full, including during keyboard selection. These fixtures do not establish provider authentication. - Separately, the staging credential-repair workflow validated an existing authorized shared OpenAI connection, persisted the agent binding across reload, and resumed the original task into real provider/tool execution. This configuration recovery does not deploy this UI patch or establish task completion. - All CI completes on final head `a283bda2ad2f026f4f3c0fee03bf2962b0ad6e02`: 55 successful checks, the neutral Paperclip review, and two intentionally skipped Storybook checks; no pending or failed checks. This includes the partitioned tests, typecheck, build, runner suites, canary, and eight browser shards. Greptile and Paperclip review both rate this exact head 5/5 with no unresolved findings. [CI run](https://github.com/paperclipai/paperclip/actions/runs/40338613167). - The review's selected-trigger truncation finding is fixed in the final head. Its title assertion failed before the fix, and all 67 account-selection tests, UI typecheck, and token gates pass afterward. ## Risks - Labels are longer and can truncate in narrow selected-value controls; the trigger title exposes the complete label. Dropdown choices wrap, and the adoption dialog uses the complete label. - The UI uses existing authorized account metadata. It adds no credential reads, access grants, provider fallback, or account selection policy. ## Model Used OpenAI Codex (GPT-6). The exact serving model identifier and context window are not exposed in this session. The agent used code editing, terminal tests, and browser tools. ## 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>
This commit is contained in:
1 parent
0373f9f261
commit
d64cb7be52
9 files changed
+84
-20
No files matched your search
@@ -59,6 +59,12 @@ the selected connection owns its provider routing.
|
||||
Changing those separately may make a binding incompatible; saving then requires
|
||||
a compatible choice. Agent configuration cannot grant access to another account.
|
||||
|
||||
Agent account choices and legacy-adoption confirmations show the sign-in method,
|
||||
personal or company-shared scope, and available account or owner identity beside
|
||||
the connection name. Accounts with the same provider name can be distinguished
|
||||
before selection. These labels use existing authorized account metadata, never
|
||||
credentials or authentication paths.
|
||||
|
||||
Personal defaults are unique per company, user, and provider. A Claude bot can use one user’s subscription and another user’s API key without changing its harness or model. Explicit shared selections remain pinned to the selected account and method.
|
||||
The first successful personal connection sets a default only when none exists.
|
||||
The additive `ai_provider_defaults` table preserves the legacy per-method preferences. Migration selects each user’s most recently updated provider preference (including unavailable accounts), and rerunning it never overwrites a provider default. New writes maintain the legacy table for older servers. A database trigger propagates older servers’ explicit default updates to the provider default. Inserting an additional method default does not replace an existing provider default.
|
||||
|
||||
@@ -128,6 +128,14 @@ from the hiring picker; API fixture creation does not prove this UI journey.
|
||||
|
||||
Automated: [instruction service](../../server/src/__tests__/agent-instructions-service.test.ts) and [instruction revisions](../../server/src/__tests__/agent-instruction-revisions.test.ts) cover file/revision contracts.
|
||||
|
||||
The [AI account field regression](../../ui/src/components/ai-connections/AiConnectionField.test.tsx)
|
||||
checks duplicate provider names, account identity in choices and adoption confirmation,
|
||||
the exact selected grant, and disabled unavailable accounts with authorized metadata fixtures.
|
||||
Manually compare a personal API key and shared subscription with the same name in
|
||||
Harness / Runtime. Confirm their method, scope and available identity before saving.
|
||||
Reload the saved choice and verify the next eligible task uses it; labels alone
|
||||
do not establish credential validity or change account access.
|
||||
|
||||
Manual: Save a harmless instruction edit, reload, and verify its revision and effect in the next eligible run. Pause and resume the disposable agent; confirm pause prevents new work and does not erase previous runs.
|
||||
|
||||
For failed setup, use an unavailable model or invalid command on a disposable agent.
|
||||
|
||||
@@ -17,7 +17,6 @@ vi.mock("./AiConnectionCredentialStep", () => ({
|
||||
return <div>Provider sign-in</div>;
|
||||
},
|
||||
}));
|
||||
vi.mock("./AiConnectionManagement", () => ({ AiConnectionLegacyNotice: () => null }));
|
||||
vi.mock("./AiProviderSetup", () => ({ AiProviderSetup: () => <div>Advanced provider setup</div> }));
|
||||
vi.mock("@/pages/apps/AppLogo", () => ({ AppLogo: () => null }));
|
||||
|
||||
@@ -82,6 +81,33 @@ it("reconnects the unavailable personal default in place", async () => {
|
||||
expect(onChange).toHaveBeenCalledWith({ provider: "anthropic", method: "subscription", mode: "responsible_user" });
|
||||
});
|
||||
|
||||
it("identifies duplicate-named accounts before adopting the exact personal grant", async () => {
|
||||
const personal = account({ id: "00000000-0000-4000-8000-000000000001", grantId: "00000000-0000-4000-8000-000000000002", provider: "openai", method: "api_key", name: "OpenAI", ownerName: "Avery", status: "connected", isDefault: false });
|
||||
const shared = account({ id: "00000000-0000-4000-8000-000000000003", grantId: "00000000-0000-4000-8000-000000000004", provider: "openai", method: "subscription", name: "OpenAI", accountLabel: "engineering@example.test", ownership: "shared", ownerUserId: undefined, status: "connected", isDefault: false });
|
||||
const sharedKey = account({ id: "00000000-0000-4000-8000-000000000005", grantId: "00000000-0000-4000-8000-000000000006", provider: "openai", method: "api_key", name: "OpenAI", ownerName: "Engineering", ownership: "shared", ownerUserId: undefined, status: "connected", isDefault: false });
|
||||
const unavailable = account({ id: "expired-account", grantId: "expired-grant", provider: "openai", method: "api_key", name: "Expired account", ownerName: "Avery" });
|
||||
mocks.list.mockResolvedValue({ currentUserId: "owner", connections: [shared, personal, sharedKey, unavailable], canManageConnections: true });
|
||||
flushSync(() => root.render(<QueryClientProvider client={client}>
|
||||
<AiConnectionField companyId="company" agentId="agent" agentName="Nova" adapterType="codex_local" legacy onChange={onChange} />
|
||||
</QueryClientProvider>));
|
||||
await settle();
|
||||
await click("Choose a managed connection");
|
||||
const trigger = container.querySelector('[role="combobox"][aria-label="Connection"]')!;
|
||||
flushSync(() => trigger.dispatchEvent(new KeyboardEvent("keydown", { key: "ArrowDown", bubbles: true })));
|
||||
await settle();
|
||||
const choices = [...document.querySelectorAll<HTMLElement>('[role="option"]')];
|
||||
expect(choices.map(choice => choice.textContent)).toContain("OpenAI · API key · Personal · Avery");
|
||||
expect(choices.map(choice => choice.textContent)).toContain("OpenAI · ChatGPT subscription · Company shared · engineering@example.test");
|
||||
expect(choices.map(choice => choice.textContent)).toContain("OpenAI · API key · Company shared · Engineering");
|
||||
expect(choices.find(choice => choice.textContent?.includes("Expired account"))?.getAttribute("aria-disabled")).toBe("true");
|
||||
flushSync(() => choices.find(choice => choice.textContent === "OpenAI · API key · Personal · Avery")!.click());
|
||||
await settle();
|
||||
expect(document.querySelector('[role="dialog"]')?.textContent).toContain("OpenAI · API key · Personal · Avery");
|
||||
expect(onChange).not.toHaveBeenCalled();
|
||||
await click("Use this binding when saved");
|
||||
expect(onChange).toHaveBeenCalledExactlyOnceWith({ provider: "openai", method: "api_key", mode: "delegated", connectionId: personal.id, grantId: personal.grantId });
|
||||
});
|
||||
|
||||
it("opens provider setup directly when connecting from the advanced mode", async () => {
|
||||
await mount([], true, true);
|
||||
await click("Connect another account");
|
||||
@@ -163,7 +189,7 @@ it("offers Pi account selection before choosing its required model", async () =>
|
||||
});
|
||||
|
||||
it.each(["openai", "anthropic", "google", "openrouter"] as const)("keeps a matching saved Pi %s API-key account selectable", async provider => {
|
||||
const saved = account({ provider, method: "api_key", name: "Pi provider account", status: "connected" });
|
||||
const saved = account({ provider, method: "api_key", name: "Pi provider account", accountLabel: "avery@example.test", status: "connected" });
|
||||
mocks.list.mockResolvedValue({ currentUserId: "owner", connections: [saved], canManageConnections: true });
|
||||
flushSync(() => root.render(<QueryClientProvider client={client}>
|
||||
<AiConnectionField companyId="company" agentId="agent" agentName="Pi" adapterType="pi_local" model={`${provider}/provider-model`}
|
||||
@@ -174,6 +200,7 @@ it.each(["openai", "anthropic", "google", "openrouter"] as const)("keeps a match
|
||||
expect(container.textContent).not.toContain("incompatible");
|
||||
expect(container.textContent).not.toContain("does not support");
|
||||
const trigger = container.querySelector('[role="combobox"][aria-label="Connection"]')!;
|
||||
expect(trigger.getAttribute("title")).toBe("Pi provider account · API key · Personal · avery@example.test");
|
||||
flushSync(() => trigger.dispatchEvent(new KeyboardEvent("keydown", { key: "ArrowDown", bubbles: true })));
|
||||
await settle();
|
||||
const choice = [...document.querySelectorAll<HTMLElement>('[role="option"]')].find(node => node.textContent?.includes("Pi provider account"));
|
||||
|
||||
@@ -14,6 +14,7 @@ import { AiConnectionSelect } from "./AiConnectionSelect";
|
||||
import { AiProviderSetup } from "./AiProviderSetup";
|
||||
import { AiConnectionCredentialStep } from "./AiConnectionCredentialStep";
|
||||
import { AiConnectionLegacyNotice } from "./AiConnectionManagement";
|
||||
import { aiConnectionDisplayLabel } from "./model";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import {
|
||||
@@ -89,6 +90,11 @@ export function AiConnectionField({
|
||||
enabled: Boolean(provider),
|
||||
});
|
||||
const personalDefault = accounts.data?.connections.find((account) => account.provider === provider && account.isDefault && account.ownership === "personal" && account.ownerUserId === accounts.data.currentUserId);
|
||||
const pendingAccount = pendingAdoption?.mode === "responsible_user"
|
||||
? personalDefault
|
||||
: pendingAdoption && pendingAdoption.mode !== "router"
|
||||
? accounts.data?.connections.find(account => account.id === pendingAdoption.connectionId && account.grantId === pendingAdoption.grantId)
|
||||
: undefined;
|
||||
const selectDefault = useMutation({
|
||||
mutationFn: async (result: NonNullable<typeof savedAccount>) => {
|
||||
// Reconnect retains the existing default and its access. A new account
|
||||
@@ -177,10 +183,8 @@ export function AiConnectionField({
|
||||
</DialogHeader>
|
||||
{pendingAdoption?.mode !== "router" && <p className="text-sm">
|
||||
{pendingAdoption?.mode === "responsible_user"
|
||||
? `Responsible user’s default. For you: ${accounts.data?.connections.find((account) => account.isDefault && account.provider === provider)?.name ?? "Not connected"}. Other users use their own default.`
|
||||
: accounts.data?.connections.find(
|
||||
(account) => account.id === pendingAdoption?.connectionId,
|
||||
)?.name}
|
||||
? `Responsible user’s default. For you: ${pendingAccount ? aiConnectionDisplayLabel(pendingAccount) : "Not connected"}. Other users use their own default.`
|
||||
: pendingAccount && aiConnectionDisplayLabel(pendingAccount)}
|
||||
</p>}
|
||||
{pendingAdoption?.mode !== "router" && <p className="text-xs text-muted-foreground">
|
||||
After adoption, missing credentials block execution. Previous
|
||||
|
||||
@@ -14,15 +14,15 @@ import {
|
||||
} from "@/components/ui/select";
|
||||
import { AppLogo } from "@/pages/apps/AppLogo";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { aiConnectionProblem, personalAiDefault } from "./model";
|
||||
import { aiConnectionDisplayLabel, aiConnectionProblem, personalAiDefault } from "./model";
|
||||
import type { AiConnectionPickerProps } from "./AiConnectionPicker";
|
||||
import type { AiConnectionSummary } from "./model";
|
||||
|
||||
function ConnectionLabel({ connection, label }: { connection: AiConnectionSummary; label?: string }) {
|
||||
function ConnectionLabel({ connection, label, wrap = false }: { connection: AiConnectionSummary; label?: string; wrap?: boolean }) {
|
||||
return (
|
||||
<span className="flex min-w-0 items-center gap-2">
|
||||
<AppLogo name={connection.name} brandKey={aiConnectionCatalogSlug(connection.provider, connection.routing)} size={20} compact />
|
||||
<span className="truncate">{label ?? connection.name}</span>
|
||||
<span className={wrap ? "min-w-0 break-words whitespace-normal" : "truncate"}>{label ?? aiConnectionDisplayLabel(connection)}</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
@@ -119,7 +119,7 @@ export function AiConnectionSelect({
|
||||
onValueChange={change}
|
||||
disabled={readOnly || loading || Boolean(error)}
|
||||
>
|
||||
<SelectTrigger id={selectId} aria-label="Connection" className="w-full">
|
||||
<SelectTrigger id={selectId} aria-label="Connection" title={selected ? aiConnectionDisplayLabel(selected) : undefined} className="w-full">
|
||||
<SelectValue
|
||||
placeholder={
|
||||
loading ? "Loading connections…" : "Choose a connection"
|
||||
@@ -135,7 +135,7 @@ export function AiConnectionSelect({
|
||||
<SelectContent>
|
||||
{incompatible && selected && (
|
||||
<SelectItem value={selected.id} disabled>
|
||||
<ConnectionLabel connection={selected} label={`${selected.name} · Incompatible`} />
|
||||
<ConnectionLabel connection={selected} label={`${aiConnectionDisplayLabel(selected)} · Incompatible`} wrap />
|
||||
</SelectItem>
|
||||
)}
|
||||
{compatible.map((c) => (
|
||||
@@ -144,7 +144,7 @@ export function AiConnectionSelect({
|
||||
value={c.id}
|
||||
disabled={Boolean(aiConnectionProblem(c))}
|
||||
>
|
||||
<ConnectionLabel connection={c} label={`${c.name}${c.status !== "connected" ? " · Reconnect" : ""}`} />
|
||||
<ConnectionLabel connection={c} label={`${aiConnectionDisplayLabel(c)}${c.status !== "connected" ? " · Reconnect" : ""}`} wrap />
|
||||
</SelectItem>
|
||||
))}
|
||||
{(canUseDefault || value?.mode === "responsible_user") && (
|
||||
|
||||
@@ -57,6 +57,16 @@ export function aiMethodLabel(provider: AiProvider, method: AiAuthMethod) {
|
||||
: provider === "github" ? "Personal access token" : "API key";
|
||||
}
|
||||
|
||||
export function aiConnectionDisplayLabel(connection: AiConnectionSummary) {
|
||||
const personal = connection.ownership === "personal";
|
||||
return [
|
||||
connection.name,
|
||||
aiMethodLabel(connection.provider, connection.method),
|
||||
personal ? "Personal" : "Company shared",
|
||||
connection.accountLabel || connection.ownerName,
|
||||
].filter(Boolean).join(" · ");
|
||||
}
|
||||
|
||||
export function matchesAiRequirement(
|
||||
connection: AiConnectionSummary,
|
||||
requirement: AiConnectionRequirement,
|
||||
|
||||
@@ -120,7 +120,7 @@ export const ConnectionJourney: Story = { play: async ({ canvasElement }) => {
|
||||
await expect(await screen.findByText("1 other selected agent won’t change.")).toBeVisible();
|
||||
await userEvent.click(screen.getByRole("checkbox", { name: "Change connection" }));
|
||||
await userEvent.click(await screen.findByRole("combobox", { name: "Connection" }));
|
||||
await userEvent.click(screen.getByRole("option", { name: "OpenAI · Production" }));
|
||||
await userEvent.click(screen.getByRole("option", { name: /^OpenAI · Production ·/ }));
|
||||
await expect(screen.getByRole("button", { name: "Update 4 agents" })).toBeEnabled();
|
||||
await userEvent.click(screen.getByRole("button", { name: "Update 4 agents" }));
|
||||
await expect(await within(canvasElement).findByText("Updated 4 Codex agents.")).toBeVisible();
|
||||
@@ -131,7 +131,7 @@ export const RetryJourney: Story = { args: { failOne: true }, play: async ({ can
|
||||
const screen = await selectCodex(canvasElement);
|
||||
await userEvent.click(await screen.findByRole("checkbox", { name: "Change connection" }));
|
||||
await userEvent.click(await screen.findByRole("combobox", { name: "Connection" }));
|
||||
await userEvent.click(screen.getByRole("option", { name: "OpenAI · Production" }));
|
||||
await userEvent.click(screen.getByRole("option", { name: /^OpenAI · Production ·/ }));
|
||||
await expect(screen.getByRole("button", { name: "Update 4 agents" })).toBeEnabled();
|
||||
await userEvent.click(screen.getByRole("button", { name: "Update 4 agents" }));
|
||||
await expect(await screen.findByText("3 updated · 1 couldn’t be saved")).toBeVisible();
|
||||
|
||||
@@ -46,7 +46,7 @@ export const ConnectionJourney: Story = {
|
||||
await step("Changing only the connection keeps every model", async () => {
|
||||
await userEvent.click(screen.getByRole("checkbox", { name: "Change connection" }));
|
||||
await userEvent.click(screen.getByRole("combobox", { name: "Connection" }));
|
||||
await userEvent.click(screen.getByRole("option", { name: "OpenAI · Production" }));
|
||||
await userEvent.click(screen.getByRole("option", { name: /^OpenAI · Production ·/ }));
|
||||
await userEvent.click(screen.getByRole("button", { name: "Update 4 agents" }));
|
||||
await expect(await canvas.findByText("Updated 4 Codex agents.")).toBeVisible();
|
||||
await expect(canvas.getByTestId("agent-row-scout")).toHaveTextContent("gpt-5.3-codex");
|
||||
@@ -100,7 +100,7 @@ export const RetryJourney: Story = {
|
||||
play: async ({ canvasElement }) => {
|
||||
const screen = within(canvasElement.ownerDocument.body);
|
||||
await userEvent.click(await screen.findByRole("combobox", { name: "Connection" }));
|
||||
await userEvent.click(screen.getByRole("option", { name: "OpenAI · Production" }));
|
||||
await userEvent.click(screen.getByRole("option", { name: /^OpenAI · Production ·/ }));
|
||||
await userEvent.click(screen.getByRole("button", { name: "Update 4 agents" }));
|
||||
await expect(await screen.findByText("3 updated · 1 couldn’t be saved")).toBeVisible();
|
||||
await expect(within(canvasElement).getByTestId("agent-row-atlas")).toHaveTextContent("OpenAI · Production");
|
||||
|
||||
@@ -61,6 +61,13 @@ const accounts: AiManagedConnectionSummary[] = [
|
||||
isDefault: true,
|
||||
status: "connected",
|
||||
},
|
||||
...["engineering@example.test", "design@example.test"].map((accountLabel, index) => ({
|
||||
id: `00000000-0000-4000-8000-00000000000${index + 8}`,
|
||||
grantId: `00000000-0000-4000-8000-00000000001${index}`,
|
||||
companyId, provider: "openai" as const, method: "subscription" as const,
|
||||
name: "OpenAI", ownership: "shared" as const, accountLabel,
|
||||
isDefault: false, status: "connected" as const,
|
||||
})),
|
||||
];
|
||||
function Providers({ canManageConnections = true }: { canManageConnections?: boolean }) {
|
||||
const [client] = useState(() => {
|
||||
@@ -200,7 +207,7 @@ export const NewAgentConnectionModes: Story = {
|
||||
await expect(canvas.getByRole("radio", { name: "Advanced Custom Gateway" })).toBeChecked();
|
||||
await expect(canvas.getByRole("button", { name: "Use connection" })).toBeDisabled();
|
||||
await userEvent.click(canvas.getByRole("combobox", { name: "Connection" }));
|
||||
await userEvent.click(within(canvasElement.ownerDocument.body).getByRole("option", { name: "Company OpenRouter" }));
|
||||
await userEvent.click(within(canvasElement.ownerDocument.body).getByRole("option", { name: "Company OpenRouter · API key · Company shared" }));
|
||||
await userEvent.click(canvas.getByRole("button", { name: "Use connection" }));
|
||||
await expect(canvas.getByRole("status")).toHaveTextContent("Continue to Configure with openrouter.");
|
||||
await userEvent.click(canvas.getByRole("radio", { name: "OpenAI Subscription" }));
|
||||
@@ -259,13 +266,15 @@ export const CompatibleConnectionDropdown: Story = {
|
||||
body.getByRole("option", { name: "Responsible user’s default" }),
|
||||
).toHaveAttribute("data-state", "checked");
|
||||
await expect(
|
||||
body.getByRole("option", { name: "Company OpenRouter" }),
|
||||
body.getByRole("option", { name: "Company OpenRouter · API key · Company shared" }),
|
||||
).toBeVisible();
|
||||
await expect(body.getByRole("option", { name: "OpenAI · ChatGPT subscription · Company shared · engineering@example.test" })).toBeVisible();
|
||||
await expect(body.getByRole("option", { name: "OpenAI · ChatGPT subscription · Company shared · design@example.test" })).toBeVisible();
|
||||
await expect(
|
||||
body.queryByRole("option", { name: "My Claude subscription" }),
|
||||
body.queryByRole("option", { name: /My Claude subscription/ }),
|
||||
).not.toBeInTheDocument();
|
||||
await userEvent.click(
|
||||
body.getByRole("option", { name: "Company OpenRouter" }),
|
||||
body.getByRole("option", { name: "Company OpenRouter · API key · Company shared" }),
|
||||
);
|
||||
await expect(
|
||||
within(canvasElement).getByRole("combobox", { name: "Connection" }),
|
||||
|
||||
Reference in new issue
Block a user