fix(connections): label GitHub scopes explicitly

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
Dotta 2026-09-05 08:53:51 -05:00
parent 408c5996e2
commit 5193144e46
2 changed files with 58 additions and 25 deletions

View File

@ -262,7 +262,10 @@ function personalGrant(overrides: Record<string, unknown> = {}) {
};
}
function dedicatedGitHubGrant(overrides: Record<string, unknown> = {}) {
function dedicatedGitHubGrant(
overrides: Record<string, unknown> = {},
githubOverrides: Record<string, unknown> = {},
) {
return organizationGrant({
id: "grant-agent",
kind: "agent",
@ -282,6 +285,7 @@ function dedicatedGitHubGrant(overrides: Record<string, unknown> = {}) {
webhookHealth: "pending",
lastWebhookAt: null,
lastAccessRefreshAt: "2026-09-05T12:00:00.000Z",
...githubOverrides,
},
},
...overrides,
@ -1467,7 +1471,7 @@ describe("AppDetail", () => {
await renderAppDetail();
expect(container.querySelector('a[href="/agents/agent-1"]')?.textContent).toContain("Used only by Coder");
expect(container.querySelector('a[href="/agents/coder"]')?.textContent).toContain("Used only by Coder");
expect(container.textContent).toContain("Repositories");
expect(container.textContent).toContain("1 selected repositories");
expect(container.querySelector(
@ -1489,19 +1493,9 @@ describe("AppDetail", () => {
}));
listConnectionGrantsMock.mockResolvedValue({
connection: { id: "conn-1", uid: "conn-1" },
grants: [dedicatedGitHubGrant({
providerTenant: {
github: {
userId: "123",
login: "dottabot",
installationCount: 1,
repositoryCount: 0,
repositorySelection: "all",
installationIds: ["456"],
installationOwnerLogins: ["paperclipai"],
managementUrl: "https://github.com/settings/installations/456",
},
},
grants: [dedicatedGitHubGrant({}, {
repositoryCount: 0,
repositorySelection: "all",
})],
capabilities: fullCapabilities(),
currentUserId: "user-1",
@ -1514,6 +1508,32 @@ describe("AppDetail", () => {
expect(container.textContent).not.toContain("selected repositories");
});
it.each([
["mixed", "Mixed access; scope varies by installation"],
["none", "No repositories selected"],
] as const)("labels %s GitHub repository access explicitly", async (repositorySelection, expected) => {
mockParams.tab = "permissions";
getConnectionMock.mockResolvedValue(connection({
credentialPolicy: "per_agent",
authKind: "oauth",
}));
listConnectionGrantsMock.mockResolvedValue({
connection: { id: "conn-1", uid: "conn-1" },
grants: [dedicatedGitHubGrant({}, {
repositoryCount: 0,
repositorySelection,
})],
capabilities: fullCapabilities(),
currentUserId: "user-1",
members: [],
});
await renderAppDetail();
expect(container.textContent).toContain(expected);
expect(container.textContent).not.toContain("selected repositories");
});
it("persists an empty audience as all organization members", async () => {
mockParams.tab = "permissions";
getConnectionMock.mockResolvedValue(connection({ createdByUserId: "user-1" }));

View File

@ -31,8 +31,8 @@ import {
AlertDialogTitle,
} from "@/components/ui/alert-dialog";
import { Link } from "@/lib/router";
import { brandChipBadge } from "@/lib/status-colors";
import { cn } from "@/lib/utils";
import { brandBanner, brandChipBadge } from "@/lib/status-colors";
import { agentUrl, cn } from "@/lib/utils";
import {
audienceUserIds,
grantAccountLabel,
@ -99,7 +99,7 @@ export function IdentitiesSection({
credentialPolicy: ToolConnectionCredentialPolicy;
ownerUserId: string | null;
connectedUser: { label: string; image: string | null } | null;
dedicatedAgent: { id: string; name: string } | null;
dedicatedAgent: { id: string; name: string; urlKey?: string | null } | null;
grantsQuery: ConnectionGrantsResponse | undefined;
loading: boolean;
error: boolean;
@ -181,7 +181,7 @@ export function IdentitiesSection({
status={agentGrant?.status ?? null}
detail={dedicatedAgent ? (
<Link
to={`/agents/${dedicatedAgent.id}`}
to={agentUrl(dedicatedAgent)}
className="transition-colors hover:text-foreground hover:underline"
>
Used only by {dedicatedAgent.name}
@ -296,19 +296,32 @@ function GitHubConnectionSummary({
}) {
const github = grant.providerTenant?.github;
if (!github) return null;
const allRepositories = github.repositorySelection === "all";
const repositoryWarning = github.repositorySelection === "all"
? "All current and future repositories"
: github.repositorySelection === "mixed"
? "Mixed access; scope varies by installation"
: null;
const repositorySummary = github.repositorySelection === "none"
? "No repositories selected"
: `${github.repositoryCount} selected repositories`;
return (
<div className="divide-y divide-border border-y border-border">
<div className="flex flex-wrap items-center justify-between gap-3 py-3">
<div className="min-w-0">
<div className="text-sm font-medium text-foreground">Repositories</div>
{allRepositories ? (
<div className="flex items-center gap-1.5 text-xs text-amber-800 dark:text-amber-200">
<TriangleAlert className="h-3.5 w-3.5 shrink-0" />
All current and future repositories
{repositoryWarning ? (
<div
role="note"
className={cn(
"mt-1 inline-flex items-center gap-1.5 rounded-md border px-2 py-1 text-xs",
brandBanner.warning,
)}
>
<TriangleAlert className="h-3.5 w-3.5 shrink-0" aria-hidden="true" />
{repositoryWarning}
</div>
) : (
<div className="text-xs text-muted-foreground">{github.repositoryCount} selected repositories</div>
<div className="text-xs text-muted-foreground">{repositorySummary}</div>
)}
</div>
{github.managementUrl ? (