mirror of
https://github.com/stablyai/orca.git
synced 2026-10-19 00:03:20 +00:00
Preserve host identity on PR/issue work items throughout the mutation and diff pipeline so Enterprise instances (including ported endpoints like github.acme.test:8443) can execute mutations without ambiguity. Rate-limit gh commands by the pre-qualified --repo host, cache auth state per ported host, and surface Enterprise hosts in project metadata and error messages.
161 lines
5.3 KiB
TypeScript
161 lines
5.3 KiB
TypeScript
import { describe, expect, it, vi } from 'vitest'
|
|
|
|
const { ghExecFileAsyncMock } = vi.hoisted(() => ({ ghExecFileAsyncMock: vi.fn() }))
|
|
|
|
vi.mock('../git/runner', () => ({
|
|
ghExecFileAsync: ghExecFileAsyncMock
|
|
}))
|
|
import { diagnoseGhAuth, parseAuthStatus } from './auth-diagnose'
|
|
|
|
describe('parseAuthStatus', () => {
|
|
it('parses an env-shadowed login alongside a keyring login (real gh output)', () => {
|
|
const text = `github.com
|
|
✓ Logged in to github.com account nwparker (GITHUB_TOKEN)
|
|
- Active account: true
|
|
- Git operations protocol: https
|
|
- Token: gho_************************************
|
|
- Token scopes: 'gist', 'read:org', 'repo', 'workflow'
|
|
|
|
✓ Logged in to github.com account nwparker (keyring)
|
|
- Active account: false
|
|
- Git operations protocol: https
|
|
- Token: gho_************************************
|
|
- Token scopes: 'gist', 'read:org', 'repo', 'workflow'
|
|
`
|
|
const accounts = parseAuthStatus(text)
|
|
expect(accounts).toHaveLength(2)
|
|
expect(accounts[0]).toMatchObject({
|
|
host: 'github.com',
|
|
user: 'nwparker',
|
|
active: true,
|
|
envToken: 'GITHUB_TOKEN',
|
|
source: 'env'
|
|
})
|
|
expect(accounts[0].scopes).toEqual(['gist', 'read:org', 'repo', 'workflow'])
|
|
expect(accounts[1]).toMatchObject({
|
|
active: false,
|
|
envToken: null,
|
|
source: 'keyring'
|
|
})
|
|
})
|
|
|
|
it('parses a single keyring login', () => {
|
|
const text = `github.com
|
|
✓ Logged in to github.com account alice (keyring)
|
|
- Active account: true
|
|
- Token scopes: 'project', 'read:org', 'repo'
|
|
`
|
|
const accounts = parseAuthStatus(text)
|
|
expect(accounts).toHaveLength(1)
|
|
expect(accounts[0]).toMatchObject({
|
|
user: 'alice',
|
|
active: true,
|
|
envToken: null,
|
|
source: 'keyring'
|
|
})
|
|
expect(accounts[0].scopes).toContain('project')
|
|
})
|
|
|
|
it('detects GH_TOKEN env source', () => {
|
|
const text = `github.com
|
|
✓ Logged in to github.com account bot (GH_TOKEN)
|
|
- Active account: true
|
|
- Token scopes: 'repo'
|
|
`
|
|
const [acc] = parseAuthStatus(text)
|
|
expect(acc.envToken).toBe('GH_TOKEN')
|
|
expect(acc.source).toBe('env')
|
|
})
|
|
|
|
it('returns empty array when nothing is logged in', () => {
|
|
expect(parseAuthStatus('You are not logged into any GitHub hosts.')).toEqual([])
|
|
})
|
|
|
|
it('parses multiple hosts in one output (github.com + GHES)', () => {
|
|
const text = `github.com
|
|
✓ Logged in to github.com account alice (keyring)
|
|
- Active account: true
|
|
- Token scopes: 'read:org', 'repo'
|
|
|
|
ghe.acme.io
|
|
✓ Logged in to ghe.acme.io account bob (keyring)
|
|
- Active account: true
|
|
- Token scopes: 'project', 'repo'
|
|
`
|
|
const accounts = parseAuthStatus(text)
|
|
expect(accounts.map((a) => a.host)).toEqual(['github.com', 'ghe.acme.io'])
|
|
expect(accounts.map((a) => a.user)).toEqual(['alice', 'bob'])
|
|
expect(accounts[1].scopes).toContain('project')
|
|
})
|
|
|
|
it('parses a ported GHES auth-status section', () => {
|
|
const text = `ghe.acme.io:8443
|
|
✓ Logged in to ghe.acme.io:8443 account bob (keyring)
|
|
- Active account: true
|
|
- Token scopes: 'project', 'repo'
|
|
`
|
|
|
|
expect(parseAuthStatus(text)).toEqual([
|
|
expect.objectContaining({ host: 'ghe.acme.io:8443', user: 'bob', active: true })
|
|
])
|
|
})
|
|
|
|
it('recovers host from the Logged-in line when the section header is missing', () => {
|
|
// gh prints a colon after the host on some versions; we tolerate it,
|
|
// but if the regex ever fails to match the header we still want
|
|
// accounts attributed to the host from the inline message.
|
|
const text = ` ✓ Logged in to github.acme.io account carol (keyring)
|
|
- Active account: true
|
|
- Token scopes: 'project'
|
|
`
|
|
const accounts = parseAuthStatus(text)
|
|
expect(accounts).toHaveLength(1)
|
|
expect(accounts[0].host).toBe('github.acme.io')
|
|
})
|
|
})
|
|
|
|
describe('diagnoseGhAuth host scoping', () => {
|
|
const TWO_HOST_STATUS = `github.com
|
|
✓ Logged in to github.com account alice (keyring)
|
|
- Active account: true
|
|
- Token scopes: 'project', 'read:org', 'repo'
|
|
ghe.acme.io
|
|
✓ Logged in to ghe.acme.io account bob (keyring)
|
|
- Active account: true
|
|
- Token scopes: 'repo'
|
|
`
|
|
|
|
it('reports an unauthenticated required host and keeps scope advice host-scoped', async () => {
|
|
ghExecFileAsyncMock.mockResolvedValue({ stdout: TWO_HOST_STATUS, stderr: '' })
|
|
|
|
const diag = await diagnoseGhAuth('github.other-corp.com')
|
|
|
|
expect(diag.requiredHost).toBe('github.other-corp.com')
|
|
expect(diag.requiredHostAuthenticated).toBe(false)
|
|
expect(diag.activeAccount).toBeNull()
|
|
})
|
|
|
|
it("scopes the diagnosis to the required host's account", async () => {
|
|
ghExecFileAsyncMock.mockResolvedValue({ stdout: TWO_HOST_STATUS, stderr: '' })
|
|
|
|
const diag = await diagnoseGhAuth('GHE.Acme.IO')
|
|
|
|
expect(diag.requiredHost).toBe('ghe.acme.io')
|
|
expect(diag.requiredHostAuthenticated).toBe(true)
|
|
expect(diag.activeAccount?.user).toBe('bob')
|
|
// Why: bob's GHES token lacks project/read:org even though alice's
|
|
// github.com token has them — advice must reflect the required host.
|
|
expect(diag.missingScopes).toEqual(['project', 'read:org'])
|
|
})
|
|
|
|
it('keeps legacy host-less behavior byte-identical', async () => {
|
|
ghExecFileAsyncMock.mockResolvedValue({ stdout: TWO_HOST_STATUS, stderr: '' })
|
|
|
|
const diag = await diagnoseGhAuth()
|
|
|
|
expect(diag.requiredHost).toBeNull()
|
|
expect(diag.requiredHostAuthenticated).toBeNull()
|
|
expect(diag.activeAccount?.user).toBe('alice')
|
|
})
|
|
})
|