Show host ownership for CLI provider accounts

Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
Jinwoo-H
2026-06-12 20:34:52 -07:00
co-authored by Orca
parent e40496d808
commit ea127e0924
3 changed files with 142 additions and 12 deletions
@@ -219,6 +219,7 @@ Done means: reviewers and future agents can understand the vision, implementatio
- [x] 2026-06-12: Added a narrow `.gitignore` rule for `notes/artifacts/` so verification screenshots stay local while the HTML report can reference them; verified with `git check-ignore`.
- [x] 2026-06-12: Added `taskSourceContext` and `workspaceRunContext` to retryable pending worktree creation requests, seeded them from the Composer's selected task source and project-host setup, and verified the pending store preserves both identities with focused worktree tests plus web typecheck.
- [x] 2026-06-12: Threaded GitHub Tasks row-level mutations through `TaskSourceContext` for issue state, assignees, reviewer requests/removals, PR merge/auto-merge, and new issue creation/follow-up reads; verified with focused GitHub/TaskPage tests, focused oxlint, `pnpm run typecheck`, and `git diff --check`.
- [x] 2026-06-12: Added provider account-scope diagnostics to GitHub/GitLab CLI integration cards so code-host credential checks show whether they belong to the local desktop client or selected remote server, matching the existing Linear/Jira ownership copy; verified with focused integration-card scope tests and oxlint.
- [x] 2026-06-12: Updated PR #5071 with the current verified scope, evidence docs, validation commands, explicit out-of-scope cloud/onboarding items, and remaining provider/version-skew risk.
- [x] 2026-06-12: Added load-time backfill for legacy automations and automation runs that predate `runContext`/`sourceContext`, deriving explicit project host and provider source contexts from the saved repo id/path; verified with focused persistence migration coverage, automation service/precheck tests, daemon/settings merge-repair tests, and full `pnpm run typecheck`.
- [x] 2026-06-12: Repaired the merge-repair lint gate without disabling max-lines by extracting automation run-target/usage modules and settings subcomponents, fixed stale source-context hook dependencies, and tightened GitHub PR checks cache ownership so `noCache` bypasses cache/in-flight requests while head-specific checks are reused during refresh events; verified targeted oxlint, focused automation/settings/GitHub cache tests (10 files / 157 tests), `github-checks-cache.test.ts`, `git diff --check`, and full `pnpm run typecheck`.
@@ -0,0 +1,102 @@
// @vitest-environment happy-dom
import { act } from 'react'
import { createRoot, type Root } from 'react-dom/client'
import { afterEach, describe, expect, it, vi } from 'vitest'
import {
GitHubIntegrationCard,
GitLabIntegrationCard
} from './cli-source-control-integration-cards'
type StoreState = {
settings: { activeRuntimeEnvironmentId: string | null }
}
const mocks = vi.hoisted(() => ({
store: { current: null as StoreState | null },
preflight: {
statuses: {
ghStatus: 'connected',
glabStatus: 'connected',
bitbucketStatus: 'not-configured',
azureDevOpsStatus: 'not-configured',
giteaStatus: 'not-configured',
bitbucketAccount: null,
azureDevOpsAccount: null,
giteaAccount: null
},
unavailable: false,
refresh: vi.fn()
}
}))
vi.mock('@/store', () => ({
useAppStore: (selector: (state: StoreState) => unknown) => {
if (!mocks.store.current) {
throw new Error('Store state was not installed')
}
return selector(mocks.store.current)
}
}))
vi.mock('./source-control-preflight-card-status', () => ({
usePreflightCardStatuses: () => mocks.preflight
}))
let root: Root | null = null
let container: HTMLDivElement | null = null
async function renderCard(card: React.ReactNode): Promise<HTMLDivElement> {
container = document.createElement('div')
document.body.appendChild(container)
root = createRoot(container)
await act(async () => {
root?.render(card)
})
return container
}
describe('CLI source-control integration card account scope', () => {
afterEach(async () => {
if (root) {
await act(async () => {
root?.unmount()
})
}
root = null
container?.remove()
container = null
mocks.store.current = null
mocks.preflight.statuses.ghStatus = 'connected'
mocks.preflight.statuses.glabStatus = 'connected'
mocks.preflight.unavailable = false
mocks.preflight.refresh.mockClear()
})
it('shows local-client ownership for connected GitHub CLI credentials', async () => {
mocks.store.current = { settings: { activeRuntimeEnvironmentId: null } }
const rendered = await renderCard(<GitHubIntegrationCard />)
expect(rendered.textContent).toContain('GitHub')
expect(rendered.textContent).toContain('Connected')
expect(rendered.textContent).toContain('Account scope: Local Mac')
expect(rendered.textContent).toContain(
'Credentials and account checks for this provider are owned by this desktop client.'
)
})
it('shows remote-server ownership for GitLab CLI credential checks', async () => {
mocks.store.current = { settings: { activeRuntimeEnvironmentId: 'runtime-1' } }
mocks.preflight.statuses.glabStatus = 'not-authenticated'
const rendered = await renderCard(<GitLabIntegrationCard />)
expect(rendered.textContent).toContain('GitLab')
expect(rendered.textContent).toContain('Account scope: Remote server: runtime-1')
expect(rendered.textContent).toContain(
'Credentials and account checks for this provider are owned by this remote server.'
)
expect(rendered.textContent).toContain('glab auth login')
})
})
@@ -1,9 +1,36 @@
import { ExternalLink, Github, Gitlab, Terminal } from 'lucide-react'
import { Button } from '@/components/ui/button'
import { useAppStore } from '@/store'
import { IntegrationCardDetails, IntegrationCardShell } from './integration-card-shell'
import { getProviderAccountScope } from './provider-account-scope'
import { usePreflightCardStatuses } from './source-control-preflight-card-status'
import { translate } from '@/i18n/i18n'
function ProviderAccountScopeDetails({
children
}: {
children?: React.ReactNode
}): React.JSX.Element {
const settings = useAppStore((s) => s.settings)
const accountScope = getProviderAccountScope(settings)
return (
<IntegrationCardDetails>
<div className="text-xs">
<span className="font-medium text-foreground">
{translate(
'auto.components.settings.cli.source.control.integration.cards.account_scope',
'Account scope: {{value0}}',
{ value0: accountScope.label }
)}
</span>
<div className="mt-0.5 text-muted-foreground">{accountScope.description}</div>
</div>
{children}
</IntegrationCardDetails>
)
}
export function GitHubIntegrationCard(): React.JSX.Element {
const { statuses, unavailable, refresh } = usePreflightCardStatuses('gh')
const status = unavailable ? 'unavailable' : statuses.ghStatus
@@ -43,9 +70,9 @@ export function GitHubIntegrationCard(): React.JSX.Element {
: 'Not authenticated'
}
>
{status !== 'checking' && !connected ? (
<IntegrationCardDetails>
{status === 'unavailable' ? (
<ProviderAccountScopeDetails>
{status !== 'checking' && !connected ? (
status === 'unavailable' ? (
<>
<p className="text-xs text-muted-foreground">
{translate(
@@ -125,9 +152,9 @@ export function GitHubIntegrationCard(): React.JSX.Element {
</Button>
</div>
</>
)}
</IntegrationCardDetails>
) : null}
)
) : null}
</ProviderAccountScopeDetails>
</IntegrationCardShell>
)
}
@@ -171,9 +198,9 @@ export function GitLabIntegrationCard(): React.JSX.Element {
: 'Not authenticated'
}
>
{status !== 'checking' && !connected ? (
<IntegrationCardDetails>
{status === 'unavailable' ? (
<ProviderAccountScopeDetails>
{status !== 'checking' && !connected ? (
status === 'unavailable' ? (
<>
<p className="text-xs text-muted-foreground">
{translate(
@@ -257,9 +284,9 @@ export function GitLabIntegrationCard(): React.JSX.Element {
</Button>
</div>
</>
)}
</IntegrationCardDetails>
) : null}
)
) : null}
</ProviderAccountScopeDetails>
</IntegrationCardShell>
)
}