import { beforeEach, describe, expect, it, vi } from 'vitest' import type * as SshGitDispatch from '../providers/ssh-git-dispatch' import type * as GitHubEnterpriseRepository from './github-enterprise-repository' import type * as GhUtils from './gh-utils' const { getEnterpriseGitHubRepoSlugMock, getOwnerRepoMock, getOwnerRepoForRemoteMock, getSshGitProviderGenerationMock, isGitHubHostAuthenticatedMock, shouldProbeGitRemoteMock } = vi.hoisted(() => ({ getEnterpriseGitHubRepoSlugMock: vi.fn(), getOwnerRepoMock: vi.fn(), getOwnerRepoForRemoteMock: vi.fn(), getSshGitProviderGenerationMock: vi.fn(() => 0), isGitHubHostAuthenticatedMock: vi.fn(), shouldProbeGitRemoteMock: vi.fn(async () => true) })) vi.mock('../providers/ssh-git-dispatch', async (importOriginal) => ({ ...(await importOriginal()), getSshGitProviderGeneration: getSshGitProviderGenerationMock })) vi.mock('./gh-utils', async (importOriginal) => ({ ...(await importOriginal()), getOwnerRepo: getOwnerRepoMock, // Why: origin resolution uses getOwnerRepoForRemote, not getOwnerRepo. getOwnerRepoForRemote: getOwnerRepoForRemoteMock })) vi.mock('./github-enterprise-repository', async (importOriginal) => ({ ...(await importOriginal()), getEnterpriseGitHubRepoSlug: getEnterpriseGitHubRepoSlugMock, isGitHubHostAuthenticated: isGitHubHostAuthenticatedMock })) vi.mock('../git/remote-name-listing', () => ({ shouldProbeGitRemote: shouldProbeGitRemoteMock })) import { _resetOriginGitHubApiRepositoryCache, getGitHubApiRepositoryForRemote, getIssueGitHubApiRepository, getOriginGitHubApiRepository, githubHostExecOptions, resolveGitHubApiRepository, resolveGitHubApiRepositoryCandidates, resolveGitHubRepoExecution } from './github-api-repository' beforeEach(() => { _resetOriginGitHubApiRepositoryCache() getEnterpriseGitHubRepoSlugMock.mockReset().mockResolvedValue(null) getOwnerRepoMock.mockReset().mockResolvedValue(null) getOwnerRepoForRemoteMock.mockReset().mockResolvedValue(null) getSshGitProviderGenerationMock.mockReset().mockReturnValue(0) isGitHubHostAuthenticatedMock.mockReset().mockResolvedValue(false) shouldProbeGitRemoteMock.mockReset().mockResolvedValue(true) }) describe('githubHostExecOptions', () => { it('pins every known repository host', () => { expect(githubHostExecOptions({ owner: 'acme', repo: 'widgets', host: 'github.com' })).toEqual({ host: 'github.com' }) expect( githubHostExecOptions({ owner: 'acme', repo: 'widgets', host: 'github.acme-corp.com' }) ).toEqual({ host: 'github.acme-corp.com' }) }) it('does not invent a host when repository identity is unavailable or legacy', () => { expect(githubHostExecOptions({ owner: 'acme', repo: 'widgets' })).toEqual({}) expect(githubHostExecOptions(null)).toEqual({}) }) }) describe('resolveGitHubRepoExecution', () => { it('combines local repository and GitHub host execution options', async () => { const ownerRepo = { owner: 'acme', repo: 'widgets', host: 'github.acme-corp.com:8443' } isGitHubHostAuthenticatedMock.mockResolvedValue(true) await expect( resolveGitHubRepoExecution('/repo', ownerRepo, null, { wslDistro: 'Ubuntu' }) ).resolves.toEqual({ ownerRepo, ghOptions: { cwd: '/repo', wslDistro: 'Ubuntu', host: 'github.acme-corp.com:8443' } }) expect(isGitHubHostAuthenticatedMock).toHaveBeenCalledWith( 'github.acme-corp.com:8443', '/repo', null, { wslDistro: 'Ubuntu' } ) }) it('rejects an explicit Enterprise host absent from the local gh auth inventory', async () => { await expect( resolveGitHubApiRepository( '/remote/repo', { owner: 'acme', repo: 'widgets', host: 'evil.example.test' }, 'ssh-1' ) ).resolves.toBeNull() expect(isGitHubHostAuthenticatedMock).toHaveBeenCalledWith( 'evil.example.test', '/remote/repo', 'ssh-1', {} ) }) it.each([ { owner: 'acme%2Fadmin', repo: 'widgets' }, { owner: 'acme', repo: '..' } ])('rejects repository overrides that could alter a gh REST path: %o', async (repository) => { await expect( resolveGitHubApiRepository('/repo', { ...repository, host: 'github.com' }) ).resolves.toBeNull() expect(isGitHubHostAuthenticatedMock).not.toHaveBeenCalled() }) it.each(['acme', 'octocat_acme'])( 'normalizes github.com for %s without an auth inventory probe', async (owner) => { await expect( resolveGitHubApiRepository('/repo', { owner, repo: 'widgets', host: ' GitHub.COM ' }) ).resolves.toEqual({ owner, repo: 'widgets', host: 'github.com' }) expect(isGitHubHostAuthenticatedMock).not.toHaveBeenCalled() } ) it('backfills the origin host for a host-less caller-specific resolver', async () => { const ownerRepo = { owner: 'upstream', repo: 'widgets' } getOwnerRepoForRemoteMock.mockResolvedValue({ owner: 'fork', repo: 'widgets' }) await expect(resolveGitHubRepoExecution('/repo', async () => ownerRepo)).resolves.toEqual({ ownerRepo: { ...ownerRepo, host: 'github.com' }, ghOptions: { cwd: '/repo', host: 'github.com' } }) }) it('backfills an Enterprise origin host for a host-less caller-specific resolver', async () => { const ownerRepo = { owner: 'upstream', repo: 'widgets' } getEnterpriseGitHubRepoSlugMock.mockResolvedValue({ owner: 'fork', repo: 'widgets', host: 'github.acme-corp.com' }) await expect(resolveGitHubRepoExecution('/repo', async () => ownerRepo)).resolves.toEqual({ ownerRepo: { ...ownerRepo, host: 'github.acme-corp.com' }, ghOptions: { cwd: '/repo', host: 'github.acme-corp.com' } }) }) it('rejects a host-less caller-specific resolver for a connection-backed repository', async () => { await expect( resolveGitHubRepoExecution( '/remote/repo', async () => ({ owner: 'upstream', repo: 'widgets' }), 'ssh-1' ) ).resolves.toEqual({ ownerRepo: null, ghOptions: {} }) }) it('rejects a host-less caller-specific resolver for an unresolved local repository', async () => { await expect( resolveGitHubRepoExecution('/repo', async () => ({ owner: 'upstream', repo: 'widgets' })) ).resolves.toEqual({ ownerRepo: null, ghOptions: { cwd: '/repo' } }) }) it('preserves an authoritative null from a caller-specific resolver', async () => { getOwnerRepoMock.mockResolvedValue({ owner: 'origin', repo: 'widgets' }) await expect(resolveGitHubRepoExecution('/repo', async () => null)).resolves.toEqual({ ownerRepo: null, ghOptions: { cwd: '/repo' } }) expect(getOwnerRepoMock).not.toHaveBeenCalled() }) }) describe('origin repository cache', () => { it('isolates Enterprise identity across SSH provider generations', async () => { const beforeReconnect = { owner: 'acme', repo: 'widgets', host: 'github.acme-corp.com' } const afterReconnect = { owner: 'acme', repo: 'other', host: 'github.acme-corp.com' } getEnterpriseGitHubRepoSlugMock .mockResolvedValueOnce(beforeReconnect) .mockResolvedValueOnce(afterReconnect) await expect(getOriginGitHubApiRepository('/remote/repo', 'ssh-1')).resolves.toEqual( beforeReconnect ) getSshGitProviderGenerationMock.mockReturnValue(1) await expect(getOriginGitHubApiRepository('/remote/repo', 'ssh-1')).resolves.toEqual( afterReconnect ) expect(getEnterpriseGitHubRepoSlugMock).toHaveBeenCalledTimes(2) }) it('keeps an indeterminate auth inventory unverifiable during candidate discovery', async () => { getEnterpriseGitHubRepoSlugMock.mockResolvedValue(undefined) await expect( getGitHubApiRepositoryForRemote( '/remote/repo', 'origin', 'ssh-1', {}, { requireVerifiedSshProbe: true } ) ).rejects.toThrow('GitHub repository identity is unverifiable.') }) it('does not reuse a tolerant SSH miss for verified candidate discovery', async () => { const enterprise = { owner: 'acme', repo: 'widgets', host: 'github.acme-corp.com' } getEnterpriseGitHubRepoSlugMock.mockResolvedValueOnce(null).mockResolvedValueOnce(enterprise) await expect(getOriginGitHubApiRepository('/remote/repo', 'ssh-1')).resolves.toBeNull() await expect( getGitHubApiRepositoryForRemote( '/remote/repo', 'origin', 'ssh-1', {}, { requireVerifiedSshProbe: true } ) ).resolves.toEqual(enterprise) expect(getEnterpriseGitHubRepoSlugMock).toHaveBeenCalledTimes(2) }) it('does not cache an indeterminate Enterprise auth probe', async () => { const enterprise = { owner: 'acme', repo: 'widgets', host: 'github.acme-corp.com' } getEnterpriseGitHubRepoSlugMock .mockResolvedValueOnce(undefined) .mockResolvedValueOnce(enterprise) await expect(getOriginGitHubApiRepository('/repo')).resolves.toBeNull() await expect(getOriginGitHubApiRepository('/repo')).resolves.toEqual(enterprise) expect(getEnterpriseGitHubRepoSlugMock).toHaveBeenCalledTimes(2) }) it('still caches a definitive negative Enterprise probe', async () => { await expect(getOriginGitHubApiRepository('/repo')).resolves.toBeNull() await expect(getOriginGitHubApiRepository('/repo')).resolves.toBeNull() expect(getEnterpriseGitHubRepoSlugMock).toHaveBeenCalledTimes(1) }) it('does not reuse a cached answer after the SSH provider reconnects', async () => { const oldRepository = { owner: 'old-owner', repo: 'widgets', host: 'github.acme-corp.com' } const newRepository = { owner: 'new-owner', repo: 'widgets', host: 'github.acme-corp.com' } getSshGitProviderGenerationMock.mockReturnValue(1) getEnterpriseGitHubRepoSlugMock .mockResolvedValueOnce(oldRepository) .mockResolvedValueOnce(newRepository) await expect(getOriginGitHubApiRepository('/repo', 'ssh-1')).resolves.toEqual(oldRepository) await expect(getOriginGitHubApiRepository('/repo', 'ssh-1')).resolves.toEqual(oldRepository) expect(getEnterpriseGitHubRepoSlugMock).toHaveBeenCalledTimes(1) getSshGitProviderGenerationMock.mockReturnValue(2) await expect(getOriginGitHubApiRepository('/repo', 'ssh-1')).resolves.toEqual(newRepository) expect(getEnterpriseGitHubRepoSlugMock).toHaveBeenCalledTimes(2) }) it('does not reuse a cached negative after the SSH provider reconnects', async () => { const repository = { owner: 'acme', repo: 'widgets', host: 'github.acme-corp.com' } getSshGitProviderGenerationMock.mockReturnValue(1) getEnterpriseGitHubRepoSlugMock.mockResolvedValueOnce(null).mockResolvedValueOnce(repository) await expect(getOriginGitHubApiRepository('/repo', 'ssh-1')).resolves.toBeNull() await expect(getOriginGitHubApiRepository('/repo', 'ssh-1')).resolves.toBeNull() expect(getEnterpriseGitHubRepoSlugMock).toHaveBeenCalledTimes(1) getSshGitProviderGenerationMock.mockReturnValue(2) await expect(getOriginGitHubApiRepository('/repo', 'ssh-1')).resolves.toEqual(repository) expect(getEnterpriseGitHubRepoSlugMock).toHaveBeenCalledTimes(2) }) it('isolates an in-flight probe from an older SSH provider generation', async () => { const oldRepository = { owner: 'old-owner', repo: 'widgets', host: 'github.acme-corp.com' } const newRepository = { owner: 'new-owner', repo: 'widgets', host: 'github.acme-corp.com' } let resolveOldProbe: (repository: typeof oldRepository) => void = () => {} const oldProbeResult = new Promise((resolve) => { resolveOldProbe = resolve }) getEnterpriseGitHubRepoSlugMock .mockImplementationOnce(() => oldProbeResult) .mockResolvedValueOnce(newRepository) getSshGitProviderGenerationMock.mockReturnValue(1) const oldProbe = getOriginGitHubApiRepository('/repo', 'ssh-1') await vi.waitFor(() => expect(getEnterpriseGitHubRepoSlugMock).toHaveBeenCalledTimes(1)) getSshGitProviderGenerationMock.mockReturnValue(2) const newProbe = getOriginGitHubApiRepository('/repo', 'ssh-1') await vi.waitFor(() => expect(getEnterpriseGitHubRepoSlugMock).toHaveBeenCalledTimes(2)) await expect(newProbe).resolves.toEqual(newRepository) resolveOldProbe(oldRepository) await expect(oldProbe).resolves.toEqual(oldRepository) await expect(getOriginGitHubApiRepository('/repo', 'ssh-1')).resolves.toEqual(newRepository) expect(getEnterpriseGitHubRepoSlugMock).toHaveBeenCalledTimes(2) }) }) describe('skip missing upstream remote probes', () => { it('starts the issue origin probe before checking whether upstream exists', async () => { let releaseRemoteProbe: (value: boolean) => void = () => undefined shouldProbeGitRemoteMock.mockReturnValue( new Promise((resolve) => { releaseRemoteProbe = resolve }) ) let originStarted = false getOwnerRepoForRemoteMock.mockImplementation(async (_path, remote) => { if (remote === 'origin') { originStarted = true return { owner: 'fork', repo: 'orca' } } return { owner: 'stablyai', repo: 'orca' } }) const resultPromise = getIssueGitHubApiRepository('/repo') expect(originStarted).toBe(true) releaseRemoteProbe(true) await expect(resultPromise).resolves.toEqual({ owner: 'stablyai', repo: 'orca', host: 'github.com' }) }) it('does not probe upstream for issue identity when that remote is absent', async () => { shouldProbeGitRemoteMock.mockResolvedValue(false) getOwnerRepoForRemoteMock.mockImplementation(async (_path, remote) => remote === 'origin' ? { owner: 'acme', repo: 'widgets' } : null ) await expect(getIssueGitHubApiRepository('/repo')).resolves.toEqual({ owner: 'acme', repo: 'widgets', host: 'github.com' }) expect(getOwnerRepoForRemoteMock.mock.calls.map(([, remote]) => remote)).toEqual(['origin']) }) it('still probes upstream for issue identity when that remote is present', async () => { getOwnerRepoForRemoteMock.mockImplementation(async (_path, remote) => remote === 'upstream' ? { owner: 'stablyai', repo: 'orca' } : { owner: 'fork', repo: 'orca' } ) await expect(getIssueGitHubApiRepository('/repo')).resolves.toEqual({ owner: 'stablyai', repo: 'orca', host: 'github.com' }) expect(getOwnerRepoForRemoteMock).toHaveBeenCalledWith('/repo', 'upstream', undefined, {}) }) it('observes a rejected origin probe when upstream resolves the issue repository', async () => { const originError = new Error('origin probe failed') getOwnerRepoForRemoteMock.mockImplementation(async (_path, remote) => { if (remote === 'origin') { throw originError } return { owner: 'stablyai', repo: 'orca' } }) await expect(getIssueGitHubApiRepository('/repo')).resolves.toEqual({ owner: 'stablyai', repo: 'orca', host: 'github.com' }) }) it('preserves a rejected origin probe when upstream cannot resolve the issue repository', async () => { const originError = new Error('origin probe failed') getOwnerRepoForRemoteMock.mockImplementation(async (_path, remote) => { if (remote === 'origin') { throw originError } return null }) await expect(getIssueGitHubApiRepository('/repo')).rejects.toBe(originError) }) it('does not probe upstream for PR candidates when that remote is absent', async () => { shouldProbeGitRemoteMock.mockResolvedValue(false) getOwnerRepoForRemoteMock.mockResolvedValue({ owner: 'fork', repo: 'orca' }) await expect(resolveGitHubApiRepositoryCandidates('/repo')).resolves.toEqual({ candidates: [{ owner: 'fork', repo: 'orca', host: 'github.com' }], headRepo: { owner: 'fork', repo: 'orca', host: 'github.com' } }) expect(getOwnerRepoForRemoteMock.mock.calls.map(([, remote]) => remote)).toEqual(['origin']) }) it('observes and propagates a verified origin probe failure while listing remotes', async () => { let releaseRemoteProbe: (value: boolean) => void = () => undefined shouldProbeGitRemoteMock.mockReturnValue( new Promise((resolve) => { releaseRemoteProbe = resolve }) ) const originError = new Error('origin probe failed') getOwnerRepoForRemoteMock.mockImplementation(async (_path, remote) => { if (remote === 'origin') { throw originError } return { owner: 'stablyai', repo: 'orca' } }) const resultPromise = resolveGitHubApiRepositoryCandidates('/repo') await vi.waitFor(() => expect(getOwnerRepoForRemoteMock).toHaveBeenCalledWith( '/repo', 'origin', undefined, {}, { requireVerifiedSshProbe: true } ) ) releaseRemoteProbe(true) await expect(resultPromise).rejects.toBe(originError) }) it('still probes upstream for PR candidates when that remote is present', async () => { getOwnerRepoForRemoteMock.mockImplementation(async (_path, remote) => remote === 'upstream' ? { owner: 'Acme', repo: 'Orca' } : { owner: 'acme', repo: 'orca' } ) await expect(resolveGitHubApiRepositoryCandidates('/repo')).resolves.toEqual({ candidates: [{ owner: 'Acme', repo: 'Orca', host: 'github.com' }], headRepo: { owner: 'acme', repo: 'orca', host: 'github.com' } }) expect(getOwnerRepoForRemoteMock).toHaveBeenCalledWith( '/repo', 'upstream', undefined, {}, { requireVerifiedSshProbe: true } ) }) })