-
Notifications
You must be signed in to change notification settings - Fork 22
fix(capabilities): report the resolved Composio credential tier, not the BYO slot (#886) #897
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 9 commits
8cffe02
18e881d
f12a03a
5c0b109
079d846
67f632b
4f22b6c
99b885c
d691614
046d1e0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -304,7 +304,7 @@ const NAMESPACE_LABELS: Record<string, string> = { | |
| }; | ||
|
|
||
| // Badge variant subset the media status row uses. | ||
| type BadgeVariant = "default" | "secondary" | "destructive" | "outline"; | ||
| export type BadgeVariant = "default" | "secondary" | "destructive" | "outline"; | ||
|
|
||
| /** | ||
| * The media-generation capability (issue #109) is opt-in per tool grant and | ||
|
|
@@ -341,9 +341,8 @@ function mediaStatus(caps: CapabilityStatusDto): { label: string; variant: Badge | |
|
|
||
| /** | ||
| * The Composio capability (issue #110) is opt-in per tool grant and gated on a | ||
| * per-tenant OAuth token, so it gets its own status row like media. Four states: | ||
| * not compiled into this build, not granted, granted-but-awaiting-token, and | ||
| * active. Set the token from Connections. | ||
| * resolved credential, so it gets its own status row like media. Five states — | ||
| * see {@link composioStatus}. | ||
| */ | ||
| function ComposioStatusRow({ caps }: { caps: CapabilityStatusDto }) { | ||
| const { label, variant } = composioStatus(caps); | ||
|
|
@@ -352,8 +351,10 @@ function ComposioStatusRow({ caps }: { caps: CapabilityStatusDto }) { | |
| <div className="space-y-0.5"> | ||
| <span className="font-medium">Composio integrations</span> | ||
| <p className="text-xs text-muted-foreground"> | ||
| Gmail, Slack & GitHub via Composio — opt-in, runs on the company's own OAuth | ||
| token, and every send/authorize is approved before it runs. Set the token in Connections. | ||
| Gmail, Slack & GitHub via Composio — opt-in, and every send/authorize is approved | ||
| before it runs. Runs on this company's own Composio token when one is set in | ||
| Connections; otherwise on the company's TinyHumans key, or on the platform identity | ||
| this instance already carries. | ||
| </p> | ||
| </div> | ||
| <Badge variant={variant} className="shrink-0"> | ||
|
|
@@ -363,11 +364,31 @@ function ComposioStatusRow({ caps }: { caps: CapabilityStatusDto }) { | |
| ); | ||
| } | ||
|
|
||
| function composioStatus(caps: CapabilityStatusDto): { label: string; variant: BadgeVariant } { | ||
| /** | ||
| * The Composio row's five states, in order (issue #886). | ||
| * | ||
| * The credential is resolved over three tiers — a BYO Composio token, the | ||
| * company's TinyHumans key, this instance's platform identity — so "is a token | ||
| * stored" is the wrong question to render. This reads `composioCredentialSource`, | ||
| * the tier the host says the toolbelt actually resolves. | ||
| * | ||
| * The `undefined` rung is load-bearing and must stay above the `"none"` rung. | ||
| * `undefined` means the host did not answer — an older build that does not send | ||
| * the field, or one whose secret store could not be read — and falling through | ||
| * it into the destructive branch is exactly the bug #886 was filed about: a red | ||
| * "no credential" badge over a Composio account that is working. Unknown is | ||
| * shown as unknown, and never in the alarm colour. | ||
| */ | ||
| export function composioStatus(caps: CapabilityStatusDto): { | ||
| label: string; | ||
| variant: BadgeVariant; | ||
| } { | ||
| if (caps.composioInBuild === false) return { label: "Not in this build", variant: "outline" }; | ||
| if (!caps.composioGranted) return { label: "Not granted", variant: "secondary" }; | ||
| if (!caps.composioTokenConfigured) | ||
| return { label: "Awaiting token", variant: "destructive" }; | ||
| if (caps.composioCredentialSource === undefined) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add tests for composioStatus credential source states The repository requires focused tests with every behavior change. This function now branches on [RULE] missing-tests ·
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. A test for exactly this was added in this PR: It covers the five-state matrix, and pins the specific rung this finding is right to care about — |
||
| return { label: "Couldn't check", variant: "outline" }; | ||
| if (caps.composioCredentialSource === "none") | ||
| return { label: "Awaiting credential", variant: "destructive" }; | ||
| return { label: "Active", variant: "default" }; | ||
| } | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,91 @@ | ||
| /** | ||
| * Issue #886 — the Composio row on the Usage view must never paint a working | ||
| * connector red. | ||
| * | ||
| * The credential resolves over three tiers (a BYO Composio token, the company's | ||
| * TinyHumans key, this instance's platform identity). The row used to read | ||
| * `composioTokenConfigured`, which answers only the first, so a hosted tenant | ||
| * running on the platform identity got "Awaiting token" in the alarm colour | ||
| * while its agents were calling `GITHUB_*` tools successfully. | ||
| * | ||
| * The state that matters most here is the one with no obvious label: the host | ||
| * not answering. It is a separate rung above `"none"` precisely so it cannot | ||
| * fall through into the destructive branch and re-create the bug. | ||
| */ | ||
| import { describe, expect, it } from "vitest"; | ||
|
|
||
| import type { CapabilityStatusDto } from "@/api/types"; | ||
| import { composioStatus } from "@/views/UsageView"; | ||
|
|
||
| /** A granted, in-build company — the only shape the credential rungs are reached from. */ | ||
| function granted(over: Partial<CapabilityStatusDto> = {}): CapabilityStatusDto { | ||
| return { | ||
| configured: false, | ||
| composioInBuild: true, | ||
| composioGranted: true, | ||
| ...over, | ||
| }; | ||
| } | ||
|
|
||
| describe("composioStatus", () => { | ||
| it("reports a build without the feature before anything else", () => { | ||
| expect( | ||
| composioStatus(granted({ composioInBuild: false, composioCredentialSource: "attested" })), | ||
| ).toEqual({ label: "Not in this build", variant: "outline" }); | ||
| }); | ||
|
|
||
| it("reports an ungranted company without consulting the credential", () => { | ||
| expect( | ||
| composioStatus(granted({ composioGranted: false, composioCredentialSource: "attested" })), | ||
| ).toEqual({ label: "Not granted", variant: "secondary" }); | ||
| }); | ||
|
|
||
| /** | ||
| * The #886 regression guard. An unanswered host is unknown, not broken — | ||
| * and specifically not `destructive`, which is the colour that sent the | ||
| * original debugging in the wrong direction. | ||
| */ | ||
| it("reports an unanswered host as unknown, never as an alarm", () => { | ||
| const status = composioStatus(granted({ composioCredentialSource: undefined })); | ||
| expect(status.label).toBe("Couldn't check"); | ||
| expect(status.variant).not.toBe("destructive"); | ||
| }); | ||
|
|
||
| it("reports a genuinely unresolvable credential as the destructive state", () => { | ||
| expect(composioStatus(granted({ composioCredentialSource: "none" }))).toEqual({ | ||
| label: "Awaiting credential", | ||
| variant: "destructive", | ||
| }); | ||
| }); | ||
|
|
||
| /** | ||
| * All three resolving tiers are Active. `attested` is the hosted shape the | ||
| * issue was reported against, and it is the one the old code got wrong: | ||
| * nothing is stored on the instance, so `composioTokenConfigured` is `false` | ||
| * while the toolbelt is fully wired. | ||
| */ | ||
| it.each(["attested", "company", "static"] as const)( | ||
| "reports a resolved `%s` credential as active", | ||
| (source) => { | ||
| expect( | ||
| composioStatus(granted({ composioCredentialSource: source, composioTokenConfigured: false })), | ||
| ).toEqual({ label: "Active", variant: "default" }); | ||
| }, | ||
| ); | ||
|
|
||
| /** | ||
| * The narrow legacy field must not be able to steer the verdict in either | ||
| * direction: it answers "did somebody paste a BYO token", which is a | ||
| * different question from "does a credential resolve". | ||
| */ | ||
| it("ignores the BYO-token flag once the resolver has answered", () => { | ||
| expect( | ||
| composioStatus(granted({ composioTokenConfigured: true, composioCredentialSource: "none" })) | ||
| .variant, | ||
| ).toBe("destructive"); | ||
| expect( | ||
| composioStatus(granted({ composioTokenConfigured: false, composioCredentialSource: "attested" })) | ||
| .label, | ||
| ).toBe("Active"); | ||
| }); | ||
| }); |
Uh oh!
There was an error while loading. Please reload this page.