Files
orca/src/main/github/github-repository-identity.ssh-host-alias.test.ts
T
Brennan Benson 8e9b5c908c fix(github): fail closed instead of running client git against a remote repoPath when the SSH provider is unregistered (#14945)
* fix(github): fail closed when the SSH git provider is gone

getCurrentHeadOid and probeTrackedUpstreamBranches only routed through the
SSH provider when one was registered. With connectionId set but the provider
unregistered (dropped connection, not yet reattached) they fell through to
client-side git with cwd pointing at the remote repoPath — on a machine with
a same-named local path that silently answers for the wrong repository.

getCurrentHeadOid feeds shouldHideMergedImplicitPR, so a wrong OID changes
which PR the UI attributes to a worktree.

Both now take their existing unknown path (null / probeFailed) instead,
matching repo-default-branch.ts. Local and WSL routing is unchanged.

* fix(github): preserve PR state when SSH probes fail

* fix(github): keep failed SSH discovery unverifiable

* fix(github): propagate SSH identity failures

* fix(github): scope verified SSH identity probes

* test(github): preserve tolerant resolver calls

* fix(github): preserve indeterminate auth discovery

* fix(github): isolate SSH repository probe generations

* test(github): expose SSH probe generation in mocks
2026-08-17 00:12:14 -07:00

424 lines
14 KiB
TypeScript

import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import type * as GitRunner from '../git/runner'
const {
commandExecFileAsyncMock,
getSshGitProviderGenerationMock,
getSshGitProviderMock,
gitExecFileAsyncMock,
resolveWithSshGMock,
readLocalGitConfigSignatureMock
} = vi.hoisted(() => ({
commandExecFileAsyncMock: vi.fn(),
getSshGitProviderGenerationMock: vi.fn(() => 0),
getSshGitProviderMock: vi.fn(),
gitExecFileAsyncMock: vi.fn(),
resolveWithSshGMock: vi.fn(),
readLocalGitConfigSignatureMock: vi.fn(async () => 'sig-10284')
}))
vi.mock('../git/runner', async (importOriginal) => ({
...(await importOriginal<typeof GitRunner>()),
commandExecFileAsync: commandExecFileAsyncMock,
gitExecFileAsync: gitExecFileAsyncMock
}))
vi.mock('../providers/ssh-git-dispatch', () => ({
getSshGitProvider: getSshGitProviderMock,
getSshGitProviderGeneration: getSshGitProviderGenerationMock,
SSH_GIT_PROVIDER_UNAVAILABLE_MESSAGE: 'Remote connection dropped.'
}))
vi.mock('./local-git-config-signature', () => ({
readLocalGitConfigSignature: readLocalGitConfigSignatureMock
}))
vi.mock('../ssh/ssh-g-config-resolution', () => ({
resolveWithSshG: resolveWithSshGMock
}))
import {
getOwnerRepoForRemote,
_resetOwnerRepoCache,
_getOwnerRepoCacheSize
} from './github-repository-identity'
import {
classifyGitHubOwnerRepoFromRemoteUrl,
resolveGitHubOwnerRepoFromRemoteUrl,
_resetSshHostnameResolutionCache
} from './github-ssh-host-alias-resolution'
const REPO = '/tmp/ssh-alias-checkout'
function sshConfig(hostname: string, port = 22) {
return {
hostname,
port,
identityFile: [],
identitiesOnly: false,
forwardAgent: false,
proxyUseFdpass: false,
controlMaster: 'no',
controlPersist: 'no'
}
}
function mockRemoteUrl(url: string): void {
gitExecFileAsyncMock.mockImplementation(async (args: string[]) => {
if (args[0] === 'remote' && args[1] === 'get-url') {
return { stdout: `${url}\n` }
}
throw new Error(`unexpected git args: ${args.join(' ')}`)
})
}
function sshProvider(hostname: string, remoteUrl = 'git@github-work:team/orca.git') {
return {
exec: vi.fn().mockResolvedValue({
stdout: `${remoteUrl}\n`,
stderr: ''
}),
execNonInteractive: vi.fn().mockResolvedValue({
stdout: `hostname ${hostname}\nport 22\n`,
stderr: '',
exitCode: 0,
timedOut: false,
canceled: false
})
}
}
beforeEach(() => {
_resetOwnerRepoCache()
_resetSshHostnameResolutionCache()
commandExecFileAsyncMock.mockReset()
getSshGitProviderGenerationMock.mockReset()
getSshGitProviderGenerationMock.mockReturnValue(0)
getSshGitProviderMock.mockReset()
gitExecFileAsyncMock.mockReset()
resolveWithSshGMock.mockReset()
readLocalGitConfigSignatureMock.mockClear()
})
afterEach(() => {
vi.useRealTimers()
})
describe('#10284 SSH Host alias → github.com owner/repo', () => {
it('resolveGitHubOwnerRepoFromRemoteUrl expands HostName ssh.github.com', async () => {
resolveWithSshGMock.mockResolvedValueOnce(sshConfig('ssh.github.com', 443))
await expect(
resolveGitHubOwnerRepoFromRemoteUrl('git@github-work:team/orca.git')
).resolves.toEqual({ owner: 'team', repo: 'orca' })
expect(resolveWithSshGMock).toHaveBeenCalledWith('github-work')
})
it('getOwnerRepoForRemote resolves SCP alias remote used for multi-account GitHub', async () => {
mockRemoteUrl('git@github-work:team/orca.git')
resolveWithSshGMock.mockResolvedValueOnce(sshConfig('github.com'))
await expect(getOwnerRepoForRemote(REPO, 'origin')).resolves.toEqual({
owner: 'team',
repo: 'orca'
})
expect(resolveWithSshGMock).toHaveBeenCalledWith('github-work')
})
it('getOwnerRepoForRemote resolves ssh:// Host alias remotes', async () => {
mockRemoteUrl('ssh://git@github.com-work/acme/widgets.git')
resolveWithSshGMock.mockResolvedValueOnce(sshConfig('ssh.github.com', 443))
await expect(getOwnerRepoForRemote(REPO, 'origin')).resolves.toEqual({
owner: 'acme',
repo: 'widgets'
})
expect(resolveWithSshGMock).toHaveBeenCalledWith('github.com-work')
})
it('resolves aliases inside the repository WSL runtime', async () => {
mockRemoteUrl('git@github-work:team/orca.git')
commandExecFileAsyncMock.mockResolvedValueOnce({
stdout: 'hostname github.com\nport 22\n',
stderr: ''
})
await expect(
getOwnerRepoForRemote(REPO, 'origin', null, { wslDistro: 'Ubuntu' })
).resolves.toEqual({
owner: 'team',
repo: 'orca'
})
expect(commandExecFileAsyncMock).toHaveBeenCalledWith('ssh', ['-G', '--', 'github-work'], {
cwd: REPO,
timeout: 5_000,
wslDistro: 'Ubuntu'
})
expect(resolveWithSshGMock).not.toHaveBeenCalled()
})
it('resolves aliases inside the repository SSH runtime', async () => {
const provider = sshProvider('github.com')
getSshGitProviderMock.mockReturnValue(provider)
getSshGitProviderGenerationMock.mockReturnValue(4)
await expect(getOwnerRepoForRemote('/remote/repo', 'origin', 'ssh-1')).resolves.toEqual({
owner: 'team',
repo: 'orca'
})
expect(provider.execNonInteractive).toHaveBeenCalledWith(
'ssh',
['-G', '--', 'github-work'],
'/remote/repo',
5_000
)
expect(resolveWithSshGMock).not.toHaveBeenCalled()
})
it('does not read an SSH repoPath locally when its provider is missing', async () => {
getSshGitProviderMock.mockReturnValue(undefined)
await expect(
getOwnerRepoForRemote(
'/remote/repo',
'origin',
'ssh-1',
{},
{
requireVerifiedSshProbe: true
}
)
).rejects.toThrow('Remote connection dropped.')
expect(gitExecFileAsyncMock).not.toHaveBeenCalled()
})
it('preserves a one-shot SSH remote failure through candidate discovery', async () => {
const failure = new Error('relay request failed')
const provider = {
exec: vi
.fn()
.mockRejectedValueOnce(failure)
.mockResolvedValueOnce({ stdout: 'git@github.com:team/orca.git\n', stderr: '' })
}
getSshGitProviderMock.mockReturnValue(provider)
await expect(
getOwnerRepoForRemote(
'/remote/repo',
'origin',
'ssh-1',
{},
{
requireVerifiedSshProbe: true
}
)
).rejects.toBe(failure)
await expect(getOwnerRepoForRemote('/remote/repo', 'origin', 'ssh-1')).resolves.toEqual({
owner: 'team',
repo: 'orca'
})
expect(provider.exec).toHaveBeenCalledTimes(2)
expect(gitExecFileAsyncMock).not.toHaveBeenCalled()
})
it('keeps failed SSH alias classification unverifiable', async () => {
const provider = sshProvider('')
getSshGitProviderMock.mockReturnValue(provider)
await expect(
getOwnerRepoForRemote(
'/remote/repo',
'origin',
'ssh-1',
{},
{
requireVerifiedSshProbe: true
}
)
).rejects.toThrow('Remote repository identity is unverifiable.')
expect(gitExecFileAsyncMock).not.toHaveBeenCalled()
})
it('isolates the same alias across native and WSL runtimes', async () => {
resolveWithSshGMock.mockResolvedValueOnce(sshConfig('github.com'))
commandExecFileAsyncMock.mockResolvedValueOnce({
stdout: 'hostname gitlab.com\nport 22\n',
stderr: ''
})
await expect(
resolveGitHubOwnerRepoFromRemoteUrl('git@forge-work:team/orca.git')
).resolves.toEqual({ owner: 'team', repo: 'orca' })
await expect(
resolveGitHubOwnerRepoFromRemoteUrl('git@forge-work:team/orca.git', {
repoPath: REPO,
wslDistro: 'Ubuntu'
})
).resolves.toBeNull()
expect(resolveWithSshGMock).toHaveBeenCalledTimes(1)
expect(commandExecFileAsyncMock).toHaveBeenCalledTimes(1)
})
it('invalidates alias resolution when an SSH provider reconnects', async () => {
const context = { repoPath: '/remote/repo', connectionId: 'ssh-1' }
getSshGitProviderGenerationMock.mockReturnValue(1)
getSshGitProviderMock.mockReturnValue(sshProvider('github.com'))
await expect(
resolveGitHubOwnerRepoFromRemoteUrl('git@forge-work:team/orca.git', context)
).resolves.toEqual({ owner: 'team', repo: 'orca' })
getSshGitProviderGenerationMock.mockReturnValue(2)
getSshGitProviderMock.mockReturnValue(sshProvider('gitlab.com'))
await expect(
resolveGitHubOwnerRepoFromRemoteUrl('git@forge-work:team/orca.git', context)
).resolves.toBeNull()
})
it('invalidates owner/repo identity when an SSH provider reconnects', async () => {
getSshGitProviderGenerationMock.mockReturnValue(1)
getSshGitProviderMock.mockReturnValue(
sshProvider('github.com', 'git@github-work:team/orca.git')
)
await expect(getOwnerRepoForRemote('/remote/repo', 'origin', 'ssh-1')).resolves.toEqual({
owner: 'team',
repo: 'orca'
})
getSshGitProviderGenerationMock.mockReturnValue(2)
getSshGitProviderMock.mockReturnValue(
sshProvider('github.com', 'git@github-work:acme/widgets.git')
)
await expect(getOwnerRepoForRemote('/remote/repo', 'origin', 'ssh-1')).resolves.toEqual({
owner: 'acme',
repo: 'widgets'
})
})
it('does not call ssh -G for literal github.com remotes', async () => {
mockRemoteUrl('git@github.com:team/orca.git')
await expect(getOwnerRepoForRemote(REPO, 'origin')).resolves.toEqual({
owner: 'team',
repo: 'orca'
})
expect(resolveWithSshGMock).not.toHaveBeenCalled()
})
it('does not call ssh -G for https remotes', async () => {
mockRemoteUrl('https://github.com/team/orca.git')
await expect(getOwnerRepoForRemote(REPO, 'origin')).resolves.toEqual({
owner: 'team',
repo: 'orca'
})
expect(resolveWithSshGMock).not.toHaveBeenCalled()
})
it('returns null when alias resolves to a non-GitHub host', async () => {
mockRemoteUrl('git@gitlab-work:team/orca.git')
resolveWithSshGMock.mockResolvedValueOnce(sshConfig('gitlab.com'))
await expect(getOwnerRepoForRemote(REPO, 'origin')).resolves.toBeNull()
})
it('returns null when ssh -G fails for an alias', async () => {
mockRemoteUrl('git@github-work:team/orca.git')
resolveWithSshGMock.mockResolvedValueOnce(null)
await expect(getOwnerRepoForRemote(REPO, 'origin')).resolves.toBeNull()
})
it('does not rewrite transport: identity resolution only consumes HostName', async () => {
const remote = 'git@github-work:team/orca.git'
mockRemoteUrl(remote)
resolveWithSshGMock.mockResolvedValueOnce(sshConfig('github.com'))
await getOwnerRepoForRemote(REPO, 'origin')
expect(gitExecFileAsyncMock).toHaveBeenCalledWith(
['remote', 'get-url', 'origin'],
expect.objectContaining({ cwd: REPO })
)
const gitArgLists = gitExecFileAsyncMock.mock.calls.map(([args]) => args.join(' '))
expect(gitArgLists.every((cmd) => cmd.startsWith('remote get-url'))).toBe(true)
})
it('classifies ssh -G failure as indeterminate (not stable not-github)', async () => {
resolveWithSshGMock.mockResolvedValueOnce(null)
await expect(
classifyGitHubOwnerRepoFromRemoteUrl('git@github-work:team/orca.git')
).resolves.toEqual({ kind: 'indeterminate' })
})
it('does not long-negative-cache owner/repo when ssh -G is indeterminate', async () => {
mockRemoteUrl('git@github-work:team/orca.git')
resolveWithSshGMock.mockResolvedValue(null)
await expect(getOwnerRepoForRemote(REPO, 'origin')).resolves.toBeNull()
expect(_getOwnerRepoCacheSize()).toBe(0)
_resetSshHostnameResolutionCache()
await expect(getOwnerRepoForRemote(REPO, 'origin')).resolves.toBeNull()
expect(resolveWithSshGMock).toHaveBeenCalledTimes(2)
expect(_getOwnerRepoCacheSize()).toBe(0)
})
it('does not pin an SSH-config-dependent miss to the Git config signature', async () => {
vi.useFakeTimers()
mockRemoteUrl('git@forge-work:team/orca.git')
resolveWithSshGMock
.mockResolvedValueOnce(sshConfig('gitlab.com'))
.mockResolvedValueOnce(sshConfig('github.com'))
await expect(getOwnerRepoForRemote(REPO, 'origin')).resolves.toBeNull()
await vi.advanceTimersByTimeAsync(60_001)
await expect(getOwnerRepoForRemote(REPO, 'origin')).resolves.toEqual({
owner: 'team',
repo: 'orca'
})
})
it('caches a successful HostName expansion so repeat probes skip ssh -G', async () => {
mockRemoteUrl('git@github-work:team/orca.git')
resolveWithSshGMock.mockResolvedValue(sshConfig('github.com'))
await expect(getOwnerRepoForRemote(REPO, 'origin')).resolves.toEqual({
owner: 'team',
repo: 'orca'
})
await expect(getOwnerRepoForRemote(REPO, 'origin')).resolves.toEqual({
owner: 'team',
repo: 'orca'
})
expect(resolveWithSshGMock).toHaveBeenCalledTimes(1)
})
it('isolates case-sensitive OpenSSH Host aliases in the cache', async () => {
resolveWithSshGMock.mockImplementation(async (host: string) =>
sshConfig(host === 'GitHub-Work' ? 'github.com' : 'gitlab.com')
)
await expect(
classifyGitHubOwnerRepoFromRemoteUrl('git@GitHub-Work:team/orca.git')
).resolves.toEqual({
kind: 'github',
ownerRepo: { owner: 'team', repo: 'orca' }
})
await expect(
classifyGitHubOwnerRepoFromRemoteUrl('git@github-work:team/orca.git')
).resolves.toEqual({
kind: 'not-github',
cacheWithGitConfigSignature: false
})
expect(resolveWithSshGMock).toHaveBeenCalledTimes(2)
})
it('classifies a resolved non-GitHub HostName as not-github', async () => {
resolveWithSshGMock.mockResolvedValueOnce(sshConfig('gitlab.com'))
await expect(
classifyGitHubOwnerRepoFromRemoteUrl('git@gitlab-work:team/orca.git')
).resolves.toEqual({ kind: 'not-github', cacheWithGitConfigSignature: false })
})
})