refactor(github): organize concrete feature owners (#41601)

This commit is contained in:
Neil
2026-11-12 09:17:26 -08:00
committed by GitHub
parent ff9fd6634b
commit fb8d6f950b
1003 changed files with 4460 additions and 8422 deletions
@@ -1,13 +1,16 @@
import {
isUnresolvedSshHostAlias,
foldComparableGitHubHost
} from '../../shared/git/remotes/git-remote-host-alias'
import { githubHostFromIdentityKey } from '../../shared/github/repository-identity-key'
import { rawSshTransportHost, remoteUrlUsesSshTransport } from './github-remote-identity-parsing'
} from '../../../shared/git/remotes/git-remote-host-alias'
import { githubHostFromIdentityKey } from '../../../shared/github/repositories/repository-identity-key'
import {
rawSshTransportHost,
remoteUrlUsesSshTransport
} from '../repositories/github-remote-identity-parsing'
import {
resolveSshConfigHostname,
type SshConfigResolutionContext
} from './github-ssh-host-alias-resolution'
} from '../repositories/github-ssh-host-alias-resolution'
export type SshAliasHostnameResolver = (
alias: string,
@@ -2,7 +2,7 @@ import { describe, expect, it, vi } from 'vitest'
const { ghExecFileAsyncMock } = vi.hoisted(() => ({ ghExecFileAsyncMock: vi.fn() }))
vi.mock('../git/command-runner/hosted-cli/gh-exec-file', async (importOriginal) => ({
vi.mock('../../git/command-runner/hosted-cli/gh-exec-file', async (importOriginal) => ({
...(await importOriginal<Record<string, unknown>>()),
ghExecFileAsync: ghExecFileAsyncMock
}))
@@ -1,6 +1,6 @@
// Environment tokens override keyring credentials and cannot be refreshed through gh auth refresh.
import { ghExecFileAsync } from '../git/command-runner/hosted-cli/gh-exec-file'
import type { GhAuthDiagnostic, GhAuthAccount } from '../../shared/github/auth-types'
import { ghExecFileAsync } from '../../git/command-runner/hosted-cli/gh-exec-file'
import type { GhAuthDiagnostic, GhAuthAccount } from '../../../shared/github/accounts/auth-types'
// ProjectV2 and surrounding repository/organization queries require these scopes.
const REQUIRED_SCOPES = ['project', 'read:org', 'repo'] as const
@@ -6,12 +6,12 @@ import {
type GhCapabilityTarget
} from './gh-capability-state'
import { invalidateGhAccountTokenCache, resolveGhAccountToken } from './gh-account-token'
import { normalizeGhAccountBinding } from '../../shared/github/account-binding'
import { normalizeGhAccountBinding } from '../../../shared/github/accounts/account-binding'
import type {
GhAccountBindingInventory,
GhAccountBindingValidationResult
} from '../../shared/github/auth-types'
import type { LocalGitExecOptions } from './github-repository-identity'
} from '../../../shared/github/accounts/auth-types'
import type { LocalGitExecOptions } from '../repositories/github-repository-identity'
function capabilityTargetFromLocalGitOptions(
localGitOptions: LocalGitExecOptions = {}
@@ -1,12 +1,12 @@
// Resolve bound credentials into child environments only; never mutate process-wide auth or log tokens.
import { ghExecFileAsync } from '../git/command-runner/hosted-cli/gh-exec-file'
import type { GhCaptureLifetimeHooks } from '../git/command-runner/hosted-cli/gh-capture-lifetime'
import { addWslEnvKeys } from '../../shared/wsl-env'
import { ghExecFileAsync } from '../../git/command-runner/hosted-cli/gh-exec-file'
import type { GhCaptureLifetimeHooks } from '../../git/command-runner/hosted-cli/gh-capture-lifetime'
import { addWslEnvKeys } from '../../../shared/wsl-env'
import {
ghTokenEnvVarForHost,
normalizeGhAccountBinding,
type GhAccountBinding
} from '../../shared/github/account-binding'
} from '../../../shared/github/accounts/account-binding'
import {
createGhMultiAccountUnsupportedError,
getGhExecutionHostKey,
@@ -1,11 +1,11 @@
// Require gh >=2.40 multi-account support per execution runtime before offering bindings.
import { ghExecFileAsync } from '../git/command-runner/hosted-cli/gh-exec-file'
import { parseWslUncPath } from '../../shared/wsl-paths'
import type { GhMultiAccountCapability } from '../../shared/github/auth-types'
import { ghExecFileAsync } from '../../git/command-runner/hosted-cli/gh-exec-file'
import { parseWslUncPath } from '../../../shared/wsl-paths'
import type { GhMultiAccountCapability } from '../../../shared/github/accounts/auth-types'
import {
createGhLogicalLifetime,
type GhCaptureLifetimeHooks
} from '../git/command-runner/hosted-cli/gh-capture-lifetime'
} from '../../git/command-runner/hosted-cli/gh-capture-lifetime'
export type GhCapabilityTarget = {
cwd?: string
@@ -1,6 +1,6 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import type * as GhUtils from './gh-utils'
import type * as GitRunner from '../git/command-runner/git-exec-file'
import type * as GhUtilsOwner1 from '../../git/command-runner/hosted-cli/gh-exec-file'
import type * as GitRunner from '../../git/command-runner/git-exec-file'
import { mkdtempSync, rmSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
@@ -9,29 +9,29 @@ import {
closeTestStores,
makeRepo,
testState
} from '../persistence/persistence-test-harness'
import { OrcaRuntimeService } from '../runtime/orca-runtime'
import { RpcDispatcher } from '../runtime/rpc/dispatcher'
import { GITHUB_REPO_WORK_ITEM_METHODS } from '../runtime/rpc/methods/source-control/github/github-repo-work-item-methods'
import { GITHUB_PULL_REQUEST_METHODS } from '../runtime/rpc/methods/source-control/github/github-pull-request-methods'
import { registerGitHubWorkItemHandlers } from '../ipc/source-control/github-work-item-handlers'
import { registerGitHubIssueMutationHandlers } from '../ipc/source-control/github-issue-mutation-handlers'
import { registerGitHubPRReviewHandlers } from '../ipc/source-control/github-pr-review-handlers'
import { ghPullRequestsAndWorkItemsApi } from '../../preload/api/gh-bridge-pull-requests-and-work-items'
import { ghMutationsAndProjectsApi } from '../../preload/api/gh-bridge-mutations-and-projects'
import type { GhAccountBinding } from '../../shared/github/account-binding'
import { RPC_PARAMS_BY_METHOD } from '../../shared/rpc-contract/rpc-params-catalog.generated'
} from '../../persistence/persistence-test-harness'
import { OrcaRuntimeService } from '../../runtime/orca-runtime'
import { RpcDispatcher } from '../../runtime/rpc/dispatcher'
import { GITHUB_REPO_WORK_ITEM_METHODS } from '../../runtime/rpc/methods/source-control/github/github-repo-work-item-methods'
import { GITHUB_PULL_REQUEST_METHODS } from '../../runtime/rpc/methods/source-control/github/github-pull-request-methods'
import { registerGitHubWorkItemHandlers } from '../../ipc/source-control/github-work-item-handlers'
import { registerGitHubIssueMutationHandlers } from '../../ipc/source-control/github-issue-mutation-handlers'
import { registerGitHubPRReviewHandlers } from '../../ipc/source-control/github-pr-review-handlers'
import { ghPullRequestsAndWorkItemsApi } from '../../../preload/api/gh-bridge-pull-requests-and-work-items'
import { ghMutationsAndProjectsApi } from '../../../preload/api/gh-bridge-mutations-and-projects'
import type { GhAccountBinding } from '../../../shared/github/accounts/account-binding'
import { RPC_PARAMS_BY_METHOD } from '../../../shared/rpc-contract/rpc-params-catalog.generated'
const { exec, handle, invoke } = vi.hoisted(() => ({
exec: vi.fn<typeof GhUtils.ghExecFileAsync>(),
exec: vi.fn<typeof GhUtilsOwner1.ghExecFileAsync>(),
handle: vi.fn<(channel: string, listener: (event: unknown, args: unknown) => unknown) => void>(),
invoke: vi.fn<(channel: string, args: unknown) => Promise<unknown>>()
}))
vi.mock('./gh-utils', async (original) => ({
...(await original<typeof GhUtils>()),
vi.mock('../../git/command-runner/hosted-cli/gh-exec-file', async (original) => ({
...(await original<typeof GhUtilsOwner1>()),
ghExecFileAsync: exec
}))
vi.mock('../git/command-runner/git-exec-file', async (original) => {
vi.mock('../../git/command-runner/git-exec-file', async (original) => {
const actual = await original<typeof GitRunner>()
return {
...actual,
@@ -55,13 +55,13 @@ vi.mock('electron', () => ({
BrowserWindow: { fromId: () => null, getAllWindows: () => [] },
webContents: { fromId: () => null, getAllWebContents: () => [] }
}))
vi.mock('../ipc/ui', () => ({ sendToTrustedUIRenderer: vi.fn() }))
import type { Project } from '../../shared/project-types'
import type { Repo } from '../../shared/repo-types'
import { ghRepoExecOptions, githubRepoContext } from './github-repository-identity'
vi.mock('../../ipc/ui', () => ({ sendToTrustedUIRenderer: vi.fn() }))
import type { Project } from '../../../shared/project-types'
import type { Repo } from '../../../shared/repo-types'
import { ghRepoExecOptions, githubRepoContext } from '../repositories/github-repository-identity'
import { installGitHubProjectAccounts } from './github-project-account'
import { githubReadExecutionScope } from './github-read-execution-scope'
import { resolveGitHubRepoExecution } from './github-api-repository'
import { codeHostReadExecutionScope as githubReadExecutionScope } from '../../source-control/code-host-read-execution-scope'
import { resolveGitHubRepoExecution } from '../repositories/github-api-repository'
let release: (() => void) | undefined
afterEach(() => release?.())
@@ -1,16 +1,16 @@
import type { Repo } from '../../shared/repo-types'
import { normalizeRuntimePathForComparison } from '../../shared/cross-platform-path'
import { getConnectionExecutionHostId, type ExecutionHostId } from '../../shared/execution-host'
import { normalizeGhAccountBinding } from '../../shared/github/account-binding'
import { isGitRepoKind } from '../../shared/repo-kind'
import { splitWorktreeIdForFilesystem } from '../../shared/worktree/id'
import { parseWslUncPath } from '../../shared/wsl-paths'
import type { Repo } from '../../../shared/repo-types'
import { normalizeRuntimePathForComparison } from '../../../shared/cross-platform-path'
import { getConnectionExecutionHostId, type ExecutionHostId } from '../../../shared/execution-host'
import { normalizeGhAccountBinding } from '../../../shared/github/accounts/account-binding'
import { isGitRepoKind } from '../../../shared/repo-kind'
import { splitWorktreeIdForFilesystem } from '../../../shared/worktree/id'
import { parseWslUncPath } from '../../../shared/wsl-paths'
import {
resolveLocalProjectRuntimesForRepos,
type ProjectRuntimeResolutionStore
} from '../workspaces/execution-host/local-project-runtime-resolution'
import { getStoredRepoExecutionHostId } from '../workspaces/execution-host/repo-execution-host'
import type { GitHubRepoContext } from './github-repository-identity'
} from '../../workspaces/execution-host/local-project-runtime-resolution'
import { getStoredRepoExecutionHostId } from '../../workspaces/execution-host/repo-execution-host'
import type { GitHubRepoContext } from '../repositories/github-repository-identity'
type AccountStore = ProjectRuntimeResolutionStore & {
getRepos: () => readonly Repo[]
@@ -1,41 +1,43 @@
import type { createGitHubPullRequest } from './client/create/create-github-pull-request'
import type { createGitHubPullRequest } from '../client/create/create-github-pull-request'
import { afterEach, beforeEach, expect, it, vi } from 'vitest'
import { mkdtempSync, rmSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { createFakeSpawnedChild } from '../../shared/__fixtures__/fake-spawned-child'
import { createFakeSpawnedChild } from '../../../shared/__fixtures__/fake-spawned-child'
import {
createStore,
closeTestStores,
testState,
makeRepo
} from '../persistence/persistence-test-harness'
} from '../../persistence/persistence-test-harness'
const { spawnMock, remote } = vi.hoisted(() => ({ spawnMock: vi.fn(), remote: vi.fn() }))
vi.mock('node:child_process', async (original) => ({ ...(await original()), spawn: spawnMock }))
vi.mock('./github-api-repository-remote-probe', () => ({
vi.mock('../repositories/github-api-repository-remote-probe', () => ({
getOriginGitHubApiRepository: remote,
getGitHubApiRepositoryForRemote: remote
}))
vi.mock('../git/remotes/remote-name-listing', () => ({ shouldProbeGitRemote: async () => false }))
vi.mock('./rate-limit', () => ({
vi.mock('../../git/remotes/remote-name-listing', () => ({
shouldProbeGitRemote: async () => false
}))
vi.mock('../api/rate-limit', () => ({
repositoryRateLimitGuard: () => ({ blocked: false }),
noteRepositoryRateLimitSpend: () => {},
spendsSharedGitHubComQuota: () => false
}))
vi.mock('./client/list/work-item-search-page', () => ({ usesGraphqlWorkItemSearch: () => false }))
vi.mock('./client/lookup/pr-lookup-rate-limit', () => ({ assertRateLimitBudget: async () => {} }))
vi.mock('../client/list/work-item-search-page', () => ({ usesGraphqlWorkItemSearch: () => false }))
vi.mock('../client/lookup/pr-lookup-rate-limit', () => ({ assertRateLimitBudget: async () => {} }))
vi.mock('../source-control/forge-provider-detection', () => ({
vi.mock('../../source-control/forge-provider-detection', () => ({
detectHostedReviewProvider: async () => 'github',
getForgeProviderForRepository: async () => ({
id: 'github',
getReviewForBranch: async () => null,
createReview: async (...args: Parameters<typeof createGitHubPullRequest>) =>
(await import('./client/create/create-github-pull-request')).createGitHubPullRequest(...args)
(await import('../client/create/create-github-pull-request')).createGitHubPullRequest(...args)
})
}))
vi.mock('../source-control/hosted-review-creation-git-state', async (original) => ({
vi.mock('../../source-control/hosted-review-creation-git-state', async (original) => ({
...(await original()),
getCurrentBranch: async () => 'feature',
hasUncommittedChanges: async () => false,
@@ -43,29 +45,29 @@ vi.mock('../source-control/hosted-review-creation-git-state', async (original) =
baseRefExistsOnRemote: async () => true,
getDefaultBaseRef: async () => 'parent'
}))
vi.mock('../source-control/hosted-review-creation-provider', async (original) => ({
vi.mock('../../source-control/hosted-review-creation-provider', async (original) => ({
...(await original()),
isProviderAuthenticated: async () => true
}))
import { installGitHubProjectAccounts } from './github-project-account'
import { getPRChecks } from './client/check/get-pr-checks'
import { rerunPRChecks } from './client/check/rerun-pr-checks'
import { getPRFileContents } from './work-item-details'
import './pr-start-point'
import '../runtime/integrations/github/runtime-github-repository-query-commands'
import { getPRChecks } from '../client/check/get-pr-checks'
import { getPRFileContents } from '../pull-requests/files/pull-request-file-contents'
import '../pull-requests/start-points/pr-start-point'
import '../../runtime/integrations/github/runtime-github-repository-query-commands'
import { clearGhAccountTokenCacheForTests } from './gh-account-token'
import { clearGhCapabilityStateForTests } from './gh-capability-state'
import { _resetGhRateLimitBreaker } from '../git/command-runner/hosted-cli/gh-rate-limit-breaker'
import { getPRForBranchOutcome } from './client/lookup/pr-for-branch-outcome'
import { createStackedHostedReview } from '../source-control/stacked-hosted-review-creation'
import './pr-refresh-candidate-policy'
import { fetchPullRequestWorkItem } from './client/fetch/work-item-fetch'
import { repositoryMergeMetadataCache } from './client/detect/repository-merge-metadata-cache'
import { _resetPRStackSummaryCacheForTests } from './client/lookup/pr-stack-summary-cache'
import { _resetGitHubPRStackCacheForTests } from './github-pr-stack'
import { resetMergedPRCommitMembershipCacheForTest } from './merged-pr-commit-membership'
import './github-repository-identity'
import { _resetGhRateLimitBreaker } from '../../git/command-runner/hosted-cli/gh-rate-limit-breaker'
import { getPRForBranchOutcome } from '../client/lookup/pr-for-branch-outcome'
import '../pull-requests/refresh/pr-refresh-candidate-policy'
import { fetchPullRequestWorkItem } from '../client/fetch/work-item-fetch'
import { repositoryMergeMetadataCache } from '../client/detect/repository-merge-metadata-cache'
import { _resetPRStackSummaryCacheForTests } from '../client/lookup/pr-stack-summary-cache'
import { _resetGitHubPRStackCacheForTests } from '../pull-requests/stacks/github-pr-stack'
import { resetMergedPRCommitMembershipCacheForTest } from '../pull-requests/refresh/merged-pr-commit-membership'
import '../repositories/github-repository-identity'
const alice = { host: 'github.com', user: 'Alice' }
const bob = { host: 'github.com', user: 'Bob' }
@@ -220,21 +222,6 @@ it('separates unresolved callers by saved account before coalescing and returns
expect(requests.map((r) => r.token)).toEqual(['Alice-token', 'Bob-token'])
})
it('settles the full rerun read/write on the initial account and uses the rebind on the next operation', async () => {
rebindDuringFirstResolution()
await expect(
rerunPRChecks('/work', 42, { prRepo: repository, failedOnly: true }, null, { ghAccount: bob })
).resolves.toEqual({ ok: true, count: 1 })
expect(requests.map((r) => ({ post: r.args.includes('POST'), token: r.token }))).toEqual([
{ post: false, token: 'Alice-token' },
{ post: true, token: 'Alice-token' }
])
await expect(
rerunPRChecks('/work', 42, { prRepo: repository, failedOnly: true })
).resolves.toEqual({ ok: true, count: 1 })
expect(requests.slice(2).map((r) => r.token)).toEqual(['Bob-token', 'Bob-token'])
})
it('loads both file versions under one saved selection across repository resolution and future rebind', async () => {
rebindDuringFirstResolution()
request = (args, child, token) =>
@@ -262,56 +249,6 @@ it('loads both file versions under one saved selection across repository resolut
})
})
it('settles stack prepare/create/register with one saved selection after a pending rebind', async () => {
let finish: () => void = () => {}
let started: () => void = () => {}
const resolving = new Promise<void>((resolve) => {
started = resolve
})
remote.mockImplementationOnce(
() =>
new Promise((resolve) => {
finish = () => resolve(repository)
started()
})
)
request = (args, child) => {
let output: unknown = []
if (args[0] === 'pr' && args[1] === 'create') {
output = { number: 43, url: 'https://github.com/acme/widgets/pull/43' }
} else if (args.some((arg) => arg.includes('pulls?') && arg.includes('%3Aparent'))) {
output = [
{
number: 42,
html_url: 'https://github.com/acme/widgets/pull/42',
head: { ref: 'parent' },
base: { ref: 'main' }
}
]
} else if (args.includes('POST')) {
output = { number: 50 }
}
queueMicrotask(() => complete(child, JSON.stringify(output)))
}
const pending = createStackedHostedReview(
'/work',
{ provider: 'github', base: 'parent', head: 'feature', title: 'Change' },
'local'
)
await resolving
rebind()
finish()
await expect(pending).resolves.toMatchObject({
ok: true,
number: 43,
stackNumber: 50,
parentReview: { number: 42 }
})
expect(requests.some((r) => r.args[0] === 'pr' && r.args[1] === 'create')).toBe(true)
expect(requests.some((r) => r.args.includes('POST'))).toBe(true)
expect(requests.every((r) => r.token === 'Alice-token')).toBe(true)
})
it('returns each saved viewer merge preference with omitted detector scope and reuses only that viewer', async () => {
request = (args, child, token) =>
queueMicrotask(() =>
@@ -1,5 +1,5 @@
import type { ClassifiedError } from '../../shared/classified-error'
import { classifiedCodeHostError as classified } from '../../shared/code-host-failure-copy'
import type { ClassifiedError } from '../../../shared/classified-error'
import { classifiedCodeHostError as classified } from '../../../shared/code-host-failure-copy'
export function classifyGhError(stderr: string): ClassifiedError {
const s = stderr.toLowerCase()
@@ -1,6 +1,5 @@
import type { PRCheckDetail } from '../../shared/github/check-types'
import type { CheckStatus, IssueInfo, PRInfo } from '../../shared/github/pull-request-types'
import { derivePRCheckStatusFromRollup } from '../../shared/source-control/checks/pr-check-status'
import type { PRCheckDetail } from '../../../shared/github/checks/check-types'
import type { IssueInfo, PRInfo } from '../../../shared/github/pull-requests/pull-request-types'
// REST check runs separate status/conclusion; gh pr checks combines them into one state.
@@ -138,7 +137,3 @@ export function mapIssueInfo(data: {
...(typeof data.body === 'string' ? { description: data.body } : {})
}
}
export function deriveCheckStatus(rollup: unknown[] | null | undefined): CheckStatus {
return derivePRCheckStatusFromRollup(rollup)
}
@@ -2,8 +2,12 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import type { revalidateGitHubRestJson } from './github-rest-revalidation'
const { gh, spend } = vi.hoisted(() => ({ gh: vi.fn(), spend: vi.fn() }))
vi.mock('./gh-utils', () => ({ ghExecFileAsync: gh }))
vi.mock('./rate-limit', () => ({ noteRepositoryRateLimitSpend: spend }))
vi.mock('../../git/command-runner/hosted-cli/gh-exec-file', () => ({
ghExecFileAsync: gh
}))
vi.mock('./rate-limit', () => ({
noteRepositoryRateLimitSpend: spend
}))
const repository = { owner: 'acme', repo: 'widgets', host: 'github.com' }
const endpoint = 'repos/acme/widgets/pulls/7'
@@ -58,23 +62,6 @@ describe('GitHub REST revalidation', () => {
expect(gh.mock.calls[2][0]).not.toContain('-H')
})
it('keeps credentials, hosts, runtimes, directories, and endpoints separate', async () => {
gh.mockResolvedValue(http(200, '{"number":7}', '"v1"'))
await read(repository, endpoint, options)
await read(repository, `${endpoint}/files`, options)
await read(repository, endpoint, { cwd: '/other' })
await read(repository, endpoint, { ...options, wslDistro: 'Ubuntu' })
await read({ ...repository, host: 'github.example.com' }, endpoint, options)
await read(repository, endpoint, {
...options,
ghAccount: { host: 'github.com', user: 'other' }
})
vi.stubEnv('GH_TOKEN', 'different-test-account')
await read(repository, endpoint, options)
expect(gh.mock.calls.every(([args]) => !args.includes('-H'))).toBe(true)
expect(gh.mock.calls[4][1].host).toBe('github.example.com')
})
it('does not retain malformed or validator-free bodies and rejects ungrounded 304 responses', async () => {
gh.mockResolvedValueOnce(http(200, '{"number":7}'))
.mockResolvedValueOnce(http(200, 'truncated', '"bad"'))
@@ -1,12 +1,10 @@
import { BoundedMap } from '../../shared/bounded-map'
import { extractExecError } from '../git/command-runner/process-capture/exec-error'
import { ghExecFileAsync } from './gh-utils'
import { githubReadExecutionScope } from './github-read-execution-scope'
import {
githubHostExecOptions,
type GitHubApiRepository,
type GitHubRepoExecOptions
} from './github-api-repository'
import { BoundedMap } from '../../../shared/bounded-map'
import { extractExecError } from '../../git/command-runner/process-capture/exec-error'
import { ghExecFileAsync } from '../../git/command-runner/hosted-cli/gh-exec-file'
import { codeHostReadExecutionScope as githubReadExecutionScope } from '../../source-control/code-host-read-execution-scope'
import { githubHostExecOptions } from '../repositories/github-repository-host'
import type { GitHubRepositoryIdentity as GitHubApiRepository } from '../../../shared/github/repositories/repository-identity'
import type { GitHubRepoExecOptions } from '../repositories/github-api-repository'
import { noteRepositoryRateLimitSpend } from './rate-limit'
type Representation = { etag: string; body: string }
@@ -4,18 +4,18 @@ const { ghExecFileAsyncMock } = vi.hoisted(() => ({
ghExecFileAsyncMock: vi.fn()
}))
vi.mock('../git/command-runner/hosted-cli/gh-exec-file', async (importOriginal) => ({
vi.mock('../../git/command-runner/hosted-cli/gh-exec-file', async (importOriginal) => ({
...(await importOriginal<Record<string, unknown>>()),
ghExecFileAsync: ghExecFileAsyncMock
}))
import { acquire, release } from './gh-utils'
import { acquire, release } from '../execution/github-operation-slots'
import { getRateLimit, _resetRateLimitCache } from './rate-limit'
import {
clearGhRateLimitBlock,
notifyGhPrimaryRateLimit,
type GhRateLimitBucket
} from '../git/command-runner/hosted-cli/gh-rate-limit-breaker'
} from '../../git/command-runner/hosted-cli/gh-rate-limit-breaker'
// Refine only the tripping host/runtime's breaker; preserve the shared native snapshot.
@@ -3,11 +3,11 @@ import type {
GetRateLimitResult,
GitHubRateLimitBucket,
GitHubRateLimitSnapshot
} from '../../shared/github/rate-limit-types'
import { isDefaultGitHubHost } from '../../shared/github/repository-identity-key'
import { PrioritySemaphore } from '../../shared/priority-semaphore'
import { isWslUncPath } from '../../shared/wsl-paths'
import { ghExecFileAsync } from '../git/command-runner/hosted-cli/gh-exec-file'
} from '../../../shared/github/api/rate-limit-types'
import { isDefaultGitHubHost } from '../../../shared/github/repositories/repository-identity-key'
import { PrioritySemaphore } from '../../../shared/priority-semaphore'
import { isWslUncPath } from '../../../shared/wsl-paths'
import { ghExecFileAsync } from '../../git/command-runner/hosted-cli/gh-exec-file'
import {
clearGhRateLimitBlock,
getGhRateLimitBlockedUntilMs,
@@ -16,7 +16,7 @@ import {
recordGhPrimaryRateLimit,
registerGhRateLimitResetProbe,
type GhRateLimitBucket
} from '../git/command-runner/hosted-cli/gh-rate-limit-breaker'
} from '../../git/command-runner/hosted-cli/gh-rate-limit-breaker'
// GET /rate_limit is quota-exempt; caching avoids a subprocess per refresh.
const RATE_LIMIT_CACHE_TTL_MS = 30_000
@@ -1,6 +1,11 @@
import { acquire, extractExecError, ghExecFileAsync, release } from './gh-utils'
import type { GitHubRepoExecution } from './github-api-repository'
import { assertGitHubImage, GitHubImageValidationError } from '../../shared/github/image-attachment'
import { acquire, release } from '../execution/github-operation-slots'
import { extractExecError } from '../../git/command-runner/process-capture/exec-error'
import { ghExecFileAsync } from '../../git/command-runner/hosted-cli/gh-exec-file'
import type { GitHubRepoExecution } from '../repositories/github-api-repository'
import {
assertGitHubImage,
GitHubImageValidationError
} from '../../../shared/github/attachments/image-attachment'
export async function uploadGitHubImage(args: {
fileName: string
@@ -1,181 +0,0 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import type { PRRefreshOutcome } from '../../shared/github/pull-request-refresh-types'
import type { GitHubRepoContext, LocalGitExecOptions } from './github-repository-identity'
import { getRepoExecutionHostId } from '../../shared/execution-host'
import { hostedReviewInfoFromGitHubPRInfo } from '../../shared/source-control/hosted-review/hosted-review-github'
import { makePR } from './pr-refresh-coordinator-test-harness'
import {
__resetHostedReviewBranchCacheForTests,
invalidateHostedReviewBranchCache,
withHostedReviewBranchCache
} from '../source-control/hosted-review-branch-cache'
const mocks = vi.hoisted(() => ({ resolve: vi.fn(), acquire: vi.fn(), release: vi.fn() }))
vi.mock('./gh-utils', () => ({
acquire: mocks.acquire,
release: mocks.release,
githubRepoContext: (
repoPath: string,
connectionId: string | null,
options: LocalGitExecOptions
) => ({
repoPath,
connectionId,
...options
}),
ghRepoExecOptions: (context: GitHubRepoContext) => context
}))
vi.mock('./client/lookup/branch-lookup-resolution', () => ({
resolvePRForBranchOutcome: mocks.resolve
}))
vi.mock('./client', async () => ({
getPRForBranchOutcome: (await import('./client/lookup/pr-for-branch-outcome'))
.getPRForBranchOutcome,
getRepoSlug: vi.fn(async () => ({ owner: 'acme', repo: 'orca' })),
getGitHubPRLookupRateLimitBlock: vi.fn(async () => null),
createGitHubPullRequest: vi.fn()
}))
vi.mock('../gitlab/merge-requests/merge-request-lookup', () => ({
getProjectSlug: vi.fn(async () => null)
}))
import { getPRForBranchOutcome } from './client/lookup/pr-for-branch-outcome'
const outcome: PRRefreshOutcome = { kind: 'no-pr', fetchedAt: 1 }
beforeEach(() => {
vi.clearAllMocks()
mocks.resolve.mockReset()
__resetHostedReviewBranchCacheForTests()
mocks.acquire.mockResolvedValue(undefined)
})
afterEach(() => {
vi.unstubAllEnvs()
vi.useRealTimers()
})
describe('PR lookup coalescing', () => {
it('cleans up errors so later reads can recover', async () => {
mocks.resolve.mockRejectedValueOnce(new Error('network down')).mockResolvedValue(outcome)
const results = await Promise.all([
getPRForBranchOutcome('/repo', 'topic'),
getPRForBranchOutcome('/repo', 'topic')
])
expect(results.every((result) => result.kind === 'upstream-error')).toBe(true)
expect(mocks.resolve).toHaveBeenCalledTimes(1)
await expect(getPRForBranchOutcome('/repo', 'topic')).resolves.toEqual(outcome)
expect(mocks.resolve).toHaveBeenCalledTimes(2)
})
it.each([
{ label: 'native', connectionId: null, options: {} },
{ label: 'WSL', connectionId: null, options: { localGitExecOptions: { wslDistro: 'Ubuntu' } } },
{ label: 'SSH', connectionId: 'host / encoded', options: {} }
])(
'does not adopt a pre-creation no-PR read after $label invalidation',
async ({ connectionId, options }) => {
const created: PRRefreshOutcome = { kind: 'found', pr: makePR({ number: 13 }), fetchedAt: 2 }
const createdReview = hostedReviewInfoFromGitHubPRInfo(created.pr)
let finish: (value: PRRefreshOutcome) => void = () => {}
mocks.resolve
.mockImplementationOnce(
() =>
new Promise<PRRefreshOutcome>((resolve) => {
finish = resolve
})
)
.mockResolvedValue(created)
const executionHostId = getRepoExecutionHostId({ connectionId })
const identity = { repoPath: '/repo', executionHostId, branch: 'topic', ...options }
const lookup = async () => {
const result = await getPRForBranchOutcome(
'/repo',
'topic',
null,
connectionId,
null,
options
)
if (result.kind === 'upstream-error') {
throw new Error(result.message)
}
return result.kind === 'found' ? hostedReviewInfoFromGitHubPRInfo(result.pr) : null
}
const beforeCreation = withHostedReviewBranchCache(identity, { headOid: null }, lookup)
await vi.waitFor(() => expect(mocks.resolve).toHaveBeenCalledTimes(1))
invalidateHostedReviewBranchCache('/repo', executionHostId)
const afterCreation = withHostedReviewBranchCache(identity, { headOid: null }, lookup)
try {
await vi.waitFor(() => expect(mocks.resolve).toHaveBeenCalledTimes(2))
await expect(afterCreation).resolves.toEqual(createdReview)
} finally {
finish(outcome)
await Promise.all([beforeCreation, afterCreation])
}
await expect(
withHostedReviewBranchCache(identity, { headOid: null }, lookup)
).resolves.toEqual(createdReview)
expect(mocks.resolve).toHaveBeenCalledTimes(2)
}
)
})
it.each([true, false])(
'explicit host intent replaces the probe without losing its successor (old first: %s)',
async (oldFirst) => {
vi.useFakeTimers({ toFake: ['Date'] })
vi.setSystemTime(1_000)
const old = Promise.withResolvers<PRRefreshOutcome>()
const fresh = Promise.withResolvers<PRRefreshOutcome>()
const merged: PRRefreshOutcome = {
kind: 'found',
pr: makePR({ state: 'merged' }),
fetchedAt: 2
}
mocks.resolve.mockReturnValueOnce(old.promise).mockReturnValueOnce(fresh.promise)
const before = getPRForBranchOutcome('/repo', 'click', null, null, null)
await vi.waitFor(() => expect(mocks.resolve).toHaveBeenCalledTimes(1))
const intent = { userRefresh: true }
const click = getPRForBranchOutcome('/repo', 'click', null, null, null, intent)
try {
await vi.waitFor(() => expect(mocks.resolve).toHaveBeenCalledTimes(2))
if (oldFirst) {
old.resolve(outcome)
await before
}
const ordinary = getPRForBranchOutcome('/repo', 'click', null, null, null)
fresh.resolve(merged)
expect(await click).toEqual(merged)
expect(await ordinary).toEqual(merged)
expect(mocks.resolve).toHaveBeenCalledTimes(2)
} finally {
old.resolve(outcome)
fresh.resolve(merged)
await Promise.all([before, click])
}
}
)
it('an explicit hosted GitHub read reaches a new lower probe through the real forge adapter', async () => {
const { readHostedReviewForBranch } = await import('../source-control/hosted-review')
const old = Promise.withResolvers<PRRefreshOutcome>()
const merged: PRRefreshOutcome = {
kind: 'found',
pr: makePR({ state: 'merged', headSha: 'new-head' }),
fetchedAt: 2
}
mocks.resolve.mockReturnValueOnce(old.promise).mockResolvedValueOnce(merged)
const before = getPRForBranchOutcome('/repo', 'adapter', null, null, null)
await vi.waitFor(() => expect(mocks.resolve).toHaveBeenCalledTimes(1))
const click = readHostedReviewForBranch({
repoPath: '/repo',
executionHostId: 'local',
branch: 'adapter',
userRefresh: true
})
try {
await vi.waitFor(() => expect(mocks.resolve).toHaveBeenCalledTimes(2))
expect((await click).review).toMatchObject({ state: 'merged' })
} finally {
old.resolve(outcome)
await Promise.all([before, click])
}
})
@@ -1,102 +0,0 @@
import type * as GitExecFileModule from '../git/command-runner/git-exec-file'
import type * as GhExecFileModule from '../git/command-runner/hosted-cli/gh-exec-file'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import type * as GithubApiRepositoryModule from './github-api-repository'
import type * as GitHubEnterpriseRepositoryModule from './github-enterprise-repository'
const { clientMocks, moduleMocks } = await vi.hoisted(async () => {
const moduleMocks = await import('./client-test-mocks')
return { clientMocks: moduleMocks.createGitHubClientMocks(), moduleMocks }
})
vi.mock('./gh-utils', () => moduleMocks.ghUtilsModuleMock(clientMocks))
vi.mock('../git/command-runner/git-exec-file', async (importOriginal) => ({
...(await importOriginal<typeof GitExecFileModule>()),
gitExecFileAsync: clientMocks.gitExecFileAsyncMock
}))
vi.mock('../git/command-runner/hosted-cli/gh-exec-file', async (importOriginal) => ({
...(await importOriginal<typeof GhExecFileModule>()),
ghExecFileAsync: clientMocks.ghExecFileAsyncMock
}))
vi.mock('./local-git-config-signature', () =>
moduleMocks.localGitConfigSignatureModuleMock(clientMocks)
)
vi.mock('./github-enterprise-repository', async (importOriginal) =>
moduleMocks.githubEnterpriseRepositoryModuleMock(
await importOriginal<typeof GitHubEnterpriseRepositoryModule>()
)
)
vi.mock('./rate-limit', () => moduleMocks.rateLimitModuleMock(clientMocks))
vi.mock('./github-api-repository', async (importOriginal) =>
moduleMocks.githubApiRepositoryModuleMock(
clientMocks,
await importOriginal<typeof GithubApiRepositoryModule>()
)
)
import { getGitHubPRLookupRateLimitBlock } from './client/lookup/pr-lookup-rate-limit'
import { _resetOwnerRepoCache } from './gh-utils'
import { resetOriginRepositoryCache } from './client-test-harness'
import type { RateLimitGuardResult } from './client-test-mocks'
const {
execFileAsyncMock,
ghExecFileAsyncMock,
getOwnerRepoMock,
getRemoteUrlForRepoMock,
gitExecFileAsyncMock,
getRateLimitMock,
rateLimitGuardMock,
noteRateLimitSpendMock
} = clientMocks
describe('getGitHubPRLookupRateLimitBlock', () => {
beforeEach(() => {
resetOriginRepositoryCache()
execFileAsyncMock.mockReset()
ghExecFileAsyncMock.mockReset()
getOwnerRepoMock.mockReset()
getOwnerRepoMock.mockResolvedValue({ owner: 'acme', repo: 'widgets' })
getRemoteUrlForRepoMock.mockReset()
gitExecFileAsyncMock.mockReset()
getRateLimitMock.mockReset()
getRateLimitMock.mockResolvedValue(undefined)
rateLimitGuardMock.mockReset()
rateLimitGuardMock.mockReturnValue({ blocked: false })
noteRateLimitSpendMock.mockReset()
_resetOwnerRepoCache()
})
it('reports a block when either lookup bucket is exhausted', async () => {
rateLimitGuardMock.mockImplementation(((bucket: string) =>
bucket === 'graphql'
? { blocked: true, remaining: 4, limit: 5000, resetAt: 1_800_000_000 }
: { blocked: false }) as () => RateLimitGuardResult)
await expect(getGitHubPRLookupRateLimitBlock('/repo-root')).resolves.toEqual({
resetAt: 1_800_000_000
})
})
it('reports the latest reset when both lookup buckets are exhausted', async () => {
rateLimitGuardMock.mockImplementation(((bucket: string) => ({
blocked: true,
remaining: 4,
limit: 5000,
// Why: core resets first, so returning it would retry into graphql's block.
resetAt: bucket === 'core' ? 1_800_000_000 : 1_800_003_600
})) as () => RateLimitGuardResult)
await expect(getGitHubPRLookupRateLimitBlock('/repo-root')).resolves.toEqual({
resetAt: 1_800_003_600
})
})
it('fails open when the exempt rate-limit probe itself fails', async () => {
getRateLimitMock.mockRejectedValue(new Error('probe offline'))
await expect(getGitHubPRLookupRateLimitBlock('/repo-root')).resolves.toBeNull()
})
})
@@ -1,91 +0,0 @@
import type * as GitExecFileModule from '../git/command-runner/git-exec-file'
import type * as GhExecFileModule from '../git/command-runner/hosted-cli/gh-exec-file'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import type * as GithubApiRepositoryModule from './github-api-repository'
import type * as GitHubEnterpriseRepositoryModule from './github-enterprise-repository'
const { clientMocks, moduleMocks } = await vi.hoisted(async () => {
const moduleMocks = await import('./client-test-mocks')
return { clientMocks: moduleMocks.createGitHubClientMocks(), moduleMocks }
})
vi.mock('./gh-utils', () => moduleMocks.ghUtilsModuleMock(clientMocks))
vi.mock('../git/command-runner/git-exec-file', async (importOriginal) => ({
...(await importOriginal<typeof GitExecFileModule>()),
gitExecFileAsync: clientMocks.gitExecFileAsyncMock
}))
vi.mock('../git/command-runner/hosted-cli/gh-exec-file', async (importOriginal) => ({
...(await importOriginal<typeof GhExecFileModule>()),
ghExecFileAsync: clientMocks.ghExecFileAsyncMock
}))
vi.mock('./local-git-config-signature', () =>
moduleMocks.localGitConfigSignatureModuleMock(clientMocks)
)
vi.mock('./github-enterprise-repository', async (importOriginal) =>
moduleMocks.githubEnterpriseRepositoryModuleMock(
await importOriginal<typeof GitHubEnterpriseRepositoryModule>()
)
)
vi.mock('./rate-limit', () => moduleMocks.rateLimitModuleMock(clientMocks))
vi.mock('./github-api-repository', async (importOriginal) =>
moduleMocks.githubApiRepositoryModuleMock(
clientMocks,
await importOriginal<typeof GithubApiRepositoryModule>()
)
)
import { mergePR } from './client/merge/merge-pr'
import {
installIndependentCheckFixtures,
resetGraphQLRateLimitGuardMocks
} from './client-test-harness'
const metadataRequests = vi.fn()
const unexpectedChecks: string[][] = []
afterEach(() => expect(unexpectedChecks).toEqual([]))
describe('GitHub GraphQL rate-limit guard', () => {
beforeEach(() => {
resetGraphQLRateLimitGuardMocks(clientMocks)
installIndependentCheckFixtures(clientMocks, metadataRequests, unexpectedChecks)
})
afterEach(() => vi.restoreAllMocks())
const validStackHeadSha = 'a'.repeat(40)
const validStackBaseSha = 'b'.repeat(40)
it('never falls back to legacy merge after an async stack merge transport failure', async () => {
metadataRequests
.mockResolvedValueOnce({
stdout: JSON.stringify({
number: 202,
state: 'open',
head: { ref: 'stack/api', sha: validStackHeadSha },
base: { ref: 'stack/models', sha: 'models-sha' },
stack: {
number: 51,
position: 2,
size: 2,
base: { ref: 'main', sha: validStackBaseSha }
}
})
})
.mockResolvedValueOnce({
stdout: JSON.stringify({ data: { repository: { mergeQueue: null } } })
})
.mockRejectedValueOnce(new Error('socket closed after request submission'))
await expect(
mergePR('/repo-root', 202, 'squash', undefined, {
owner: 'stablyai',
repo: 'orca',
host: 'github.com'
})
).resolves.toEqual({ ok: false, error: 'socket closed after request submission' })
expect(
metadataRequests.mock.calls.some(([args]) => args[0] === 'pr' && args[1] === 'merge')
).toBe(false)
})
})
-49
View File
@@ -1,49 +0,0 @@
export {
_getMergeQueueCacheSizeForTests,
_resetMergeQueueCacheForTests
} from './client/detect/repository-merge-metadata-cache'
export { _resetPRStackSummaryCacheForTests } from './client/lookup/pr-stack-summary-cache'
export {
_getTrackedUpstreamBranchCacheSizesForTests,
__resetTrackedUpstreamBranchCacheForTests
} from './client/lookup/tracked-upstream-cache'
export { addPRReviewComment, addPRReviewCommentReply } from './client/create/add-pr-review-comment'
export { __resetOrcaStarCheckForTests, checkOrcaStarred, starOrca } from './client/fetch/orca-star'
export { countWorkItems } from './client/list/count-work-items'
export { createGitHubPullRequest } from './client/create/create-github-pull-request'
export { getAuthenticatedViewer } from './client/fetch/authenticated-viewer'
export { getGitHubPRLookupRateLimitBlock } from './client/lookup/pr-lookup-rate-limit'
export { getPRCheckDetails } from './client/check/get-pr-check-details'
export { getPRChecks } from './client/check/get-pr-checks'
export { getPRComments } from './client/fetch/get-pr-comments'
export { getPRForBranch } from './client/lookup/get-pr-for-branch'
export { getPRForBranchOutcome } from './client/lookup/pr-for-branch-outcome'
export {
getPullRequestPushTarget,
type PullRequestPushTarget
} from './client/lookup/pull-request-push-target'
export { getRepoSlug, getRepoUpstream } from './client/fetch/repo-slug-upstream'
export {
getWorkItem,
getWorkItemByOwnerRepo,
getWorkItemWithRepository
} from './client/fetch/get-work-item'
export { listWorkItems } from './client/list/list-work-items'
export { mergePR } from './client/merge/merge-pr'
export { removePRReviewers, requestPRReviewers } from './client/update/pr-reviewers'
export { rerunPRChecks } from './client/check/rerun-pr-checks'
export { resolveReviewThread } from './client/update/resolve-review-thread'
export { setPRAutoMerge } from './client/merge/pr-auto-merge'
export { setPRCommentReaction } from './client/update/pr-comment-reaction'
export { setPRFileViewed } from './client/update/pr-file-viewed'
export { updatePRDetails, updatePRTitle } from './client/update/pr-details'
export { markPRDraft, markPRReadyForReview } from './client/update/pr-ready'
export { updatePRState } from './client/update/pr-state'
export type { GitHubPRBranchLookupOptions } from './client/lookup/pull-request-lookup-data'
export type { MainWorkItem } from './client/map/work-item-field-coercion'
export { _resetOwnerRepoCache } from './gh-utils'
export { getIssue, listIssues } from './issues'
export { createIssue } from './issue-create'
export { updateIssue } from './issue-update'
export { addIssueComment } from './issue-comment'
export { listLabels, listAssignableUsers } from './issue-field-options'
@@ -1,4 +1,4 @@
import type { PRCheckRunDetails } from '../../../../shared/github/check-types'
import type { PRCheckRunDetails } from '../../../../shared/github/checks/check-types'
export function nullableString(value: unknown): string | null {
return typeof value === 'string' && value.length > 0 ? value : null
}
@@ -17,11 +17,13 @@ vi.mock('@orca/process-host/process-tree-termination', () => ({
signalProcessTree: signalMock,
forceTerminateProcessTree: forceMock
}))
vi.mock('../../github-api-repository', () => ({ resolveGitHubRepoExecution: resolverMock }))
vi.mock('../../repositories/github-api-repository', () => ({
resolveGitHubRepoExecution: resolverMock
}))
import { getPRCheckDetails } from './get-pr-check-details'
import { prCheckLogTailCache } from './check-job-log-tails'
import { clearGhCapabilityStateForTests } from '../../gh-capability-state'
import { clearGhAccountTokenCacheForTests } from '../../gh-account-token'
import { clearGhCapabilityStateForTests } from '../../accounts/gh-capability-state'
import { clearGhAccountTokenCacheForTests } from '../../accounts/gh-account-token'
const repo = { owner: 'acme', repo: 'widgets' }
const children: ChildProcess[] = []
@@ -143,28 +145,6 @@ describe('check details existing admission ownership', () => {
expect(logChildren).toHaveLength(5)
})
it('prevents the jobs stage after a swallowed annotation PID error until close', async () => {
const controller = new AbortController()
const result = getPRCheckDetails(
'/folder',
{ checkRunId: 1, workflowRunId: 2 },
undefined,
{},
controller.signal
)
const rejected = expect(result).rejects.toMatchObject({ name: 'AbortError' })
await vi.advanceTimersByTimeAsync(0)
completeFakeSpawn(children[0], { stdout: '{"name":"loaded"}' })
await vi.advanceTimersByTimeAsync(0)
children[1].emit('error', new Error('private-output'))
await vi.advanceTimersByTimeAsync(0)
expect(children).toHaveLength(2)
controller.abort()
await rejected
expect(children).toHaveLength(2)
expect(JSON.stringify(vi.mocked(console.warn).mock.calls)).not.toContain('private-output')
})
it('retains all four creator permits through joined help and later token continuations after departure', async () => {
const options: GhExecOptions = {
cwd: '/folder',
@@ -6,8 +6,8 @@ import {
createFakeSpawnedChild
} from '../../../../shared/__fixtures__/fake-spawned-child'
import type { GhExecOptions } from '../../../git/command-runner/hosted-cli/gh-exec-file'
import { clearGhAccountTokenCacheForTests } from '../../gh-account-token'
import { clearGhCapabilityStateForTests } from '../../gh-capability-state'
import { clearGhAccountTokenCacheForTests } from '../../accounts/gh-account-token'
import { clearGhCapabilityStateForTests } from '../../accounts/gh-capability-state'
import { getPRCheckDetails } from './get-pr-check-details'
import { prCheckLogTailCache } from './check-job-log-tails'
@@ -16,7 +16,9 @@ const { spawnMock, resolverMock } = vi.hoisted(() => ({
resolverMock: vi.fn()
}))
vi.mock('node:child_process', async (original) => ({ ...(await original()), spawn: spawnMock }))
vi.mock('../../github-api-repository', () => ({ resolveGitHubRepoExecution: resolverMock }))
vi.mock('../../repositories/github-api-repository', () => ({
resolveGitHubRepoExecution: resolverMock
}))
vi.mock('@orca/process-host', async (original) => {
const actual = await original<typeof ProcessHostModule>()
return {
@@ -10,7 +10,8 @@ import * as capture from '../../../git/command-runner/process-capture/exec-file-
import { RpcDispatcher } from '../../../runtime/rpc/dispatcher'
import { GITHUB_PULL_REQUEST_METHODS } from '../../../runtime/rpc/methods/source-control/github/github-pull-request-methods'
import type { OrcaRuntimeService } from '../../../runtime/orca-runtime'
import * as ghUtils from '../../gh-utils'
import * as ghUtilsOwner1 from '../../execution/github-operation-slots'
import * as ghUtilsOwner2 from '../../../git/command-runner/hosted-cli/gh-exec-file'
import { getPRCheckDetails } from './get-pr-check-details'
import { prCheckLogTailCache } from './check-job-log-tails'
@@ -22,7 +23,9 @@ const { spawnMock, resolverMock, signalMock, forceMock, captureCap } = vi.hoiste
captureCap: { bytes: 4096, metadata: false }
}))
vi.mock('node:child_process', async (original) => ({ ...(await original()), spawn: spawnMock }))
vi.mock('../../github-api-repository', () => ({ resolveGitHubRepoExecution: resolverMock }))
vi.mock('../../repositories/github-api-repository', () => ({
resolveGitHubRepoExecution: resolverMock
}))
vi.mock('@orca/process-host/process-tree-termination', () => ({
signalProcessTree: signalMock,
forceTerminateProcessTree: forceMock
@@ -88,27 +91,27 @@ function serializeDiagnostics(value: unknown): string {
)
}
async function verifyAdmissionIsRestored(): Promise<void> {
await Promise.all(Array.from({ length: 4 }, () => ghUtils.acquire()))
await Promise.all(Array.from({ length: 4 }, () => ghUtilsOwner1.acquire()))
let fifthStarted = false
const fifth = ghUtils.acquire().then(() => {
const fifth = ghUtilsOwner1.acquire().then(() => {
fifthStarted = true
})
await vi.advanceTimersByTimeAsync(0)
expect(fifthStarted).toBe(false)
ghUtils.release()
ghUtilsOwner1.release()
await fifth
for (let i = 0; i < 4; i++) {
ghUtils.release()
ghUtilsOwner1.release()
}
}
async function verifyAdmissionIsRetained(): Promise<void> {
await Promise.all(Array.from({ length: 3 }, () => ghUtils.acquire()))
await Promise.all(Array.from({ length: 3 }, () => ghUtilsOwner1.acquire()))
const controller = new AbortController()
let started = false
const waiting = ghUtils.acquire(controller.signal).then(
const waiting = ghUtilsOwner1.acquire(controller.signal).then(
() => {
started = true
ghUtils.release()
ghUtilsOwner1.release()
},
() => {}
)
@@ -117,7 +120,7 @@ async function verifyAdmissionIsRetained(): Promise<void> {
controller.abort()
await waiting
for (let i = 0; i < 3; i++) {
ghUtils.release()
ghUtilsOwner1.release()
}
}
@@ -155,7 +158,7 @@ describe('real capture and gh diagnostics through check details RPC', () => {
'keeps %s raw capture fields internal and restores actual admission',
async (mode) => {
const captureSpy = vi.spyOn(capture, 'execFileCaptureToTermination')
const ghSpy = vi.spyOn(ghUtils, 'ghExecFileAsync')
const ghSpy = vi.spyOn(ghUtilsOwner2, 'ghExecFileAsync')
if (mode === 'runner-timeout') {
resolverMock.mockResolvedValue({
ownerRepo: { owner: 'acme', repo: 'widgets' },
@@ -1,9 +1,11 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import fixture from './__fixtures__/gh-api-unknown-escape-flag.json'
import { startCheckJobLogDownload } from './check-job-log-download'
import type { GhCaptureLifetimeHooks } from '../../../git/command-runner/hosted-cli/gh-capture-lifetime'
const { ghMock } = vi.hoisted(() => ({ ghMock: vi.fn() }))
vi.mock('../../gh-utils', () => ({ ghExecFileAsync: ghMock }))
vi.mock('../../../git/command-runner/hosted-cli/gh-exec-file', () => ({
ghExecFileAsync: ghMock
}))
const diagnostic = 'unknown flag: --allow-escape-sequences'
const repo = { owner: 'acme', repo: 'widgets' }
function download(controller = new AbortController(), deadlineAt = Date.now() + 25_000) {
@@ -36,92 +38,6 @@ describe('stateless captured diagnostic negotiation', () => {
vi.useRealTimers()
vi.restoreAllMocks()
})
it.each([
fixture.stderr,
Buffer.from(fixture.stderr),
diagnostic,
`${diagnostic}\n`,
fixture.stderr.replaceAll('\n', '\r\n'),
fixture.stderr.replaceAll('\n', '\r'),
`USAGE changed\n${diagnostic}\nflags reordered`,
`help\n${diagnostic}`,
`${'changed usage\n'.repeat(500)}${diagnostic}`,
`${diagnostic}\n${diagnostic}`,
`HTTP 403\nDEBUG record\necho other text\n${diagnostic}\nauthentication failed`,
`unknown flag: --other\n${diagnostic}\nindependent failure`
])('allows a complete diagnostic independently of surrounding output %#', async (stderr) => {
ghMock
.mockRejectedValueOnce(rejected({ stderr, stdout: 'nonempty output is irrelevant' }))
.mockResolvedValueOnce({ stdout: 'retrieved', stderr: '' })
await expect(download().result).resolves.toBe('retrieved')
expect(ghMock).toHaveBeenCalledTimes(2)
expect(ghMock.mock.calls[1][0]).toEqual(['api', 'repos/acme/widgets/actions/jobs/77/logs'])
expect(ghMock.mock.calls[1][1]).toEqual(ghMock.mock.calls[0][1])
})
it.each([
`prefix ${diagnostic}`,
`${diagnostic} suffix`,
` ${diagnostic}`,
`${diagnostic} `,
diagnostic.toUpperCase(),
`\x1b[31m${diagnostic}`,
`${diagnostic}\x00`,
'unknown flag: --other',
`${diagnostic}-suffix`,
`Error: ${diagnostic}`,
`$ printf '${diagnostic}'`,
`echo ${diagnostic}`,
`HTTP 403: ${diagnostic}`,
`DEBUG ${diagnostic}`,
JSON.stringify({ error: diagnostic }),
`"${diagnostic}"`,
'authentication failed',
'rate limit exceeded',
'network unavailable'
])('rejects framing or different text on the candidate line %#', async (stderr) => {
ghMock.mockRejectedValue(rejected({ stderr }))
await expect(download().result).rejects.toBeDefined()
expect(ghMock).toHaveBeenCalledOnce()
})
it.each([
{ code: 0 },
{ code: undefined },
{ code: null },
{ code: '1' },
{ code: Number.NaN },
{ code: Infinity },
{ code: -Infinity },
{ stderr: undefined },
{ stderr: {} },
{ stderr: null },
{ stderr: '', stdout: diagnostic, message: diagnostic, cause: diagnostic },
{ outputTruncated: true, killed: false, signal: null },
{ code: 'ERR_CHILD_PROCESS_STDIO_MAXBUFFER' },
{ code: 'ENOBUFS' },
{ name: 'AbortError' },
{ code: 'ABORT_ERR' },
{ code: 'ERR_ABORTED' },
{ code: 'ETIMEDOUT' },
{ timedOut: true },
{ killed: true },
{ signal: 'SIGTERM' },
{ signal: false }
])('honors typed exclusions before exact stderr %#', async (metadata) => {
ghMock.mockRejectedValue(rejected(metadata))
await expect(download().result).rejects.toBeDefined()
expect(ghMock).toHaveBeenCalledOnce()
})
it('does not read inherited stderr or a getter as capture evidence', async () => {
const inherited = Object.assign(Object.create({ stderr: diagnostic }), { code: 1 })
ghMock.mockRejectedValueOnce(inherited)
await expect(download().result).rejects.toBeDefined()
const getter = vi.fn(() => diagnostic)
const error = Object.defineProperty({ code: 1 }, 'stderr', { get: getter })
ghMock.mockRejectedValueOnce(error)
await expect(download().result).rejects.toBeDefined()
expect(getter).not.toHaveBeenCalled()
expect(ghMock).toHaveBeenCalledTimes(2)
})
it('waits positive first termination and starts nothing after cancellation', async () => {
let close = (): void => {}
@@ -143,19 +59,7 @@ describe('stateless captured diagnostic negotiation', () => {
await call.terminated
expect(ghMock).toHaveBeenCalledOnce()
})
it('does not reset the original deadline for fallback and starts the next job flagged', async () => {
ghMock
.mockImplementationOnce(async () => {
vi.setSystemTime(Date.now() + 4_000)
throw rejected({})
})
.mockResolvedValueOnce({ stdout: 'ok', stderr: '' })
await expect(download().result).resolves.toBe('ok')
expect(ghMock.mock.calls[1][1].timeout).toBe(21_000)
ghMock.mockResolvedValueOnce({ stdout: 'next', stderr: '' })
await expect(download().result).resolves.toBe('next')
expect(ghMock.mock.calls[2][0]).toContain('--allow-escape-sequences')
})
it('ends at the second failure and rejects expiry or pre-abort without a child', async () => {
ghMock.mockRejectedValue(rejected({}))
await expect(download().result).rejects.toBeDefined()
@@ -1,8 +1,11 @@
import { ghExecFileAsync } from '../../gh-utils'
import type { GitHubApiRepository } from '../../github-api-repository'
import { ghExecFileAsync } from '../../../git/command-runner/hosted-cli/gh-exec-file'
import type { GitHubRepositoryIdentity as GitHubApiRepository } from '../../../../shared/github/repositories/repository-identity'
import type { GhExecOptions } from '../github-exec-scope'
import { createGhLogicalLifetime } from '../../../git/command-runner/hosted-cli/gh-capture-lifetime'
import type { GhCaptureLifetimeHooks } from '../../../git/command-runner/hosted-cli/gh-capture-lifetime'
import {
createGhLogicalLifetime,
type GhCaptureLifetimeHooks
} from '../../../git/command-runner/hosted-cli/gh-capture-lifetime'
import { runCheckDetailsCall } from './check-details-admission-lifetime'
import { createCheckDetailsAbortError, waitForCheckDetailsResolution } from './check-details-abort'
@@ -1,7 +1,7 @@
import { normalizeGhAccountBinding } from '../../../../shared/github/account-binding'
import { normalizeGhAccountBinding } from '../../../../shared/github/accounts/account-binding'
import { parseWslPath } from '../../../wsl/filesystem/unc-path-access'
import { githubReadExecutionScope } from '../../github-read-execution-scope'
import type { GitHubApiRepository } from '../../github-api-repository'
import { codeHostReadExecutionScope as githubReadExecutionScope } from '../../../source-control/code-host-read-execution-scope'
import type { GitHubRepositoryIdentity as GitHubApiRepository } from '../../../../shared/github/repositories/repository-identity'
import type { GhExecOptions } from '../github-exec-scope'
function reusableHost(host: string): 'cloud' | 'enterprise' | null {
@@ -18,7 +18,9 @@ vi.mock('@orca/process-host/process-tree-termination', () => ({
signalProcessTree: signalMock,
forceTerminateProcessTree: forceMock
}))
vi.mock('../../github-api-repository', () => ({ resolveGitHubRepoExecution: resolverMock }))
vi.mock('../../repositories/github-api-repository', () => ({
resolveGitHubRepoExecution: resolverMock
}))
const children: ChildProcess[] = []
function spawn(): ChildProcess {
const child = createFakeSpawnedChild(children.length + 100)
@@ -1,73 +0,0 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import type { ChildProcess, SpawnOptions } from 'node:child_process'
import {
completeFakeSpawn,
createFakeSpawnedChild
} from '../../../../shared/__fixtures__/fake-spawned-child'
import { attachLogFixture, failedLogJob } from './check-job-log-test-fixture'
import { prCheckLogTailCache } from './check-job-log-tails'
import { clearGhCapabilityStateForTests } from '../../gh-capability-state'
import { clearGhAccountTokenCacheForTests } from '../../gh-account-token'
const { spawnMock } = vi.hoisted(() => ({ spawnMock: vi.fn() }))
vi.mock('node:child_process', async (original) => ({ ...(await original()), spawn: spawnMock }))
const children: ChildProcess[] = []
let raw = 'Error: retained diagnostic'
let active = 0
let peak = 0
function automaticSpawn(_program: string, argv: string[], _options: SpawnOptions): ChildProcess {
const child = createFakeSpawnedChild()
children.push(child)
active += 1
peak = Math.max(peak, active)
const user = argv[argv.indexOf('--user') + 1]
const stdout = argv.includes('--help') ? '--user' : argv[0] === 'auth' ? `bound-${user}` : raw
queueMicrotask(() => {
active -= 1
completeFakeSpawn(child, { stdout })
})
return child
}
describe('completed scoped excerpts through actual capture and runner', () => {
beforeEach(() => {
vi.useFakeTimers()
children.length = 0
active = 0
peak = 0
raw = 'Error: retained diagnostic'
spawnMock.mockReset().mockImplementation(automaticSpawn)
prCheckLogTailCache.clear()
clearGhCapabilityStateForTests()
clearGhAccountTokenCacheForTests()
vi.spyOn(console, 'warn').mockImplementation(() => {})
})
afterEach(async () => {
for (const child of children) {
child.emit('close', 0, null)
}
await vi.advanceTimersByTimeAsync(0)
expect(vi.getTimerCount()).toBe(0)
vi.useRealTimers()
vi.restoreAllMocks()
})
it('bounds owned encoded cache content with FIFO eviction and five-job payload', async () => {
raw = '🙂'.repeat(6000)
for (let id = 1; id <= 129; id += 1) {
await attachLogFixture([failedLogJob(id)])
}
expect(prCheckLogTailCache.size).toBe(128)
const tails = [...prCheckLogTailCache.values()].map((entry) => entry.logTail ?? '')
expect(tails.every((tail) => Buffer.byteLength(tail) <= 16 * 1024)).toBe(true)
expect(tails.reduce((bytes, tail) => bytes + Buffer.byteLength(tail), 0)).toBe(128 * 16 * 1024)
const jobs = Array.from({ length: 5 }, (_, i) => failedLogJob(i + 125))
await attachLogFixture(jobs)
expect(jobs.reduce((bytes, job) => bytes + Buffer.byteLength(job.logTail ?? ''), 0)).toBe(
80 * 1024
)
jobs[0].logTail = 'caller mutation'
await attachLogFixture([failedLogJob(125)])
expect(spawnMock).toHaveBeenCalledTimes(129)
await attachLogFixture([failedLogJob(1)])
expect(spawnMock).toHaveBeenCalledTimes(130)
})
})
@@ -1,7 +1,7 @@
import type { PRCheckRunDetails } from '../../../../shared/github/check-types'
import type { PRCheckRunDetails } from '../../../../shared/github/checks/check-types'
import { sliceCheckLogTail } from '../../../../shared/source-control/checks/logs/check-job-log-tail-slice'
import type { GitHubApiRepository } from '../../github-api-repository'
import { githubRepoIdentityKey } from '../../../../shared/github/repository-identity-key'
import type { GitHubRepositoryIdentity as GitHubApiRepository } from '../../../../shared/github/repositories/repository-identity'
import { githubRepoIdentityKey } from '../../../../shared/github/repositories/repository-identity-key'
import type { GhExecOptions } from './../github-exec-scope'
import { rethrowCheckDetailsAbort, waitForCheckDetailsResolution } from './check-details-abort'
import { assertCheckLogActive, startCheckJobLogDownload } from './check-job-log-download'
@@ -1,5 +1,5 @@
import type { PRCheckJob } from '../../../../shared/github/check-types'
import type { GitHubApiRepository } from '../../github-api-repository'
import type { PRCheckJob } from '../../../../shared/github/checks/check-types'
import type { GitHubRepositoryIdentity as GitHubApiRepository } from '../../../../shared/github/repositories/repository-identity'
import type { GhExecOptions } from '../github-exec-scope'
import { snapshotCheckJobLogExecution } from './check-job-log-execution'
import { attachFailedJobLogTails } from './check-job-log-tails'
@@ -1,17 +1,17 @@
import { beforeEach, expect, it, vi } from 'vitest'
const { exec } = vi.hoisted(() => ({ exec: vi.fn() }))
vi.mock('../git/command-runner/hosted-cli/gh-exec-file', async (original) => ({
vi.mock('../../../git/command-runner/hosted-cli/gh-exec-file', async (original) => ({
...(await original<Record<string, unknown>>()),
ghExecFileAsync: exec
}))
vi.mock('./rate-limit', () => ({
vi.mock('../../api/rate-limit', () => ({
noteRepositoryRateLimitSpend: vi.fn(),
repositoryRateLimitGuard: () => ({ blocked: false }),
spendsSharedGitHubComQuota: () => false
}))
import { readCompletePRCheckObservation } from './client/check/complete-pr-check-observation'
import { summarizePRCheckRollup } from '../../shared/source-control/checks/pr-check-status'
import { readCompletePRCheckObservation } from './complete-pr-check-observation'
import { summarizePRCheckRollup } from '../../../../shared/source-control/checks/pr-check-status'
const repository = { owner: 'acme', repo: 'widgets', host: 'github.com' }
const sha = 'a'.repeat(40)
@@ -1,4 +1,4 @@
import type { GitHubApiRepository } from '../../github-api-repository'
import type { GitHubRepositoryIdentity as GitHubApiRepository } from '../../../../shared/github/repositories/repository-identity'
import type { GhExecOptions } from '../github-exec-scope'
import { assertRateLimitBudget } from '../lookup/pr-lookup-rate-limit'
import { readGraphQLPRCheckRollup } from './pr-check-rollup'
@@ -1,5 +1,6 @@
import type * as GithubRepositoryIdentityModule from '../../repositories/github-repository-identity'
import type * as GithubEnterpriseRepositoryModule from '../../repositories/github-enterprise-repository'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import type * as GithubEnterpriseRepositoryModule from './github-enterprise-repository'
const { ghExecFileAsyncMock, getOwnerRepoMock, rateLimitGuardMock } = vi.hoisted(() => ({
ghExecFileAsyncMock: vi.fn(),
@@ -7,16 +8,21 @@ const { ghExecFileAsyncMock, getOwnerRepoMock, rateLimitGuardMock } = vi.hoisted
rateLimitGuardMock: vi.fn(() => ({ blocked: false }))
}))
vi.mock('./gh-utils', () => ({
execFileAsync: vi.fn(),
ghExecFileAsync: ghExecFileAsyncMock,
vi.mock('../../execution/github-legacy-command', () => ({
execFileAsync: vi.fn()
}))
vi.mock('../../../git/command-runner/hosted-cli/gh-exec-file', () => ({
ghExecFileAsync: ghExecFileAsyncMock
}))
vi.mock('../../repositories/github-repository-identity', async (importOriginal) => ({
...(await importOriginal<typeof GithubRepositoryIdentityModule>()),
githubRepoContext: (repoPath: string, connectionId?: string | null) => ({
repoPath,
connectionId: connectionId ?? null
}),
ghRepoExecOptions: (context: { repoPath: string }) => ({ cwd: context.repoPath }),
getOwnerRepo: getOwnerRepoMock,
getIssueOwnerRepo: vi.fn(),
getOwnerRepoForRemote: (
repoPath: string,
remoteName: string,
@@ -26,23 +32,29 @@ vi.mock('./gh-utils', () => ({
remoteName === 'origin'
? getOwnerRepoMock(repoPath, connectionId, localGitOptions)
: Promise.resolve(null),
extractExecError: vi.fn((err: unknown) => ({ stderr: String(err), stdout: '' })),
acquire: vi.fn(),
release: vi.fn(),
_resetOwnerRepoCache: vi.fn()
}))
vi.mock('../git/command-runner/git-exec-file', async (importOriginal) => ({
vi.mock('../../repositories/github-owner-repo-selection', () => ({
getOwnerRepo: getOwnerRepoMock
}))
vi.mock('../../execution/github-operation-slots', () => ({
acquire: vi.fn(),
release: vi.fn()
}))
vi.mock('../../../git/command-runner/git-exec-file', async (importOriginal) => ({
...(await importOriginal<Record<string, unknown>>()),
gitExecFileAsync: vi.fn()
}))
vi.mock('./github-enterprise-repository', async (importOriginal) => ({
vi.mock('../../repositories/github-enterprise-repository', async (importOriginal) => ({
...(await importOriginal<typeof GithubEnterpriseRepositoryModule>()),
isGitHubHostAuthenticated: vi.fn().mockResolvedValue(true)
}))
vi.mock('./rate-limit', () => ({
vi.mock('../../api/rate-limit', () => ({
rateLimitGuard: rateLimitGuardMock,
noteRateLimitSpend: vi.fn(),
repositoryRateLimitGuard: vi.fn(() => ({ blocked: false })),
@@ -50,8 +62,8 @@ vi.mock('./rate-limit', () => ({
spendsSharedGitHubComQuota: vi.fn(() => true)
}))
import { getPRCheckDetails } from './client/check/get-pr-check-details'
import { _resetOwnerRepoCache } from './gh-utils'
import { getPRCheckDetails } from './get-pr-check-details'
import { _resetOwnerRepoCache } from '../../repositories/github-repository-identity'
describe('getPRCheckDetails', () => {
beforeEach(() => {
@@ -1,10 +1,13 @@
import type { PRCheckRunDetails } from '../../../../shared/github/check-types'
import { ghExecFileAsync, acquire, release, type LocalGitExecOptions } from '../../gh-utils'
import { resolveGitHubRepoExecution, type GitHubApiRepository } from '../../github-api-repository'
import type { PRCheckRunDetails } from '../../../../shared/github/checks/check-types'
import { ghExecFileAsync } from '../../../git/command-runner/hosted-cli/gh-exec-file'
import { acquire, release } from '../../execution/github-operation-slots'
import type { LocalGitExecOptions } from '../../repositories/github-repository-identity'
import { resolveGitHubRepoExecution } from '../../repositories/github-api-repository'
import type { GitHubRepositoryIdentity as GitHubApiRepository } from '../../../../shared/github/repositories/repository-identity'
import {
GITHUB_CHECK_DETAILS_HOST_TIMEOUT_MS,
GITHUB_CHECK_DETAILS_TIMEOUT_MESSAGE
} from '../../../../shared/github/check-details-deadline'
} from '../../../../shared/github/checks/check-details-deadline'
import type { GhExecOptions } from './../github-exec-scope'
import {
nullableString,
+14 -10
View File
@@ -1,18 +1,22 @@
import { normalizePRCheckHeadSha } from '../../../../shared/github/pr-check-head'
import { normalizePRCheckHeadSha } from '../../../../shared/github/checks/pr-check-head'
import {
withGitHubRepoExecution,
githubRepoContext,
ghRepoExecOptions
} from '../../github-repository-identity'
ghRepoExecOptions,
type LocalGitExecOptions
} from '../../repositories/github-repository-identity'
import { runCoalescedProbe, type CoalescedProbes } from '../../../git/remotes/coalesced-probe'
import { githubReadExecutionScope } from '../../github-read-execution-scope'
import type { PRCheckDetail } from '../../../../shared/github/check-types'
import { GITHUB_WORK_ITEMS_SSH_REMOTE_REQUIRED_MESSAGE } from '../../../../shared/github/work-item-ssh-error'
import { ghExecFileAsync, acquire, release, type LocalGitExecOptions } from '../../gh-utils'
import { codeHostReadExecutionScope as githubReadExecutionScope } from '../../../source-control/code-host-read-execution-scope'
import type { PRCheckDetail } from '../../../../shared/github/checks/check-types'
import { GITHUB_WORK_ITEMS_SSH_REMOTE_REQUIRED_MESSAGE } from '../../../../shared/github/work-items/work-item-ssh-error'
import { ghExecFileAsync } from '../../../git/command-runner/hosted-cli/gh-exec-file'
import { acquire, release } from '../../execution/github-operation-slots'
import { extractExecError } from '../../../git/command-runner/process-capture/exec-error'
import { resolveGitHubRepoExecution, type GitHubApiRepository } from '../../github-api-repository'
import { mapCheckStatus, mapCheckConclusion } from '../../mappers'
import { noteRepositoryRateLimitSpend } from '../../rate-limit'
import { resolveGitHubRepoExecution } from '../../repositories/github-api-repository'
import type { GitHubRepositoryIdentity as GitHubApiRepository } from '../../../../shared/github/repositories/repository-identity'
import { mapCheckStatus, mapCheckConclusion } from '../../api/github-read-response'
import { noteRepositoryRateLimitSpend } from '../../api/rate-limit'
import type { GhExecOptions } from './../github-exec-scope'
import { assertRateLimitBudget } from './../lookup/pr-lookup-rate-limit'
import { PR_CHECKS_ROLLUP_QUERY, type GraphQLPRChecksResponse } from './pr-checks-graphql-query'
@@ -1,7 +1,8 @@
import { z } from 'zod'
import { ghExecFileAsync } from '../../gh-utils'
import { githubHostExecOptions, type GitHubApiRepository } from '../../github-api-repository'
import { noteRepositoryRateLimitSpend } from '../../rate-limit'
import { ghExecFileAsync } from '../../../git/command-runner/hosted-cli/gh-exec-file'
import { githubHostExecOptions } from '../../repositories/github-repository-host'
import type { GitHubRepositoryIdentity as GitHubApiRepository } from '../../../../shared/github/repositories/repository-identity'
import { noteRepositoryRateLimitSpend } from '../../api/rate-limit'
import type { GhExecOptions } from '../github-exec-scope'
import { isPendingApprovalSuiteWithoutRuns } from './pr-checks-response-mapping'
@@ -1,12 +1,13 @@
import { normalizePRCheckHeadSha } from '../../../../shared/github/pr-check-head'
import type { PRCheckDetail } from '../../../../shared/github/check-types'
import { githubRepositoryWebHost, type GitHubApiRepository } from '../../github-api-repository'
import { normalizePRCheckHeadSha } from '../../../../shared/github/checks/pr-check-head'
import type { PRCheckDetail } from '../../../../shared/github/checks/check-types'
import { githubRepositoryWebHost } from '../../repositories/github-repository-host'
import type { GitHubRepositoryIdentity as GitHubApiRepository } from '../../../../shared/github/repositories/repository-identity'
import {
mapCheckRunRESTStatus,
mapCheckRunRESTConclusion,
mapCommitStatusRESTStatus,
mapCommitStatusRESTConclusion
} from '../../mappers'
} from '../../api/github-read-response'
import type {
GraphQLPRChecksResponse,
GraphQLCheckRunContext,
@@ -1,10 +1,16 @@
import { withGitHubRepoExecution } from '../../github-repository-identity'
import type { GitHubRerunPRChecksResult } from '../../../../shared/github/check-types'
import { ghExecFileAsync, acquire, release, type LocalGitExecOptions } from '../../gh-utils'
import {
withGitHubRepoExecution,
type LocalGitExecOptions
} from '../../repositories/github-repository-identity'
import type { GitHubRerunPRChecksResult } from '../../../../shared/github/checks/check-types'
import { ghExecFileAsync } from '../../../git/command-runner/hosted-cli/gh-exec-file'
import { acquire, release } from '../../execution/github-operation-slots'
// Keep real error parsing available when tests mock gh-utils.
import { extractExecError } from '../../../git/command-runner/process-capture/exec-error'
import { classifyRerunChecksError } from '../../gh-error-classification'
import { resolveGitHubRepoExecution, type GitHubApiRepository } from '../../github-api-repository'
import { classifyRerunChecksError } from '../../api/gh-error-classification'
import { resolveGitHubRepoExecution } from '../../repositories/github-api-repository'
import type { GitHubRepositoryIdentity as GitHubApiRepository } from '../../../../shared/github/repositories/repository-identity'
import { getPRChecks } from './get-pr-checks'
import { parseActionsRunId } from './check-detail-field-mapping'
import { codeHostFailure, classifiedFailure } from '../../../../shared/code-host-failure-copy'
@@ -1,8 +1,9 @@
import { z } from 'zod'
import { extractExecError } from '../../../git/command-runner/process-capture/exec-error'
import { ghExecFileAsync } from '../../gh-utils'
import { githubHostExecOptions, type GitHubApiRepository } from '../../github-api-repository'
import { noteRepositoryRateLimitSpend } from '../../rate-limit'
import { ghExecFileAsync } from '../../../git/command-runner/hosted-cli/gh-exec-file'
import { githubHostExecOptions } from '../../repositories/github-repository-host'
import type { GitHubRepositoryIdentity as GitHubApiRepository } from '../../../../shared/github/repositories/repository-identity'
import { noteRepositoryRateLimitSpend } from '../../api/rate-limit'
import type { GhExecOptions } from '../github-exec-scope'
import { assertRateLimitBudget } from '../lookup/pr-lookup-rate-limit'
import { isPendingApprovalSuiteWithoutRuns } from './pr-checks-response-mapping'
@@ -1,15 +1,15 @@
import type { Mock } from 'vitest'
import { completePRCheckGraphQLResponse } from '../../shared/github/complete-pr-check-test-fixture'
import { completePRCheckGraphQLResponse } from '../../../shared/github/checks/complete-pr-check-test-fixture'
import type { GitHubClientMocks } from './client-test-mocks'
import { _resetOwnerRepoCache } from './gh-utils'
import { _resetMergeQueueCacheForTests } from './client/detect/repository-merge-metadata-cache'
import { _resetPRStackSummaryCacheForTests } from './client/lookup/pr-stack-summary-cache'
import { __resetTrackedUpstreamBranchCacheForTests } from './client/lookup/tracked-upstream-cache'
import { __resetPRConflictSummaryCachesForTests } from './conflict-summary'
import { resetMergedPRCommitMembershipCacheForTest } from './merged-pr-commit-membership'
import { __resetRepoDefaultBranchCacheForTests } from '../source-control/repo-default-branch'
import { _resetOriginGitHubApiRepositoryCache } from './github-api-repository'
import { _resetGitHubPRStackCacheForTests } from './github-pr-stack'
import { _resetOwnerRepoCache } from '../repositories/github-repository-identity'
import { _resetMergeQueueCacheForTests } from './detect/repository-merge-metadata-cache'
import { _resetPRStackSummaryCacheForTests } from './lookup/pr-stack-summary-cache'
import { __resetTrackedUpstreamBranchCacheForTests } from './lookup/tracked-upstream-cache'
import { __resetPRConflictSummaryCachesForTests } from '../pull-requests/conflicts/conflict-summary'
import { resetMergedPRCommitMembershipCacheForTest } from '../pull-requests/refresh/merged-pr-commit-membership'
import { __resetRepoDefaultBranchCacheForTests } from '../../source-control/repo-default-branch'
import { _resetOriginGitHubApiRepositoryCache } from '../repositories/github-api-repository-remote-probe'
import { _resetGitHubPRStackCacheForTests } from '../pull-requests/stacks/github-pr-stack'
export function resetOriginRepositoryCache(): void {
_resetOriginGitHubApiRepositoryCache()
@@ -1,9 +1,13 @@
import type * as GithubApiRepositoryRemoteProbeModule from '../repositories/github-api-repository-remote-probe'
import { vi, type Mock } from 'vitest'
import type { ClassifiedError } from '../../shared/classified-error'
import type { GitHubOwnerRepo } from '../../shared/github/pull-request-types'
import type * as GithubApiRepositoryModule from './github-api-repository'
import type * as GitHubEnterpriseRepositoryModule from './github-enterprise-repository'
import type { ghRepoExecOptions, githubRepoContext } from './github-repository-identity'
import type { ClassifiedError } from '../../../shared/classified-error'
import type { GitHubRepositoryIdentity as GitHubOwnerRepo } from '../../../shared/github/repositories/repository-identity'
import type * as GithubApiRepositoryModule from '../repositories/github-api-repository'
import type * as GitHubEnterpriseRepositoryModule from '../repositories/github-enterprise-repository'
import type {
ghRepoExecOptions,
githubRepoContext
} from '../repositories/github-repository-identity'
export type RateLimitGuardResult =
| { blocked: false }
@@ -65,25 +69,17 @@ export function createGitHubClientMocks(): GitHubClientMocks {
}
}
export type GhUtilsModuleMock = {
export function githubLegacyCommandModuleMock(mocks: GitHubClientMocks): {
execFileAsync: Mock
ghExecFileAsync: Mock
getOwnerRepo: Mock
getIssueOwnerRepo: Mock
getOwnerRepoForRemote: Mock
resolvePRRepositoryCandidates: Mock
getRemoteUrlForRepo: Mock
gitExecFileAsync: Mock
ghRepoExecOptions: Mock<typeof ghRepoExecOptions>
githubRepoContext: Mock<typeof githubRepoContext>
classifyGhError: (stderr: string) => ClassifiedError
parseGitHubOwnerRepo: (remoteUrl: string) => GitHubOwnerRepo | null
acquire: Mock
release: Mock
_resetOwnerRepoCache: Mock
} {
return {
execFileAsync: mocks.execFileAsyncMock
}
}
export function ghUtilsModuleMock(mocks: GitHubClientMocks): GhUtilsModuleMock {
export function githubHostedCliModuleMock(mocks: GitHubClientMocks): {
ghExecFileAsync: Mock
} {
const ghExecFileAsync = vi.fn(async (...args: unknown[]) => {
const result = await mocks.ghExecFileAsyncMock(...args)
const argv = args[0]
@@ -101,16 +97,40 @@ export function ghUtilsModuleMock(mocks: GitHubClientMocks): GhUtilsModuleMock {
return result
})
return {
execFileAsync: mocks.execFileAsyncMock,
ghExecFileAsync,
ghExecFileAsync
}
}
export function githubOwnerSelectionModuleMock(mocks: GitHubClientMocks): {
getOwnerRepo: Mock
resolvePRRepositoryCandidates: Mock
} {
return {
getOwnerRepo: mocks.getOwnerRepoMock,
getIssueOwnerRepo: mocks.getIssueOwnerRepoMock,
resolvePRRepositoryCandidates: mocks.resolvePRRepositoryCandidatesMock
}
}
export function githubRepositoryIdentityModuleMock(mocks: GitHubClientMocks): {
getOwnerRepoForRemote: Mock
getRemoteUrlForRepo: Mock
ghRepoExecOptions: Mock<typeof ghRepoExecOptions>
githubRepoContext: Mock<typeof githubRepoContext>
_resetOwnerRepoCache: Mock
} {
return {
getOwnerRepoForRemote: mocks.getOwnerRepoForRemoteMock,
resolvePRRepositoryCandidates: mocks.resolvePRRepositoryCandidatesMock,
getRemoteUrlForRepo: mocks.getRemoteUrlForRepoMock,
gitExecFileAsync: mocks.gitExecFileAsyncMock,
ghRepoExecOptions: mocks.ghRepoExecOptionsMock,
githubRepoContext: mocks.githubRepoContextMock,
_resetOwnerRepoCache: vi.fn()
}
}
export function githubErrorClassificationModuleMock(): {
classifyGhError: (stderr: string) => ClassifiedError
} {
return {
classifyGhError: (stderr: string) => {
const lower = stderr.toLowerCase()
if (lower.includes('not found') || stderr.includes('HTTP 404')) {
@@ -123,14 +143,28 @@ export function ghUtilsModuleMock(mocks: GitHubClientMocks): GhUtilsModuleMock {
return { type: 'permission_denied', message: stderr }
}
return { type: 'unknown', message: stderr }
},
}
}
}
export function githubRemoteIdentityParsingModuleMock(): {
parseGitHubOwnerRepo: (remoteUrl: string) => GitHubOwnerRepo | null
} {
return {
parseGitHubOwnerRepo: (remoteUrl: string) => {
const match = remoteUrl.trim().match(/github\.com[:/]([^/]+)\/([^/]+?)(?:\.git)?$/)
return match ? { owner: match[1], repo: match[2] } : null
},
}
}
}
export function githubOperationSlotsModuleMock(mocks: GitHubClientMocks): {
acquire: Mock
release: Mock
} {
return {
acquire: mocks.acquireMock,
release: mocks.releaseMock,
_resetOwnerRepoCache: vi.fn()
release: mocks.releaseMock
}
}
@@ -210,6 +244,27 @@ export function githubApiRepositoryModuleMock(
connectionId?: string | null,
localGitOptions?: unknown
) => mocks.resolvePRRepositoryCandidatesMock(repoPath, connectionId, localGitOptions),
getIssueGitHubApiRepository: async (repoPath: string, connectionId?: string | null) => {
const slug = await mocks.getIssueOwnerRepoMock(repoPath, connectionId)
return slug ? { host: 'github.com', ...slug } : slug
},
resolveIssueGitHubApiRepositorySource: async (
repoPath: string,
_preference: unknown,
connectionId?: string | null
) => {
const slug = await mocks.getIssueOwnerRepoMock(repoPath, connectionId)
return { source: slug ? { host: 'github.com', ...slug } : slug, fellBack: false }
}
}
}
export function githubApiRepositoryRemoteProbeModuleMock(
mocks: GitHubClientMocks,
actual: typeof GithubApiRepositoryRemoteProbeModule
): typeof GithubApiRepositoryRemoteProbeModule {
return {
...actual,
getGitHubApiRepositoryForRemote: (
repoPath: string,
remoteName: string,
@@ -230,18 +285,6 @@ export function githubApiRepositoryModuleMock(
const slug =
fromRemote ?? (await mocks.getOwnerRepoMock(repoPath, connectionId, localGitOptions))
return slug ? { host: 'github.com', ...slug } : slug
},
getIssueGitHubApiRepository: async (repoPath: string, connectionId?: string | null) => {
const slug = await mocks.getIssueOwnerRepoMock(repoPath, connectionId)
return slug ? { host: 'github.com', ...slug } : slug
},
resolveIssueGitHubApiRepositorySource: async (
repoPath: string,
_preference: unknown,
connectionId?: string | null
) => {
const slug = await mocks.getIssueOwnerRepoMock(repoPath, connectionId)
return { source: slug ? { host: 'github.com', ...slug } : slug, fellBack: false }
}
}
}
@@ -1,16 +1,14 @@
import { githubBodyMediaHeaders } from '../../body-image-urls'
import { githubBodyMediaHeaders } from '../../attachments/body-image-urls'
import type {
GitHubCommentResult,
GitHubPRReviewCommentInput
} from '../../../../shared/github/comment-types'
import {
ghExecFileAsync,
acquire,
release,
classifyGhError,
type LocalGitExecOptions
} from '../../gh-utils'
import { resolveGitHubRepoExecution, type GitHubApiRepository } from '../../github-api-repository'
} from '../../../../shared/github/comments/comment-types'
import { ghExecFileAsync } from '../../../git/command-runner/hosted-cli/gh-exec-file'
import { acquire, release } from '../../execution/github-operation-slots'
import { classifyGhError } from '../../api/gh-error-classification'
import type { LocalGitExecOptions } from '../../repositories/github-repository-identity'
import { resolveGitHubRepoExecution } from '../../repositories/github-api-repository'
import type { GitHubRepositoryIdentity as GitHubApiRepository } from '../../../../shared/github/repositories/repository-identity'
import { mapReviewCommentResponse } from './../map/review-comment-response'
import { codeHostFailure, classifiedFailure } from '../../../../shared/code-host-failure-copy'
export async function addPRReviewComment(
@@ -1,3 +1,5 @@
import type * as GithubRepositoryIdentityModule from '../../repositories/github-repository-identity'
import type * as GitExecFileModule from '../../../git/command-runner/git-exec-file'
import { beforeEach, describe, expect, it, vi } from 'vitest'
const {
@@ -24,11 +26,20 @@ const {
releaseMock: vi.fn()
}))
vi.mock('./gh-utils', () => ({
execFileAsync: vi.fn(),
ghExecFileAsync: ghExecFileAsyncMock,
getOwnerRepo: getOwnerRepoMock,
getIssueOwnerRepo: vi.fn(),
vi.mock('../../execution/github-legacy-command', () => ({
execFileAsync: vi.fn()
}))
vi.mock('../../../git/command-runner/hosted-cli/gh-exec-file', () => ({
ghExecFileAsync: ghExecFileAsyncMock
}))
vi.mock('../../repositories/github-owner-repo-selection', () => ({
getOwnerRepo: getOwnerRepoMock
}))
vi.mock('../../repositories/github-repository-identity', async (importOriginal) => ({
...(await importOriginal<typeof GithubRepositoryIdentityModule>()),
getOwnerRepoForRemote: getOwnerRepoForRemoteMock,
githubRepoContext: vi.fn((repoPath: string, connectionId?: string | null, options = {}) => ({
...options,
@@ -41,27 +52,27 @@ vi.mock('./gh-utils', () => ({
? {}
: { cwd: context.repoPath, ...(context.wslDistro ? { wslDistro: context.wslDistro } : {}) }
),
gitExecFileAsync: vi.fn(),
extractExecError: extractExecErrorMock,
parseGitHubOwnerRepo: vi.fn(),
acquire: acquireMock,
release: releaseMock,
_resetOwnerRepoCache: vi.fn()
}))
vi.mock('../git/command-runner/git-exec-file', async (importOriginal) => ({
...(await importOriginal<Record<string, unknown>>()),
vi.mock('../../../git/command-runner/git-exec-file', async (importOriginal) => ({
...(await importOriginal<typeof GitExecFileModule>()),
gitExecFileAsync: vi.fn()
}))
vi.mock('./github-enterprise-repository', () => ({
vi.mock('../../execution/github-operation-slots', () => ({
acquire: acquireMock,
release: releaseMock
}))
vi.mock('../../repositories/github-enterprise-repository', () => ({
getEnterpriseGitHubRepoSlug: getEnterpriseGitHubRepoSlugMock,
isGitHubHostAuthenticated: vi.fn().mockResolvedValue(true)
}))
import { createGitHubPullRequest } from './client/create/create-github-pull-request'
import { createGitHubPullRequest } from './create-github-pull-request'
import { _resetOriginGitHubApiRepositoryCache } from './github-api-repository'
import { _resetOriginGitHubApiRepositoryCache } from '../../repositories/github-api-repository-remote-probe'
beforeEach(() => {
_resetOriginGitHubApiRepositoryCache()
@@ -85,42 +96,6 @@ describe('createGitHubPullRequest', () => {
acquireMock.mockResolvedValue(undefined)
})
it('targets the origin fork (not the upstream parent) on a fork checkout (#7331)', async () => {
// Creation targets the fork holding the unqualified head branch; reads can prefer upstream.
getOwnerRepoForRemoteMock.mockImplementation(async (_repoPath: string, remoteName: string) =>
remoteName === 'origin'
? { owner: 'fsdwen', repo: 'orca' }
: { owner: 'stablyai', repo: 'orca' }
)
ghExecFileAsyncMock.mockResolvedValueOnce({
stdout: JSON.stringify({
number: 5,
url: 'https://github.com/fsdwen/orca/pull/5'
})
})
await expect(
createGitHubPullRequest(
'/repo-root',
{
provider: 'github',
base: 'main',
head: 'my-branch',
title: 'Fork PR'
},
'local'
)
).resolves.toEqual({
ok: true,
number: 5,
url: 'https://github.com/fsdwen/orca/pull/5'
})
const [args] = ghExecFileAsyncMock.mock.calls[0]
expect(args[args.indexOf('--repo') + 1]).toBe('fsdwen/orca')
expect(args[args.indexOf('--head') + 1]).toBe('my-branch')
})
it('returns the existing PR when gh reports an already-open pull request', async () => {
getOwnerRepoMock.mockResolvedValueOnce({ owner: 'acme', repo: 'widgets' })
ghExecFileAsyncMock
@@ -11,21 +11,22 @@ import {
import { mkdtemp, rm, writeFile } from 'node:fs/promises'
import { join } from 'node:path'
import { tmpdir } from 'node:os'
import { ghExecFileAsync } from '../../../git/command-runner/hosted-cli/gh-exec-file'
import { acquire, release } from '../../execution/github-operation-slots'
import {
ghExecFileAsync,
acquire,
release,
ghRepoExecOptions,
githubRepoContext
} from '../../gh-utils'
githubRepoContext,
withGitHubRepoExecution
} from '../../repositories/github-repository-identity'
import {
getHostedReviewLocalGitOptions,
type HostedReviewExecutionOptions
} from '../../../source-control/hosted-review-git-options'
import { getOriginGitHubApiRepository, githubHostExecOptions } from '../../github-api-repository'
import { getOriginGitHubApiRepository } from '../../repositories/github-api-repository-remote-probe'
import { githubHostExecOptions } from '../../repositories/github-repository-host'
import { classifyCreatePRError, parseCreatePRPayload } from './create-pr-error-classification'
import { findOpenPRByHeadBase, readPullRequestTemplate } from './pull-request-template'
import { withGitHubRepoExecution } from '../../github-repository-identity'
export async function createGitHubPullRequest(
repoPath: string,
input: CreateHostedReviewInput,
@@ -1,11 +1,13 @@
import { readFile } from 'node:fs/promises'
import { join } from 'node:path'
import { ghExecFileAsync, ghRepoExecOptions, githubRepoContext } from '../../gh-utils'
import { ghExecFileAsync } from '../../../git/command-runner/hosted-cli/gh-exec-file'
import { ghRepoExecOptions, githubRepoContext } from '../../repositories/github-repository-identity'
import {
getHostedReviewLocalGitOptions,
type HostedReviewExecutionOptions
} from '../../../source-control/hosted-review-git-options'
import { githubHostExecOptions, type GitHubApiRepository } from '../../github-api-repository'
import { githubHostExecOptions } from '../../repositories/github-repository-host'
import type { GitHubRepositoryIdentity as GitHubApiRepository } from '../../../../shared/github/repositories/repository-identity'
export async function findOpenPRByHeadBase(args: {
repoPath: string
repo: GitHubApiRepository
@@ -1,4 +1,4 @@
import type { OwnerRepo } from '../../gh-utils'
import type { GitHubRepositoryIdentity as OwnerRepo } from '../../../../shared/github/repositories/repository-identity'
import type { GhExecOptions } from './../github-exec-scope'
import { detectRepositoryMergeMetadata } from './repository-merge-metadata'
import type { MainWorkItem } from './../map/work-item-field-coercion'
@@ -1,4 +1,4 @@
import type { GitHubPRMergeMethodSettings } from '../../../../shared/github/pull-request-types'
import type { GitHubPRMergeMethodSettings } from '../../../../shared/github/pull-requests/pull-request-types'
import type { CoalescedProbes } from '../../../git/remotes/coalesced-probe'
export const MERGE_QUEUE_CACHE_TTL_MS = 10 * 60 * 1000
@@ -1,14 +1,16 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
const { exec, guard, spend } = vi.hoisted(() => ({ exec: vi.fn(), guard: vi.fn(), spend: vi.fn() }))
vi.mock('./gh-utils', () => ({ ghExecFileAsync: exec }))
vi.mock('./rate-limit', () => ({
vi.mock('../../../git/command-runner/hosted-cli/gh-exec-file', () => ({
ghExecFileAsync: exec
}))
vi.mock('../../api/rate-limit', () => ({
repositoryRateLimitGuard: guard,
noteRepositoryRateLimitSpend: spend
}))
import { detectRepositoryMergeMetadata } from './client/detect/repository-merge-metadata'
import { _resetMergeQueueCacheForTests } from './client/detect/repository-merge-metadata-cache'
import { detectRepositoryMergeMetadata } from './repository-merge-metadata'
import { _resetMergeQueueCacheForTests } from './repository-merge-metadata-cache'
const repository = { owner: 'acme', repo: 'widgets', host: 'github.com' }
const options = { cwd: '/repo' }
@@ -1,11 +1,12 @@
import { posix, win32 } from 'node:path'
import { normalizeGitHubPRMergeMethodSettings } from '../../../../shared/github/pull-request-merge-methods'
import { normalizeGitHubPRMergeMethodSettings } from '../../../../shared/github/pull-requests/merge/pull-request-merge-methods'
import { mergeCommandEnvironment } from '../../../../shared/command-environment'
import { ghExecFileAsync } from '../../gh-utils'
import { githubHostExecOptions, type GitHubApiRepository } from '../../github-api-repository'
import { githubRepoIdentityKey } from '../../../../shared/github/repository-identity-key'
import { noteRepositoryRateLimitSpend, repositoryRateLimitGuard } from '../../rate-limit'
import { githubReadExecutionScope } from '../../github-read-execution-scope'
import { ghExecFileAsync } from '../../../git/command-runner/hosted-cli/gh-exec-file'
import { githubHostExecOptions } from '../../repositories/github-repository-host'
import type { GitHubRepositoryIdentity as GitHubApiRepository } from '../../../../shared/github/repositories/repository-identity'
import { githubRepoIdentityKey } from '../../../../shared/github/repositories/repository-identity-key'
import { noteRepositoryRateLimitSpend, repositoryRateLimitGuard } from '../../api/rate-limit'
import { codeHostReadExecutionScope as githubReadExecutionScope } from '../../../source-control/code-host-read-execution-scope'
import { runCoalescedProbe } from '../../../git/remotes/coalesced-probe'
import { parseWslUncPath } from '../../../../shared/wsl-paths'
import type { GhExecOptions } from './../github-exec-scope'
@@ -1,5 +1,6 @@
import type { GitHubViewer } from '../../../../shared/github/pull-request-types'
import { ghExecFileAsync, acquire, release } from '../../gh-utils'
import type { GitHubViewer } from '../../../../shared/github/pull-requests/pull-request-types'
import { ghExecFileAsync } from '../../../git/command-runner/hosted-cli/gh-exec-file'
import { acquire, release } from '../../execution/github-operation-slots'
/** Use the bounded runner so failed viewer probes return null without stranding a GitHub admission slot. */
export async function getAuthenticatedViewer(): Promise<GitHubViewer | null> {
await acquire()
@@ -1,12 +1,15 @@
import { GITHUB_BODY_MEDIA_TYPE } from '../../body-image-urls'
import type { PRComment } from '../../../../shared/github/comment-types'
import { GITHUB_BODY_MEDIA_TYPE } from '../../attachments/body-image-urls'
import type { PRComment } from '../../../../shared/github/comments/comment-types'
import { runCoalescedProbe, type CoalescedProbes } from '../../../git/remotes/coalesced-probe'
import { extractExecError } from '../../../git/command-runner/process-capture/exec-error'
import { githubReadExecutionScope } from '../../github-read-execution-scope'
import { GITHUB_WORK_ITEMS_SSH_REMOTE_REQUIRED_MESSAGE } from '../../../../shared/github/work-item-ssh-error'
import { ghExecFileAsync, acquire, release, type LocalGitExecOptions } from '../../gh-utils'
import { resolveGitHubRepoExecution, type GitHubApiRepository } from '../../github-api-repository'
import { noteRepositoryRateLimitSpend, repositoryRateLimitGuard } from '../../rate-limit'
import { codeHostReadExecutionScope as githubReadExecutionScope } from '../../../source-control/code-host-read-execution-scope'
import { GITHUB_WORK_ITEMS_SSH_REMOTE_REQUIRED_MESSAGE } from '../../../../shared/github/work-items/work-item-ssh-error'
import { ghExecFileAsync } from '../../../git/command-runner/hosted-cli/gh-exec-file'
import { acquire, release } from '../../execution/github-operation-slots'
import type { LocalGitExecOptions } from '../../repositories/github-repository-identity'
import { resolveGitHubRepoExecution } from '../../repositories/github-api-repository'
import type { GitHubRepositoryIdentity as GitHubApiRepository } from '../../../../shared/github/repositories/repository-identity'
import { noteRepositoryRateLimitSpend, repositoryRateLimitGuard } from '../../api/rate-limit'
import { assertRateLimitBudget } from './../lookup/pr-lookup-rate-limit'
import { REVIEW_THREADS_QUERY } from './pr-review-threads-query'
@@ -1,3 +1,4 @@
import type * as GithubRepositoryIdentityModule from '../../repositories/github-repository-identity'
import { beforeEach, describe, expect, it, vi } from 'vitest'
const { ghExecFileAsyncMock, getOwnerRepoMock, rateLimitGuardMock } = vi.hoisted(() => ({
@@ -6,16 +7,21 @@ const { ghExecFileAsyncMock, getOwnerRepoMock, rateLimitGuardMock } = vi.hoisted
rateLimitGuardMock: vi.fn(() => ({ blocked: false }))
}))
vi.mock('./gh-utils', () => ({
execFileAsync: vi.fn(),
ghExecFileAsync: ghExecFileAsyncMock,
vi.mock('../../execution/github-legacy-command', () => ({
execFileAsync: vi.fn()
}))
vi.mock('../../../git/command-runner/hosted-cli/gh-exec-file', () => ({
ghExecFileAsync: ghExecFileAsyncMock
}))
vi.mock('../../repositories/github-repository-identity', async (importOriginal) => ({
...(await importOriginal<typeof GithubRepositoryIdentityModule>()),
githubRepoContext: (repoPath: string, connectionId?: string | null) => ({
repoPath,
connectionId: connectionId ?? null
}),
ghRepoExecOptions: (context: { repoPath: string }) => ({ cwd: context.repoPath }),
getOwnerRepo: getOwnerRepoMock,
getIssueOwnerRepo: vi.fn(),
getOwnerRepoForRemote: (
repoPath: string,
remoteName: string,
@@ -25,21 +31,35 @@ vi.mock('./gh-utils', () => ({
remoteName === 'origin'
? getOwnerRepoMock(repoPath, connectionId, localGitOptions)
: Promise.resolve(null),
resolveIssueSource: vi.fn(),
extractExecError: vi.fn((err: unknown) => ({ stderr: String(err), stdout: '' })),
_resetOwnerRepoCache: vi.fn()
}))
vi.mock('../../repositories/github-owner-repo-selection', () => ({
getOwnerRepo: getOwnerRepoMock,
resolveIssueSource: vi.fn()
}))
vi.mock('../../../git/command-runner/process-capture/exec-error', async (importOriginal) => ({
...(await importOriginal<Record<string, unknown>>()),
extractExecError: vi.fn((err: unknown) => ({ stderr: String(err), stdout: '' }))
}))
vi.mock('../../execution/github-operation-slots', () => ({
acquire: vi.fn(),
release: vi.fn(),
_resetOwnerRepoCache: vi.fn(),
release: vi.fn()
}))
vi.mock('../../api/gh-error-classification', () => ({
classifyGhError: (stderr: string) => ({ type: 'unknown', message: stderr }),
classifyListIssuesError: (stderr: string) => ({ type: 'unknown', message: stderr })
}))
vi.mock('../git/command-runner/git-exec-file', async (importOriginal) => ({
vi.mock('../../../git/command-runner/git-exec-file', async (importOriginal) => ({
...(await importOriginal<Record<string, unknown>>()),
gitExecFileAsync: vi.fn()
}))
vi.mock('./rate-limit', () => ({
vi.mock('../../api/rate-limit', () => ({
rateLimitGuard: rateLimitGuardMock,
noteRateLimitSpend: vi.fn(),
getRateLimit: vi.fn(async () => ({ ok: false, error: 'not probed in tests' })),
@@ -48,10 +68,10 @@ vi.mock('./rate-limit', () => ({
spendsSharedGitHubComQuota: vi.fn(() => true)
}))
import { getWorkItem } from './client/fetch/get-work-item'
import { _resetMergeQueueCacheForTests } from './client/detect/repository-merge-metadata-cache'
import { _resetOwnerRepoCache } from './gh-utils'
import { _resetOriginGitHubApiRepositoryCache } from './github-api-repository'
import { getWorkItem } from './get-work-item'
import { _resetMergeQueueCacheForTests } from '../detect/repository-merge-metadata-cache'
import { _resetOwnerRepoCache } from '../../repositories/github-repository-identity'
import { _resetOriginGitHubApiRepositoryCache } from '../../repositories/github-api-repository-remote-probe'
// Metadata and complete checks are independent provider responses.
function mockPullRequestDetail(payload: Record<string, unknown>): void {
@@ -1,5 +1,6 @@
import type * as GithubRepositoryIdentityModule from '../../repositories/github-repository-identity'
import { beforeEach, expect, it, vi } from 'vitest'
import type { GitHubApiRepository } from './github-api-repository'
import type { GitHubRepositoryIdentity as GitHubApiRepository } from '../../../../shared/github/repositories/repository-identity'
const mocks = vi.hoisted(() => ({
exec: vi.fn(),
@@ -7,10 +8,17 @@ const mocks = vi.hoisted(() => ({
acquire: vi.fn(),
release: vi.fn()
}))
vi.mock('./gh-utils', () => ({
ghExecFileAsync: mocks.exec,
vi.mock('../../../git/command-runner/hosted-cli/gh-exec-file', () => ({
ghExecFileAsync: mocks.exec
}))
vi.mock('../../execution/github-operation-slots', () => ({
acquire: mocks.acquire,
release: mocks.release,
release: mocks.release
}))
vi.mock('../../repositories/github-repository-identity', async (importOriginal) => ({
...(await importOriginal<typeof GithubRepositoryIdentityModule>()),
ghRepoExecOptions: (context: { repoPath: string; connectionId?: string; wslDistro?: string }) =>
context.connectionId
? {}
@@ -19,24 +27,34 @@ vi.mock('./gh-utils', () => ({
repoPath,
connectionId,
...options
}),
})
}))
vi.mock('../../api/gh-error-classification', () => ({
classifyGhError: () => ({ type: 'not_found' })
}))
vi.mock('./github-api-repository', () => ({
resolveIssueGitHubApiRepositorySource: mocks.source,
vi.mock('../../repositories/github-api-repository', () => ({
resolveIssueGitHubApiRepositorySource: mocks.source
}))
vi.mock('../../repositories/github-repository-host', () => ({
githubHostExecOptions: (repository?: GitHubApiRepository) => ({ host: repository?.host })
}))
vi.mock('./client', async () => ({
...(await import('./client/fetch/get-work-item')),
getPRChecks: vi.fn(),
vi.mock('../check/get-pr-checks', async () => ({
...(await import('./get-work-item')),
getPRChecks: vi.fn()
}))
vi.mock('./get-pr-comments', async () => ({
...(await import('./get-work-item')),
getPRComments: vi.fn()
}))
vi.mock('./rate-limit', () => ({
vi.mock('../../api/rate-limit', () => ({
repositoryRateLimitGuard: () => ({ blocked: false }),
noteRepositoryRateLimitSpend: vi.fn()
}))
import { getWorkItemWithRepository } from './client/fetch/get-work-item'
import { getWorkItemWithRepository } from './get-work-item'
const repository = { owner: 'team', repo: 'widgets', host: 'github.example.com:8443' }
let issue: Record<string, unknown>
@@ -1,6 +1,11 @@
import type * as GithubLegacyCommandModule from '../../execution/github-legacy-command'
import type * as GhExecFileModule from '../../../git/command-runner/hosted-cli/gh-exec-file'
import type * as GithubOwnerRepoSelectionModule from '../../repositories/github-owner-repo-selection'
import type * as GithubRepositoryIdentityModule from '../../repositories/github-repository-identity'
import type * as GithubOperationSlotsModule from '../../execution/github-operation-slots'
import type * as GithubApiRepositoryModule from '../../repositories/github-api-repository'
import type * as GithubApiRepositoryRemoteProbeModule from '../../repositories/github-api-repository-remote-probe'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import type * as GithubApiRepositoryModule from './github-api-repository'
import type * as GhUtils from './gh-utils'
const {
ghExecFileAsyncMock,
@@ -26,23 +31,60 @@ const {
releaseMock: vi.fn()
}))
vi.mock('./gh-utils', async () => {
const actual = await vi.importActual<typeof GhUtils>('./gh-utils')
vi.mock('../../execution/github-legacy-command', async () => {
const actual = await vi.importActual<typeof GithubLegacyCommandModule>(
'../../execution/github-legacy-command'
)
return {
...actual,
execFileAsync: vi.fn()
}
})
vi.mock('../../../git/command-runner/hosted-cli/gh-exec-file', async () => {
const actual = await vi.importActual<typeof GhExecFileModule>(
'../../../git/command-runner/hosted-cli/gh-exec-file'
)
return {
...actual,
ghExecFileAsync: ghExecFileAsyncMock
}
})
vi.mock('../../repositories/github-owner-repo-selection', async () => {
const actual = await vi.importActual<typeof GithubOwnerRepoSelectionModule>(
'../../repositories/github-owner-repo-selection'
)
return {
...actual,
execFileAsync: vi.fn(),
ghExecFileAsync: ghExecFileAsyncMock,
getOwnerRepo: getOwnerRepoMock,
getIssueOwnerRepo: getIssueOwnerRepoMock,
resolveIssueSource: resolveIssueSourceMock
}
})
vi.mock('../../repositories/github-repository-identity', async () => {
const actual = await vi.importActual<typeof GithubRepositoryIdentityModule>(
'../../repositories/github-repository-identity'
)
return {
...actual,
getOwnerRepoForRemote: getOwnerRepoForRemoteMock,
resolveIssueSource: resolveIssueSourceMock,
acquire: acquireMock,
release: releaseMock,
_resetOwnerRepoCache: vi.fn()
}
})
vi.mock('./rate-limit', () => ({
vi.mock('../../execution/github-operation-slots', async () => {
const actual = await vi.importActual<typeof GithubOperationSlotsModule>(
'../../execution/github-operation-slots'
)
return {
...actual,
acquire: acquireMock,
release: releaseMock
}
})
vi.mock('../../api/rate-limit', () => ({
rateLimitGuard: rateLimitGuardMock,
noteRateLimitSpend: noteRateLimitSpendMock,
getRateLimit: vi.fn(async () => ({ ok: false, error: 'not probed in tests' })),
@@ -51,10 +93,9 @@ vi.mock('./rate-limit', () => ({
spendsSharedGitHubComQuota: () => true
}))
vi.mock('./github-api-repository', async (importOriginal) => {
const actual = await importOriginal<typeof GithubApiRepositoryModule>()
vi.mock('../../repositories/github-api-repository', async (importOriginal) => {
return {
...actual,
...(await importOriginal<typeof GithubApiRepositoryModule>()),
resolveIssueGitHubApiRepositorySource: (
repoPath: string,
preference: unknown,
@@ -63,6 +104,17 @@ vi.mock('./github-api-repository', async (importOriginal) => {
) => resolveIssueSourceMock(repoPath, preference, connectionId, localGitOptions),
getIssueGitHubApiRepository: (repoPath: string, connectionId?: string | null) =>
getIssueOwnerRepoMock(repoPath, connectionId),
resolveGitHubApiRepositoryCandidates: (
repoPath: string,
connectionId?: string | null,
localGitOptions?: unknown
) => resolvePRRepositoryCandidatesMock(repoPath, connectionId, localGitOptions)
}
})
vi.mock('../../repositories/github-api-repository-remote-probe', async (importOriginal) => {
return {
...(await importOriginal<typeof GithubApiRepositoryRemoteProbeModule>()),
getOriginGitHubApiRepository: (
repoPath: string,
connectionId?: string | null,
@@ -76,17 +128,12 @@ vi.mock('./github-api-repository', async (importOriginal) => {
) =>
remoteName === 'origin'
? getOwnerRepoMock(repoPath, connectionId, localGitOptions)
: getOwnerRepoForRemoteMock(repoPath, remoteName, connectionId, localGitOptions),
resolveGitHubApiRepositoryCandidates: (
repoPath: string,
connectionId?: string | null,
localGitOptions?: unknown
) => resolvePRRepositoryCandidatesMock(repoPath, connectionId, localGitOptions)
: getOwnerRepoForRemoteMock(repoPath, remoteName, connectionId, localGitOptions)
}
})
import { getWorkItem } from './client/fetch/get-work-item'
import { _resetOwnerRepoCache } from './gh-utils'
import { getWorkItem } from './get-work-item'
import { _resetOwnerRepoCache } from '../../repositories/github-repository-identity'
describe('GitHub issue open-by-number origin preference', () => {
beforeEach(() => {
@@ -1,49 +1,79 @@
import type * as GitExecFileModule from '../git/command-runner/git-exec-file'
import type * as GhExecFileModule from '../git/command-runner/hosted-cli/gh-exec-file'
import type * as GithubRepositoryIdentityModule from '../../repositories/github-repository-identity'
import type * as GhExecFileModule from '../../../git/command-runner/hosted-cli/gh-exec-file'
import type * as GithubApiRepositoryModule from '../../repositories/github-api-repository'
import type * as GithubApiRepositoryRemoteProbeModule from '../../repositories/github-api-repository-remote-probe'
import type * as GitExecFileModule from '../../../git/command-runner/git-exec-file'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import type * as GithubApiRepositoryModule from './github-api-repository'
import type * as GitHubEnterpriseRepositoryModule from './github-enterprise-repository'
import type * as GitHubEnterpriseRepositoryModule from '../../repositories/github-enterprise-repository'
const { clientMocks, moduleMocks } = await vi.hoisted(async () => {
const moduleMocks = await import('./client-test-mocks')
const moduleMocks = await import('../client-test-mocks')
return { clientMocks: moduleMocks.createGitHubClientMocks(), moduleMocks }
})
vi.mock('./gh-utils', () => moduleMocks.ghUtilsModuleMock(clientMocks))
vi.mock('../../execution/github-legacy-command', () =>
moduleMocks.githubLegacyCommandModuleMock(clientMocks)
)
vi.mock('../git/command-runner/git-exec-file', async (importOriginal) => ({
vi.mock('../../repositories/github-owner-repo-selection', () =>
moduleMocks.githubOwnerSelectionModuleMock(clientMocks)
)
vi.mock('../../repositories/github-repository-identity', async (importOriginal) => ({
...(await importOriginal<typeof GithubRepositoryIdentityModule>()),
...moduleMocks.githubRepositoryIdentityModuleMock(clientMocks)
}))
vi.mock('../../api/gh-error-classification', () =>
moduleMocks.githubErrorClassificationModuleMock()
)
vi.mock('../../execution/github-operation-slots', () =>
moduleMocks.githubOperationSlotsModuleMock(clientMocks)
)
vi.mock('../../../git/command-runner/git-exec-file', async (importOriginal) => ({
...(await importOriginal<typeof GitExecFileModule>()),
gitExecFileAsync: clientMocks.gitExecFileAsyncMock
}))
vi.mock('../git/command-runner/hosted-cli/gh-exec-file', async (importOriginal) => ({
vi.mock('../../../git/command-runner/hosted-cli/gh-exec-file', async (importOriginal) => ({
...(await importOriginal<typeof GhExecFileModule>()),
ghExecFileAsync: clientMocks.ghExecFileAsyncMock
...moduleMocks.githubHostedCliModuleMock(clientMocks)
}))
vi.mock('../git/objects/local-git-object-quarantine', () =>
vi.mock('../../../git/objects/local-git-object-quarantine', () =>
moduleMocks.localGitObjectQuarantineModuleMock()
)
vi.mock('./local-git-config-signature', () =>
vi.mock('../../repositories/local-git-config-signature', () =>
moduleMocks.localGitConfigSignatureModuleMock(clientMocks)
)
vi.mock('./github-enterprise-repository', async (importOriginal) =>
vi.mock('../../repositories/github-enterprise-repository', async (importOriginal) =>
moduleMocks.githubEnterpriseRepositoryModuleMock(
await importOriginal<typeof GitHubEnterpriseRepositoryModule>()
)
)
vi.mock('./rate-limit', () => moduleMocks.rateLimitModuleMock(clientMocks))
vi.mock('./github-api-repository', async (importOriginal) =>
vi.mock('../../api/rate-limit', () => moduleMocks.rateLimitModuleMock(clientMocks))
vi.mock('../../repositories/github-api-repository', async (importOriginal) =>
moduleMocks.githubApiRepositoryModuleMock(
clientMocks,
await importOriginal<typeof GithubApiRepositoryModule>()
)
)
import { getWorkItemByOwnerRepo } from './client/fetch/get-work-item'
vi.mock('../../repositories/github-api-repository-remote-probe', async (importOriginal) =>
moduleMocks.githubApiRepositoryRemoteProbeModuleMock(
clientMocks,
await importOriginal<typeof GithubApiRepositoryRemoteProbeModule>()
)
)
import { getWorkItemByOwnerRepo } from './get-work-item'
import {
installIndependentCheckFixtures,
resetGraphQLRateLimitGuardMocks
} from './client-test-harness'
} from '../client-test-harness'
const { resolvePRRepositoryCandidatesMock } = clientMocks
+12 -12
View File
@@ -1,27 +1,27 @@
import { withGitHubRepoExecution } from '../../github-repository-identity'
import type { IssueSourcePreference } from '../../../../shared/repo-types'
import {
acquire,
release,
classifyGhError,
extractExecError,
withGitHubRepoExecution,
type LocalGitExecOptions
} from '../../gh-utils'
} from '../../repositories/github-repository-identity'
import type { IssueSourcePreference } from '../../../../shared/repo-types'
import { acquire, release } from '../../execution/github-operation-slots'
import { classifyGhError } from '../../api/gh-error-classification'
import { extractExecError } from '../../../git/command-runner/process-capture/exec-error'
import {
resolveGitHubApiRepository,
resolveGitHubApiRepositoryCandidates,
resolveIssueGitHubApiRepositorySource,
type GitHubApiRepository
} from '../../github-api-repository'
resolveIssueGitHubApiRepositorySource
} from '../../repositories/github-api-repository'
import type { GitHubRepositoryIdentity as GitHubApiRepository } from '../../../../shared/github/repositories/repository-identity'
import {
githubRepoIdentityKey,
isDefaultGitHubHost
} from '../../../../shared/github/repository-identity-key'
} from '../../../../shared/github/repositories/repository-identity-key'
import type { MainWorkItem } from './../map/work-item-field-coercion'
import {
getIssueDetailsViaGraphQL,
type CollapsedIssueDetails
} from '../../issue-work-item-details'
} from '../../issues/issue-work-item-details'
import {
fetchIssueWorkItem,
fetchWorkItemPreview,
+2 -1
View File
@@ -1,4 +1,5 @@
import { ghExecFileAsync, acquire, release } from '../../gh-utils'
import { ghExecFileAsync } from '../../../git/command-runner/hosted-cli/gh-exec-file'
import { acquire, release } from '../../execution/github-operation-slots'
import { runAsDefaultWslCliUserAct } from '../../../git/command-runner/hosted-cli/default-wsl-cli-fallback'
export const ORCA_REPO = 'stablyai/orca'
@@ -1,11 +1,11 @@
import { githubBodyImageUrls, removeFailedGitHubBodyHtml } from '../../body-image-urls'
import { githubBodyImageUrls, removeFailedGitHubBodyHtml } from '../../attachments/body-image-urls'
import type { Nodes } from 'mdast'
import { z } from 'zod'
import remarkParse from 'remark-parse'
import { unified } from 'unified'
import type { PRComment } from '../../../../shared/github/comment-types'
import { mapGraphQLReactionGroups } from '../../comment-reactions'
import { tailDiffHunk } from '../../../../shared/github/pr-comment-diff-hunk'
import type { PRComment } from '../../../../shared/github/comments/comment-types'
import { mapGraphQLReactionGroups } from '../../comments/comment-reactions'
import { tailDiffHunk } from '../../../../shared/github/comments/pr-comment-diff-hunk'
import { viewerAuthorship } from '../../../source-control/comment-viewer-authorship'
const commentSchema = z.object({
@@ -1,34 +1,37 @@
import { ghExecFileAsync } from '../../../git/command-runner/hosted-cli/gh-exec-file'
import { acquire, release } from '../../execution/github-operation-slots'
import {
ghExecFileAsync,
acquire,
release,
getRemoteUrlForRepo,
githubRepoContext,
githubRepoContext
} from '../../repositories/github-repository-identity'
import {
parseGitHubRemoteIdentity,
type OwnerRepo
} from '../../gh-utils'
gitHubSshConfigHostAlias,
effectiveGitHubRemoteHost
} from '../../repositories/github-remote-identity-parsing'
import type {
GitHubRepositoryIdentity as OwnerRepo,
GitHubRepositoryIdentity as GitHubApiRepository
} from '../../../../shared/github/repositories/repository-identity'
import {
getHostedReviewLocalGitOptions,
type HostedReviewExecutionOptions
} from '../../../source-control/hosted-review-git-options'
import {
getGitHubApiRepositoryForRemote,
getOriginGitHubApiRepository,
githubRepositorySlugArg,
resolveGitHubRepoExecution,
type GitHubApiRepository
} from '../../github-api-repository'
getOriginGitHubApiRepository
} from '../../repositories/github-api-repository-remote-probe'
import { githubRepositorySlugArg } from '../../repositories/github-repository-host'
import { resolveGitHubRepoExecution } from '../../repositories/github-api-repository'
import { withSharedRemoteUrlReads } from '../../../git/remotes/remote-url-probe'
import { isStableMissingGitRemoteError } from '../../../git/remotes/stable-missing-git-remote-error'
import {
normalizeForgeWebHost,
foldWwwHostAlias
} from '../../../../shared/git/remotes/git-remote-host-alias'
import {
gitHubSshConfigHostAlias,
effectiveGitHubRemoteHost
} from '../../github-remote-identity-parsing'
import { resolveSshConfigHostname } from '../../github-ssh-host-alias-resolution'
import { resolveSshConfigHostname } from '../../repositories/github-ssh-host-alias-resolution'
import { hostedReviewLocalGitOptionArgs, sameOwnerRepo } from './../github-exec-scope'
export async function getRepoSlug(
repoPath: string,
@@ -1,12 +1,13 @@
import type { IssueSourcePreference } from '../../../../shared/repo-types'
import { ghExecFileAsync } from '../../../git/command-runner/hosted-cli/gh-exec-file'
import { classifyGhError } from '../../api/gh-error-classification'
import {
ghExecFileAsync,
classifyGhError,
ghRepoExecOptions,
githubRepoContext,
type LocalGitExecOptions
} from '../../gh-utils'
import { githubHostExecOptions, type GitHubApiRepository } from '../../github-api-repository'
} from '../../repositories/github-repository-identity'
import { githubHostExecOptions } from '../../repositories/github-repository-host'
import type { GitHubRepositoryIdentity as GitHubApiRepository } from '../../../../shared/github/repositories/repository-identity'
import { githubPRStackExecutionScope, type GhExecOptions } from './../github-exec-scope'
import { resolvePullRequestLookupCandidates } from './../pull-request-lookup-candidates'
import { detectRepositoryMergeMetadata } from './../detect/repository-merge-metadata'
@@ -1,9 +1,9 @@
import type { PRRefreshOutcome } from '../../../shared/github/pull-request-refresh-types'
import type { PRRefreshOutcome } from '../../../shared/github/pull-requests/refresh/pull-request-refresh-types'
import {
classifyPRRefreshError,
safePRRefreshErrorMessage
} from '../pr-refresh-error-classification'
import { classifyGhError } from '../gh-utils'
} from '../pull-requests/refresh/pr-refresh-error-classification'
import { classifyGhError } from '../api/gh-error-classification'
// Keep real error parsing available when tests mock gh-utils.
import {
extractExecError,
+7 -5
View File
@@ -1,11 +1,13 @@
import type { LocalGitExecOptions, OwnerRepo } from '../gh-utils'
import type { LocalGitExecOptions } from '../repositories/github-repository-identity'
import type { GitHubRepositoryIdentity as OwnerRepo } from '../../../shared/github/repositories/repository-identity'
import {
hasHostedReviewLocalGitOptions,
getHostedReviewLocalGitOptions
getHostedReviewLocalGitOptions,
type HostedReviewExecutionOptions
} from '../../source-control/hosted-review-git-options'
import type { HostedReviewExecutionOptions } from '../../source-control/hosted-review-git-options'
import type { GitHubRepoExecOptions } from '../github-api-repository'
import { githubRepoIdentityKey } from '../../../shared/github/repository-identity-key'
import type { GitHubRepoExecOptions } from '../repositories/github-api-repository'
import { githubRepoIdentityKey } from '../../../shared/github/repositories/repository-identity-key'
export type GhExecOptions = GitHubRepoExecOptions & { signal?: AbortSignal }
export type HostedReviewLocalGitOptions = ReturnType<typeof getHostedReviewLocalGitOptions>
+13 -13
View File
@@ -1,27 +1,27 @@
import { withGitHubRepoExecution } from '../../github-repository-identity'
import type { IssueSourcePreference } from '../../../../shared/repo-types'
import { isGitHubWorkItemsQueryTooLarge } from '../../../../shared/github/work-items-query-bounds'
import { parseTaskQuery, type ParsedTaskQuery } from '../../../../shared/work-items/task-query'
import {
ghExecFileAsync,
acquire,
release,
withGitHubRepoExecution,
ghRepoExecOptions,
githubRepoContext,
type LocalGitExecOptions,
type OwnerRepo
} from '../../gh-utils'
type LocalGitExecOptions
} from '../../repositories/github-repository-identity'
import type { IssueSourcePreference } from '../../../../shared/repo-types'
import { isGitHubWorkItemsQueryTooLarge } from '../../../../shared/github/work-items/work-items-query-bounds'
import { parseTaskQuery, type ParsedTaskQuery } from '../../../../shared/work-items/task-query'
import { ghExecFileAsync } from '../../../git/command-runner/hosted-cli/gh-exec-file'
import { acquire, release } from '../../execution/github-operation-slots'
import type { GitHubRepositoryIdentity as OwnerRepo } from '../../../../shared/github/repositories/repository-identity'
import { githubHostExecOptions } from '../../repositories/github-repository-host'
import {
githubHostExecOptions,
resolveIssueGitHubApiRepositorySource,
type GitHubRepoExecOptions
} from '../../github-api-repository'
} from '../../repositories/github-api-repository'
import {
getRateLimit,
noteRepositoryRateLimitSpend,
repositoryRateLimitGuard,
spendsSharedGitHubComQuota
} from '../../rate-limit'
} from '../../api/rate-limit'
import { sameOwnerRepo } from './../github-exec-scope'
import { buildWorkItemListRequest, resolvePrWorkItemSource } from './work-item-list-request'
import { buildSearchQueryString, defaultOpenWorkItemQuery } from './work-item-search-query'
@@ -1,8 +1,10 @@
import type * as GithubRepositoryIdentityModule from '../../repositories/github-repository-identity'
import type * as GithubApiRepositoryModule from '../../repositories/github-api-repository'
import type * as GithubApiRepositoryRemoteProbeModule from '../../repositories/github-api-repository-remote-probe'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import type * as GithubApiRepositoryModule from './github-api-repository'
// Keep legacy REST request/failure coverage; API-boundary suites exercise the GraphQL path.
vi.mock('./client/list/work-item-search-page', () => ({ usesGraphqlWorkItemSearch: () => false }))
vi.mock('./work-item-search-page', () => ({ usesGraphqlWorkItemSearch: () => false }))
const {
execFileAsyncMock,
@@ -30,9 +32,16 @@ const {
releaseMock: vi.fn()
}))
vi.mock('./gh-utils', () => ({
execFileAsync: execFileAsyncMock,
ghExecFileAsync: ghExecFileAsyncMock,
vi.mock('../../execution/github-legacy-command', () => ({
execFileAsync: execFileAsyncMock
}))
vi.mock('../../../git/command-runner/hosted-cli/gh-exec-file', () => ({
ghExecFileAsync: ghExecFileAsyncMock
}))
vi.mock('../../repositories/github-repository-identity', async (importOriginal) => ({
...(await importOriginal<typeof GithubRepositoryIdentityModule>()),
githubRepoContext: (
repoPath: string,
connectionId?: string | null,
@@ -50,24 +59,32 @@ vi.mock('./gh-utils', () => ({
context.connectionId
? {}
: { cwd: context.repoPath, ...(context.wslDistro ? { wslDistro: context.wslDistro } : {}) },
getOwnerRepo: getOwnerRepoMock,
getIssueOwnerRepo: getIssueOwnerRepoMock,
getOwnerRepoForRemote: getOwnerRepoForRemoteMock,
resolveIssueSource: resolveIssueSourceMock,
_resetOwnerRepoCache: vi.fn()
}))
vi.mock('../../repositories/github-owner-repo-selection', () => ({
getOwnerRepo: getOwnerRepoMock,
resolveIssueSource: resolveIssueSourceMock
}))
vi.mock('../../execution/github-operation-slots', () => ({
acquire: acquireMock,
release: releaseMock,
_resetOwnerRepoCache: vi.fn(),
release: releaseMock
}))
vi.mock('../../api/gh-error-classification', () => ({
classifyGhError: (stderr: string) => ({ type: 'unknown', message: stderr }),
classifyListIssuesError: (stderr: string) => ({ type: 'unknown', message: stderr }),
classifyListPrsError: (stderr: string) => ({ type: 'unknown', message: stderr })
}))
vi.mock('../git/command-runner/git-exec-file', async (importOriginal) => ({
vi.mock('../../../git/command-runner/git-exec-file', async (importOriginal) => ({
...(await importOriginal<Record<string, unknown>>()),
gitExecFileAsync: gitExecFileAsyncMock
}))
vi.mock('./rate-limit', () => ({
vi.mock('../../api/rate-limit', () => ({
rateLimitGuard: rateLimitGuardMock,
noteRateLimitSpend: noteRateLimitSpendMock,
getRateLimit: vi.fn(async () => ({ ok: false, error: 'not probed in tests' })),
@@ -78,11 +95,9 @@ vi.mock('./rate-limit', () => ({
spendsSharedGitHubComQuota: () => true
}))
vi.mock('./github-api-repository', async (importOriginal) => {
const actual = await importOriginal<typeof GithubApiRepositoryModule>()
vi.mock('../../repositories/github-api-repository', async (importOriginal) => {
return {
...actual,
// Bridge hosted source resolution to the suite's gh-utils fixtures.
...(await importOriginal<typeof GithubApiRepositoryModule>()),
resolveIssueGitHubApiRepositorySource: (
repoPath: string,
preference: unknown,
@@ -90,7 +105,13 @@ vi.mock('./github-api-repository', async (importOriginal) => {
localGitOptions?: unknown
) => resolveIssueSourceMock(repoPath, preference, connectionId, localGitOptions),
getIssueGitHubApiRepository: (repoPath: string, connectionId?: string | null) =>
getIssueOwnerRepoMock(repoPath, connectionId),
getIssueOwnerRepoMock(repoPath, connectionId)
}
})
vi.mock('../../repositories/github-api-repository-remote-probe', async (importOriginal) => {
return {
...(await importOriginal<typeof GithubApiRepositoryRemoteProbeModule>()),
getOriginGitHubApiRepository: (
repoPath: string,
connectionId?: string | null,
@@ -107,12 +128,12 @@ vi.mock('./github-api-repository', async (importOriginal) => {
: getOwnerRepoForRemoteMock(repoPath, remoteName, connectionId, localGitOptions)
}
})
import { listWorkItems } from './client/list/list-work-items'
import { _resetMergeQueueCacheForTests } from './client/detect/repository-merge-metadata-cache'
import { _resetOwnerRepoCache } from './gh-utils'
import { _resetRemoteNameListingCache } from '../git/remotes/remote-name-listing'
import { listWorkItems } from './list-work-items'
import { _resetMergeQueueCacheForTests } from '../detect/repository-merge-metadata-cache'
import { _resetOwnerRepoCache } from '../../repositories/github-repository-identity'
import { _resetRemoteNameListingCache } from '../../../git/remotes/remote-name-listing'
import { _resetOriginGitHubApiRepositoryCache } from './github-api-repository'
import { _resetOriginGitHubApiRepositoryCache } from '../../repositories/github-api-repository-remote-probe'
beforeEach(() => {
_resetOriginGitHubApiRepositoryCache()
@@ -1,10 +1,14 @@
import { withGitHubRepoExecution } from '../../github-repository-identity'
import type { ListWorkItemsResult } from '../../../../shared/github/work-item-types'
import {
withGitHubRepoExecution,
type LocalGitExecOptions
} from '../../repositories/github-repository-identity'
import type { ListWorkItemsResult } from '../../../../shared/github/work-items/work-item-types'
import type { IssueSourcePreference } from '../../../../shared/repo-types'
import { isGitHubWorkItemsQueryTooLarge } from '../../../../shared/github/work-items-query-bounds'
import { isGitHubWorkItemsQueryTooLarge } from '../../../../shared/github/work-items/work-items-query-bounds'
import { parseTaskQuery } from '../../../../shared/work-items/task-query'
import { acquire, release, type LocalGitExecOptions } from '../../gh-utils'
import { resolveIssueGitHubApiRepositorySource } from '../../github-api-repository'
import { acquire, release } from '../../execution/github-operation-slots'
import { resolveIssueGitHubApiRepositorySource } from '../../repositories/github-api-repository'
import type { MainWorkItem } from './../map/work-item-field-coercion'
import { normalizeWorkItemPage, resolvePrWorkItemSource } from './work-item-list-request'
import { listRecentWorkItems, listQueriedWorkItems } from './work-item-pages'
@@ -1,8 +1,10 @@
import { z } from 'zod'
import type { ParsedTaskQuery } from '../../../../shared/work-items/task-query'
import { ghExecFileAsync, type LocalGitExecOptions, type OwnerRepo } from '../../gh-utils'
import { noteRepositoryRateLimitSpend } from '../../rate-limit'
import type { GitHubRepoExecOptions } from '../../github-api-repository'
import { ghExecFileAsync } from '../../../git/command-runner/hosted-cli/gh-exec-file'
import type { LocalGitExecOptions } from '../../repositories/github-repository-identity'
import type { GitHubRepositoryIdentity as OwnerRepo } from '../../../../shared/github/repositories/repository-identity'
import { noteRepositoryRateLimitSpend } from '../../api/rate-limit'
import type { GitHubRepoExecOptions } from '../../repositories/github-api-repository'
import { fetchIssueWorkItem } from '../fetch/work-item-fetch'
import { mapIssueWorkItem } from '../map/work-item'
import { restIssueActor } from '../map/issue-actor'
@@ -1,13 +1,14 @@
import type { ClassifiedError } from '../../../../shared/classified-error'
import type { IssueSourcePreference } from '../../../../shared/repo-types'
import type { ParsedTaskQuery } from '../../../../shared/work-items/task-query'
import { GITHUB_WORK_ITEMS_SSH_REMOTE_REQUIRED_MESSAGE } from '../../../../shared/github/work-item-ssh-error'
import { GITHUB_WORK_ITEMS_SSH_REMOTE_REQUIRED_MESSAGE } from '../../../../shared/github/work-items/work-item-ssh-error'
import { shouldProbeGitRemote } from '../../../git/remotes/remote-name-listing'
import type { LocalGitExecOptions, OwnerRepo } from '../../gh-utils'
import type { LocalGitExecOptions } from '../../repositories/github-repository-identity'
import type { GitHubRepositoryIdentity as OwnerRepo } from '../../../../shared/github/repositories/repository-identity'
import {
getGitHubApiRepositoryForRemote,
getOriginGitHubApiRepository
} from '../../github-api-repository'
} from '../../repositories/github-api-repository-remote-probe'
import { WORK_ITEM_PR_LIST_JSON_FIELDS, type MainWorkItem } from './../map/work-item-field-coercion'
import { WORK_ITEM_NUMBER_SORT_QUALIFIER, quoteGitHubSearchValue } from './work-item-search-query'
export type WorkItemListRequest = {
@@ -1,16 +1,15 @@
import type { ClassifiedError } from '../../../../shared/classified-error'
import { classifyGitHubUnavailable } from '../../../../shared/github/api-availability'
import { classifyGitHubUnavailable } from '../../../../shared/github/api/api-availability'
import { parseTaskQuery, type ParsedTaskQuery } from '../../../../shared/work-items/task-query'
import { sortWorkItemsByNumber } from '../../../../shared/work-items/work-item-order'
import { classifyListIssuesError, classifyListPrsError } from '../../api/gh-error-classification'
import {
classifyListIssuesError,
classifyListPrsError,
ghRepoExecOptions,
githubRepoContext,
type LocalGitExecOptions,
type OwnerRepo
} from '../../gh-utils'
import { githubHostExecOptions } from '../../github-api-repository'
type LocalGitExecOptions
} from '../../repositories/github-repository-identity'
import type { GitHubRepositoryIdentity as OwnerRepo } from '../../../../shared/github/repositories/repository-identity'
import { githubHostExecOptions } from '../../repositories/github-repository-host'
import { githubPRStackExecutionScope } from './../github-exec-scope'
import { hydrateWorkItemRepositoryMergeMetadata } from './../detect/hydrate-work-item-merge-metadata'
import type { MainWorkItem } from './../map/work-item-field-coercion'
@@ -1,7 +1,8 @@
import { z } from 'zod'
import type { ParsedTaskQuery } from '../../../../shared/work-items/task-query'
import { ghExecFileAsync, type OwnerRepo } from '../../gh-utils'
import type { GitHubRepoExecOptions } from '../../github-api-repository'
import { ghExecFileAsync } from '../../../git/command-runner/hosted-cli/gh-exec-file'
import type { GitHubRepositoryIdentity as OwnerRepo } from '../../../../shared/github/repositories/repository-identity'
import type { GitHubRepoExecOptions } from '../../repositories/github-api-repository'
import { mapPullRequestWorkItem } from '../map/work-item'
import type { MainWorkItem } from '../map/work-item-field-coercion'
import { buildWorkItemListRequest } from './work-item-list-request'
@@ -1,12 +1,13 @@
import { z } from 'zod'
import { githubReadExecutionScope as workItemSearchScope } from '../../github-read-execution-scope'
export { githubReadExecutionScope as workItemSearchScope } from '../../github-read-execution-scope'
import { codeHostReadExecutionScope as workItemSearchScope } from '../../../source-control/code-host-read-execution-scope'
import { BoundedMap } from '../../../../shared/bounded-map'
import { runCoalescedProbe, type CoalescedProbes } from '../../../git/remotes/coalesced-probe'
import { createGhRateLimitBlockedError } from '../../../git/command-runner/hosted-cli/gh-rate-limit-breaker'
import { extractExecError, ghExecFileAsync } from '../../gh-utils'
import { noteRepositoryRateLimitSpend, repositoryRateLimitGuard } from '../../rate-limit'
import type { GitHubRepoExecOptions } from '../../github-api-repository'
import { extractExecError } from '../../../git/command-runner/process-capture/exec-error'
import { ghExecFileAsync } from '../../../git/command-runner/hosted-cli/gh-exec-file'
import { noteRepositoryRateLimitSpend, repositoryRateLimitGuard } from '../../api/rate-limit'
import type { GitHubRepoExecOptions } from '../../repositories/github-api-repository'
const envelopeSchema = z.object({
data: z.record(z.string(), z.unknown()).nullish(),
@@ -0,0 +1,27 @@
import { expect, it, vi } from 'vitest'
import { api, capture } from '../../work-items/work-item-search-test-harness'
import { listWorkItems } from './list-work-items'
import './count-work-items'
import metadata from '../../__fixtures__/work-item-search-metadata.json'
it('hydrates oversized associations with the preferred page credential', async () => {
vi.stubEnv('GH_TOKEN', 'fixture-original-credential')
const row = structuredClone(metadata[0].graphql)
api.specialNodes = [{ ...row, labels: { ...row.labels, pageInfo: { hasNextPage: true } } }]
capture.mockImplementation(async (binary, args, options) => {
if (args.includes('graphql')) {
vi.stubEnv('GH_TOKEN', 'fixture-later-credential')
}
return api.capture(binary, args, options)
})
const result = await listWorkItems('fixture/repo', 24, 'is:issue')
expect(result.items[0].labels).toHaveLength(125)
const queries = api.calls.filter(
(call) =>
call.args.includes('graphql') || call.args.some((arg) => /^repos\/.+\/issues\/\d+$/.test(arg))
)
expect(queries.map((call) => call.fixtureCredential)).toEqual([
'fixture-original-credential',
'fixture-original-credential'
])
})
@@ -1,9 +1,9 @@
import { expect, it } from 'vitest'
import { api } from './work-item-search-test-harness'
import { listWorkItems } from './client/list/list-work-items'
import { countWorkItems } from './client/list/count-work-items'
import './client/list/work-item-search-page'
import '../git/command-runner/hosted-cli/gh-rate-limit-breaker'
import { api } from '../../work-items/work-item-search-test-harness'
import { listWorkItems } from './list-work-items'
import { countWorkItems } from './count-work-items'
import './work-item-search-page'
import '../../../git/command-runner/hosted-cli/gh-rate-limit-breaker'
it('keeps count/list search usable when REST Search is exhausted and reports both-bucket failures', async () => {
api.searchAvailable = false
@@ -1,13 +1,13 @@
import { BoundedMap } from '../../../../shared/bounded-map'
import { isDefaultGitHubHost } from '../../../../shared/github/repository-identity-key'
import type { OwnerRepo } from '../../gh-utils'
import type { GitHubRepoExecOptions } from '../../github-api-repository'
import { isDefaultGitHubHost } from '../../../../shared/github/repositories/repository-identity-key'
import type { GitHubRepositoryIdentity as OwnerRepo } from '../../../../shared/github/repositories/repository-identity'
import type { GitHubRepoExecOptions } from '../../repositories/github-api-repository'
import {
requestWorkItemSearch,
workItemSearchScope,
WORK_ITEM_SEARCH_CACHE_MS,
type WorkItemSearchType
} from './work-item-search-batch'
import { codeHostReadExecutionScope as workItemSearchScope } from '../../../source-control/code-host-read-execution-scope'
type PageInfo = { endCursor: string | null; hasNextPage: boolean }
export type SearchConnection<T> = { issueCount: number; pageInfo: PageInfo; nodes: T[] }
@@ -1,6 +1,7 @@
import { getPRConflictSummary } from '../../conflict-summary'
import type { ghRepoExecOptions, OwnerRepo } from '../../gh-utils'
import { hydrateGitHubPRStack } from '../../github-pr-stack'
import { getPRConflictSummary } from '../../pull-requests/conflicts/conflict-summary'
import type { ghRepoExecOptions } from '../../repositories/github-repository-identity'
import type { GitHubRepositoryIdentity as OwnerRepo } from '../../../../shared/github/repositories/repository-identity'
import { hydrateGitHubPRStack } from '../../pull-requests/stacks/github-pr-stack'
import { detectRepositoryMergeMetadata } from './../detect/repository-merge-metadata'
import { derivePullRequestMergeable, type PullRequestLookupData } from './pull-request-lookup-data'
import { getCachedGitHubPRStackSummary } from './pr-stack-summary-cache'
@@ -1,17 +1,17 @@
import type { PRRefreshOutcome } from '../../../../shared/github/pull-request-refresh-types'
import type { ghRepoExecOptions, OwnerRepo } from '../../gh-utils'
import {
isCommitPartOfMergedPR,
type MergedPRCommitMembership
} from '../../merged-pr-commit-membership'
import type { PRRefreshOutcome } from '../../../../shared/github/pull-requests/refresh/pull-request-refresh-types'
import type { ghRepoExecOptions } from '../../repositories/github-repository-identity'
import type { GitHubRepositoryIdentity as OwnerRepo } from '../../../../shared/github/repositories/repository-identity'
import { isCommitPartOfMergedPR } from '../../pull-requests/refresh/merged-pr-commit-membership'
import type { MergedReviewCommitMembership as MergedPRCommitMembership } from '../../../source-control/merged-review-membership-memo'
import { shouldHideNonOpenReviewOnDefaultBranch } from '../../../source-control/repo-default-branch'
import {
getGitHubApiRepositoryForRemote,
resolveGitHubApiRepositoryCandidates
} from '../../github-api-repository'
import { getGitHubApiRepositoryForRemote } from '../../repositories/github-api-repository-remote-probe'
import { resolveGitHubApiRepositoryCandidates } from '../../repositories/github-api-repository'
import { exactRepositoryCandidates } from '../pull-request-lookup-candidates'
import { mapPRState } from '../../mappers'
import { ownerRepoFromPullRequestUrl } from './../github-exec-scope'
import { mapPRState } from '../../api/github-read-response'
import {
ownerRepoFromPullRequestUrl,
type HostedReviewLocalGitOptions
} from './../github-exec-scope'
import { prRefreshUpstreamError } from './../gh-error-predicates'
import {
isMergedImplicitPR,
@@ -32,7 +32,7 @@ import {
createImplicitDefaultBranchPRPolicy,
type ImplicitDefaultBranchPRContext
} from './implicit-default-branch-pr'
import type { HostedReviewLocalGitOptions } from './../github-exec-scope'
export async function resolvePRForBranchOutcome(input: {
repoPath: string
branchName: string
@@ -1,45 +1,75 @@
import type * as GitExecFileModule from '../git/command-runner/git-exec-file'
import type * as GhExecFileModule from '../git/command-runner/hosted-cli/gh-exec-file'
import type * as GithubRepositoryIdentityModule from '../../repositories/github-repository-identity'
import type * as GhExecFileModule from '../../../git/command-runner/hosted-cli/gh-exec-file'
import type * as GithubApiRepositoryModule from '../../repositories/github-api-repository'
import type * as GithubApiRepositoryRemoteProbeModule from '../../repositories/github-api-repository-remote-probe'
import type * as GitExecFileModule from '../../../git/command-runner/git-exec-file'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import type * as GithubApiRepositoryModule from './github-api-repository'
import type * as GitHubEnterpriseRepositoryModule from './github-enterprise-repository'
import type * as GitHubEnterpriseRepositoryModule from '../../repositories/github-enterprise-repository'
const { clientMocks, moduleMocks } = await vi.hoisted(async () => {
const moduleMocks = await import('./client-test-mocks')
const moduleMocks = await import('../client-test-mocks')
return { clientMocks: moduleMocks.createGitHubClientMocks(), moduleMocks }
})
vi.mock('./gh-utils', () => moduleMocks.ghUtilsModuleMock(clientMocks))
vi.mock('../../execution/github-legacy-command', () =>
moduleMocks.githubLegacyCommandModuleMock(clientMocks)
)
vi.mock('../git/command-runner/git-exec-file', async (importOriginal) => ({
vi.mock('../../repositories/github-owner-repo-selection', () =>
moduleMocks.githubOwnerSelectionModuleMock(clientMocks)
)
vi.mock('../../repositories/github-repository-identity', async (importOriginal) => ({
...(await importOriginal<typeof GithubRepositoryIdentityModule>()),
...moduleMocks.githubRepositoryIdentityModuleMock(clientMocks)
}))
vi.mock('../../api/gh-error-classification', () =>
moduleMocks.githubErrorClassificationModuleMock()
)
vi.mock('../../execution/github-operation-slots', () =>
moduleMocks.githubOperationSlotsModuleMock(clientMocks)
)
vi.mock('../../../git/command-runner/git-exec-file', async (importOriginal) => ({
...(await importOriginal<typeof GitExecFileModule>()),
gitExecFileAsync: clientMocks.gitExecFileAsyncMock
}))
vi.mock('../git/command-runner/hosted-cli/gh-exec-file', async (importOriginal) => ({
vi.mock('../../../git/command-runner/hosted-cli/gh-exec-file', async (importOriginal) => ({
...(await importOriginal<typeof GhExecFileModule>()),
ghExecFileAsync: clientMocks.ghExecFileAsyncMock
...moduleMocks.githubHostedCliModuleMock(clientMocks)
}))
vi.mock('./local-git-config-signature', () =>
vi.mock('../../repositories/local-git-config-signature', () =>
moduleMocks.localGitConfigSignatureModuleMock(clientMocks)
)
vi.mock('./github-enterprise-repository', async (importOriginal) =>
vi.mock('../../repositories/github-enterprise-repository', async (importOriginal) =>
moduleMocks.githubEnterpriseRepositoryModuleMock(
await importOriginal<typeof GitHubEnterpriseRepositoryModule>()
)
)
vi.mock('./rate-limit', () => moduleMocks.rateLimitModuleMock(clientMocks))
vi.mock('./github-api-repository', async (importOriginal) =>
vi.mock('../../api/rate-limit', () => moduleMocks.rateLimitModuleMock(clientMocks))
vi.mock('../../repositories/github-api-repository', async (importOriginal) =>
moduleMocks.githubApiRepositoryModuleMock(
clientMocks,
await importOriginal<typeof GithubApiRepositoryModule>()
)
)
import { getPRForBranch } from './client/lookup/get-pr-for-branch'
import { resetPRForBranchMocks } from './client-test-harness'
import type { RestPullRequest } from './client/lookup/pull-request-lookup-data'
import type { OwnerRepo } from './gh-utils'
vi.mock('../../repositories/github-api-repository-remote-probe', async (importOriginal) =>
moduleMocks.githubApiRepositoryRemoteProbeModuleMock(
clientMocks,
await importOriginal<typeof GithubApiRepositoryRemoteProbeModule>()
)
)
import { getPRForBranch } from './get-pr-for-branch'
import { resetPRForBranchMocks } from '../client-test-harness'
import type { RestPullRequest } from './pull-request-lookup-data'
import type { GitHubRepositoryIdentity as OwnerRepo } from '../../../../shared/github/repositories/repository-identity'
const {
ghExecFileAsyncMock,
@@ -1,43 +1,73 @@
import type * as GitExecFileModule from '../git/command-runner/git-exec-file'
import type * as GhExecFileModule from '../git/command-runner/hosted-cli/gh-exec-file'
import type * as GithubRepositoryIdentityModule from '../../repositories/github-repository-identity'
import type * as GhExecFileModule from '../../../git/command-runner/hosted-cli/gh-exec-file'
import type * as GithubApiRepositoryModule from '../../repositories/github-api-repository'
import type * as GithubApiRepositoryRemoteProbeModule from '../../repositories/github-api-repository-remote-probe'
import type * as GitExecFileModule from '../../../git/command-runner/git-exec-file'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import type * as GithubApiRepositoryModule from './github-api-repository'
import type * as GitHubEnterpriseRepositoryModule from './github-enterprise-repository'
import type * as GitHubEnterpriseRepositoryModule from '../../repositories/github-enterprise-repository'
const { clientMocks, moduleMocks } = await vi.hoisted(async () => {
const moduleMocks = await import('./client-test-mocks')
const moduleMocks = await import('../client-test-mocks')
return { clientMocks: moduleMocks.createGitHubClientMocks(), moduleMocks }
})
vi.mock('./gh-utils', () => moduleMocks.ghUtilsModuleMock(clientMocks))
vi.mock('../../execution/github-legacy-command', () =>
moduleMocks.githubLegacyCommandModuleMock(clientMocks)
)
vi.mock('../git/command-runner/git-exec-file', async (importOriginal) => ({
vi.mock('../../repositories/github-owner-repo-selection', () =>
moduleMocks.githubOwnerSelectionModuleMock(clientMocks)
)
vi.mock('../../repositories/github-repository-identity', async (importOriginal) => ({
...(await importOriginal<typeof GithubRepositoryIdentityModule>()),
...moduleMocks.githubRepositoryIdentityModuleMock(clientMocks)
}))
vi.mock('../../api/gh-error-classification', () =>
moduleMocks.githubErrorClassificationModuleMock()
)
vi.mock('../../execution/github-operation-slots', () =>
moduleMocks.githubOperationSlotsModuleMock(clientMocks)
)
vi.mock('../../../git/command-runner/git-exec-file', async (importOriginal) => ({
...(await importOriginal<typeof GitExecFileModule>()),
gitExecFileAsync: clientMocks.gitExecFileAsyncMock
}))
vi.mock('../git/command-runner/hosted-cli/gh-exec-file', async (importOriginal) => ({
vi.mock('../../../git/command-runner/hosted-cli/gh-exec-file', async (importOriginal) => ({
...(await importOriginal<typeof GhExecFileModule>()),
ghExecFileAsync: clientMocks.ghExecFileAsyncMock
...moduleMocks.githubHostedCliModuleMock(clientMocks)
}))
vi.mock('./local-git-config-signature', () =>
vi.mock('../../repositories/local-git-config-signature', () =>
moduleMocks.localGitConfigSignatureModuleMock(clientMocks)
)
vi.mock('./github-enterprise-repository', async (importOriginal) =>
vi.mock('../../repositories/github-enterprise-repository', async (importOriginal) =>
moduleMocks.githubEnterpriseRepositoryModuleMock(
await importOriginal<typeof GitHubEnterpriseRepositoryModule>()
)
)
vi.mock('./rate-limit', () => moduleMocks.rateLimitModuleMock(clientMocks))
vi.mock('./github-api-repository', async (importOriginal) =>
vi.mock('../../api/rate-limit', () => moduleMocks.rateLimitModuleMock(clientMocks))
vi.mock('../../repositories/github-api-repository', async (importOriginal) =>
moduleMocks.githubApiRepositoryModuleMock(
clientMocks,
await importOriginal<typeof GithubApiRepositoryModule>()
)
)
import { getPRForBranch } from './client/lookup/get-pr-for-branch'
import { resetPRForBranchMocks } from './client-test-harness'
vi.mock('../../repositories/github-api-repository-remote-probe', async (importOriginal) =>
moduleMocks.githubApiRepositoryRemoteProbeModuleMock(
clientMocks,
await importOriginal<typeof GithubApiRepositoryRemoteProbeModule>()
)
)
import { getPRForBranch } from './get-pr-for-branch'
import { resetPRForBranchMocks } from '../client-test-harness'
const {
ghExecFileAsyncMock,
@@ -1,4 +1,4 @@
import type { PRInfo } from '../../../../shared/github/pull-request-types'
import type { PRInfo } from '../../../../shared/github/pull-requests/pull-request-types'
import type { GitHubPRBranchLookupOptions } from './pull-request-lookup-data'
import { getPRForBranchOutcome } from './pr-for-branch-outcome'
/** Linked PR numbers are authoritative; fallback numbers apply only after branch lookup misses. */
@@ -1,11 +1,15 @@
import type { OwnerRepo } from '../../gh-utils'
import { githubRepoIdentityKey } from '../../../../shared/github/repository-identity-key'
import type { GitHubRepositoryIdentity as OwnerRepo } from '../../../../shared/github/repositories/repository-identity'
import { githubRepoIdentityKey } from '../../../../shared/github/repositories/repository-identity-key'
import {
getRemoteHeadBranchName,
getRepoDefaultBranchName
} from '../../../source-control/repo-default-branch'
import type { GhExecOptions, HostedReviewLocalGitOptions } from './../github-exec-scope'
import { ownerRepoFromPullRequestUrl } from './../github-exec-scope'
import {
type GhExecOptions,
type HostedReviewLocalGitOptions,
ownerRepoFromPullRequestUrl
} from './../github-exec-scope'
import type { TrackedUpstreamBranch } from './tracked-upstream-cache'
import {
pullRequestHeadRepository,
@@ -1,44 +1,74 @@
import type * as GitExecFileModule from '../git/command-runner/git-exec-file'
import type * as GhExecFileModule from '../git/command-runner/hosted-cli/gh-exec-file'
import type * as GithubRepositoryIdentityModule from '../../repositories/github-repository-identity'
import type * as GhExecFileModule from '../../../git/command-runner/hosted-cli/gh-exec-file'
import type * as GithubApiRepositoryModule from '../../repositories/github-api-repository'
import type * as GithubApiRepositoryRemoteProbeModule from '../../repositories/github-api-repository-remote-probe'
import type * as GitExecFileModule from '../../../git/command-runner/git-exec-file'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import type * as GithubApiRepositoryModule from './github-api-repository'
import type * as GitHubEnterpriseRepositoryModule from './github-enterprise-repository'
import type * as GitHubEnterpriseRepositoryModule from '../../repositories/github-enterprise-repository'
const { clientMocks, moduleMocks } = await vi.hoisted(async () => {
const moduleMocks = await import('./client-test-mocks')
const moduleMocks = await import('../client-test-mocks')
return { clientMocks: moduleMocks.createGitHubClientMocks(), moduleMocks }
})
vi.mock('./gh-utils', () => moduleMocks.ghUtilsModuleMock(clientMocks))
vi.mock('../../execution/github-legacy-command', () =>
moduleMocks.githubLegacyCommandModuleMock(clientMocks)
)
vi.mock('../git/command-runner/git-exec-file', async (importOriginal) => ({
vi.mock('../../repositories/github-owner-repo-selection', () =>
moduleMocks.githubOwnerSelectionModuleMock(clientMocks)
)
vi.mock('../../repositories/github-repository-identity', async (importOriginal) => ({
...(await importOriginal<typeof GithubRepositoryIdentityModule>()),
...moduleMocks.githubRepositoryIdentityModuleMock(clientMocks)
}))
vi.mock('../../api/gh-error-classification', () =>
moduleMocks.githubErrorClassificationModuleMock()
)
vi.mock('../../execution/github-operation-slots', () =>
moduleMocks.githubOperationSlotsModuleMock(clientMocks)
)
vi.mock('../../../git/command-runner/git-exec-file', async (importOriginal) => ({
...(await importOriginal<typeof GitExecFileModule>()),
gitExecFileAsync: clientMocks.gitExecFileAsyncMock
}))
vi.mock('../git/command-runner/hosted-cli/gh-exec-file', async (importOriginal) => ({
vi.mock('../../../git/command-runner/hosted-cli/gh-exec-file', async (importOriginal) => ({
...(await importOriginal<typeof GhExecFileModule>()),
ghExecFileAsync: clientMocks.ghExecFileAsyncMock
...moduleMocks.githubHostedCliModuleMock(clientMocks)
}))
vi.mock('./local-git-config-signature', () =>
vi.mock('../../repositories/local-git-config-signature', () =>
moduleMocks.localGitConfigSignatureModuleMock(clientMocks)
)
vi.mock('./github-enterprise-repository', async (importOriginal) =>
vi.mock('../../repositories/github-enterprise-repository', async (importOriginal) =>
moduleMocks.githubEnterpriseRepositoryModuleMock(
await importOriginal<typeof GitHubEnterpriseRepositoryModule>()
)
)
vi.mock('./rate-limit', () => moduleMocks.rateLimitModuleMock(clientMocks))
vi.mock('./github-api-repository', async (importOriginal) =>
vi.mock('../../api/rate-limit', () => moduleMocks.rateLimitModuleMock(clientMocks))
vi.mock('../../repositories/github-api-repository', async (importOriginal) =>
moduleMocks.githubApiRepositoryModuleMock(
clientMocks,
await importOriginal<typeof GithubApiRepositoryModule>()
)
)
import { getRestPRForBranch } from './client/lookup/pr-branch-lookup'
vi.mock('../../repositories/github-api-repository-remote-probe', async (importOriginal) =>
moduleMocks.githubApiRepositoryRemoteProbeModuleMock(
clientMocks,
await importOriginal<typeof GithubApiRepositoryRemoteProbeModule>()
)
)
import { resetPRForBranchMocks } from './client-test-harness'
import { getRestPRForBranch } from './pr-branch-lookup'
import { resetPRForBranchMocks } from '../client-test-harness'
const { ghExecFileAsyncMock } = clientMocks
@@ -1,12 +1,16 @@
import { assertRateLimitBudget } from './pr-lookup-rate-limit'
import { selectBranchReview } from '../../../source-control/branch-review-selection'
import { ghExecFileAsync } from '../../gh-utils'
import type { OwnerRepo, ghRepoExecOptions } from '../../gh-utils'
import { ghExecFileAsync } from '../../../git/command-runner/hosted-cli/gh-exec-file'
import type {
GitHubRepositoryIdentity as OwnerRepo,
GitHubRepositoryIdentity as GitHubApiRepository
} from '../../../../shared/github/repositories/repository-identity'
import type { ghRepoExecOptions } from '../../repositories/github-repository-identity'
import {
githubHostExecOptions,
githubRepositoryWebHost,
type GitHubApiRepository
} from '../../github-api-repository'
githubRepositoryWebHost
} from '../../repositories/github-repository-host'
import type { GhExecOptions } from './../github-exec-scope'
import { isNoPullRequestError } from './../gh-error-predicates'
import {
@@ -22,8 +26,8 @@ import {
import { getPRByNumber } from './pr-number-lookup'
import { hydratePullRequestLookupData } from './pull-request-lookup-hydration'
import type { PRProviderReadOptions } from './shared-pr-provider-reads'
import { revalidateGitHubRestJson } from '../../github-rest-revalidation'
import { noteRepositoryRateLimitSpend } from '../../rate-limit'
import { revalidateGitHubRestJson } from '../../api/github-rest-revalidation'
import { noteRepositoryRateLimitSpend } from '../../api/rate-limit'
import { ownerRepoFromPullRequestUrl } from '../github-exec-scope'
const BRANCH_PR_LOOKUP_PAGE_SIZE = 10
@@ -0,0 +1,77 @@
import type * as GithubRepositoryIdentityModule from '../../repositories/github-repository-identity'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import type { PRRefreshOutcome } from '../../../../shared/github/pull-requests/refresh/pull-request-refresh-types'
import type {
GitHubRepoContext,
LocalGitExecOptions
} from '../../repositories/github-repository-identity'
import { __resetHostedReviewBranchCacheForTests } from '../../../source-control/hosted-review-branch-cache'
const mocks = vi.hoisted(() => ({ resolve: vi.fn(), acquire: vi.fn(), release: vi.fn() }))
vi.mock('../../execution/github-operation-slots', () => ({
acquire: mocks.acquire,
release: mocks.release
}))
vi.mock('../../repositories/github-repository-identity', async (importOriginal) => ({
...(await importOriginal<typeof GithubRepositoryIdentityModule>()),
githubRepoContext: (
repoPath: string,
connectionId: string | null,
options: LocalGitExecOptions
) => ({
repoPath,
connectionId,
...options
}),
ghRepoExecOptions: (context: GitHubRepoContext) => context
}))
vi.mock('./branch-lookup-resolution', () => ({
resolvePRForBranchOutcome: mocks.resolve
}))
vi.mock('./pr-for-branch-outcome', async () => ({
getPRForBranchOutcome: (await import('./pr-for-branch-outcome')).getPRForBranchOutcome
}))
vi.mock('../fetch/repo-slug-upstream', async () => ({
getRepoSlug: vi.fn(async () => ({ owner: 'acme', repo: 'orca' }))
}))
vi.mock('./pr-lookup-rate-limit', async () => ({
getGitHubPRLookupRateLimitBlock: vi.fn(async () => null)
}))
vi.mock('../create/create-github-pull-request', async () => ({
createGitHubPullRequest: vi.fn()
}))
vi.mock('../../../gitlab/merge-requests/merge-request-lookup', () => ({
getProjectSlug: vi.fn(async () => null)
}))
import { getPRForBranchOutcome } from './pr-for-branch-outcome'
const outcome: PRRefreshOutcome = { kind: 'no-pr', fetchedAt: 1 }
beforeEach(() => {
vi.clearAllMocks()
mocks.resolve.mockReset()
__resetHostedReviewBranchCacheForTests()
mocks.acquire.mockResolvedValue(undefined)
})
afterEach(() => {
vi.unstubAllEnvs()
vi.useRealTimers()
})
describe('PR lookup coalescing', () => {
it('cleans up errors so later reads can recover', async () => {
mocks.resolve.mockRejectedValueOnce(new Error('network down')).mockResolvedValue(outcome)
const results = await Promise.all([
getPRForBranchOutcome('/repo', 'topic'),
getPRForBranchOutcome('/repo', 'topic')
])
expect(results.every((result) => result.kind === 'upstream-error')).toBe(true)
expect(mocks.resolve).toHaveBeenCalledTimes(1)
await expect(getPRForBranchOutcome('/repo', 'topic')).resolves.toEqual(outcome)
expect(mocks.resolve).toHaveBeenCalledTimes(2)
})
})
@@ -1,13 +1,18 @@
import { withGitHubRepoExecution } from '../../github-repository-identity'
import {
withGitHubRepoExecution,
ghRepoExecOptions,
githubRepoContext
} from '../../repositories/github-repository-identity'
import { getRepoExecutionHostId } from '../../../../shared/execution-host'
import {
hostedReviewRepoScope,
scopeGeneration
} from '../../../source-control/hosted-review-scope-generations'
import { runCoalescedProbe, type CoalescedProbes } from '../../../git/remotes/coalesced-probe'
import { githubReadExecutionScope } from '../../github-read-execution-scope'
import type { PRRefreshOutcome } from '../../../../shared/github/pull-request-refresh-types'
import { acquire, release, ghRepoExecOptions, githubRepoContext } from '../../gh-utils'
import { codeHostReadExecutionScope as githubReadExecutionScope } from '../../../source-control/code-host-read-execution-scope'
import type { PRRefreshOutcome } from '../../../../shared/github/pull-requests/refresh/pull-request-refresh-types'
import { acquire, release } from '../../execution/github-operation-slots'
import { hostedReviewLocalGitOptionArgs, githubPRStackExecutionScope } from './../github-exec-scope'
import type { GitHubPRBranchLookupOptions } from './pull-request-lookup-data'
import { prRefreshUpstreamError } from './../gh-error-predicates'
@@ -1,4 +1,4 @@
import type { PRRefreshOutcome } from '../../../../shared/github/pull-request-refresh-types'
import type { PRRefreshOutcome } from '../../../../shared/github/pull-requests/refresh/pull-request-refresh-types'
// A phone's re-read right after the desktop's own lookup reuses it instead of asking GitHub again.
const PHONE_REUSE_MAX_AGE_MS = 30_000
@@ -0,0 +1,121 @@
import type * as GithubRepositoryIdentityModule from '../../repositories/github-repository-identity'
import type * as GhExecFileModule from '../../../git/command-runner/hosted-cli/gh-exec-file'
import type * as GithubApiRepositoryModule from '../../repositories/github-api-repository'
import type * as GithubApiRepositoryRemoteProbeModule from '../../repositories/github-api-repository-remote-probe'
import type * as GitExecFileModule from '../../../git/command-runner/git-exec-file'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import type * as GitHubEnterpriseRepositoryModule from '../../repositories/github-enterprise-repository'
const { clientMocks, moduleMocks } = await vi.hoisted(async () => {
const moduleMocks = await import('../client-test-mocks')
return { clientMocks: moduleMocks.createGitHubClientMocks(), moduleMocks }
})
vi.mock('../../execution/github-legacy-command', () =>
moduleMocks.githubLegacyCommandModuleMock(clientMocks)
)
vi.mock('../../repositories/github-owner-repo-selection', () =>
moduleMocks.githubOwnerSelectionModuleMock(clientMocks)
)
vi.mock('../../repositories/github-repository-identity', async (importOriginal) => ({
...(await importOriginal<typeof GithubRepositoryIdentityModule>()),
...moduleMocks.githubRepositoryIdentityModuleMock(clientMocks)
}))
vi.mock('../../api/gh-error-classification', () =>
moduleMocks.githubErrorClassificationModuleMock()
)
vi.mock('../../execution/github-operation-slots', () =>
moduleMocks.githubOperationSlotsModuleMock(clientMocks)
)
vi.mock('../../../git/command-runner/git-exec-file', async (importOriginal) => ({
...(await importOriginal<typeof GitExecFileModule>()),
gitExecFileAsync: clientMocks.gitExecFileAsyncMock
}))
vi.mock('../../../git/command-runner/hosted-cli/gh-exec-file', async (importOriginal) => ({
...(await importOriginal<typeof GhExecFileModule>()),
...moduleMocks.githubHostedCliModuleMock(clientMocks)
}))
vi.mock('../../repositories/local-git-config-signature', () =>
moduleMocks.localGitConfigSignatureModuleMock(clientMocks)
)
vi.mock('../../repositories/github-enterprise-repository', async (importOriginal) =>
moduleMocks.githubEnterpriseRepositoryModuleMock(
await importOriginal<typeof GitHubEnterpriseRepositoryModule>()
)
)
vi.mock('../../api/rate-limit', () => moduleMocks.rateLimitModuleMock(clientMocks))
vi.mock('../../repositories/github-api-repository', async (importOriginal) =>
moduleMocks.githubApiRepositoryModuleMock(
clientMocks,
await importOriginal<typeof GithubApiRepositoryModule>()
)
)
vi.mock('../../repositories/github-api-repository-remote-probe', async (importOriginal) =>
moduleMocks.githubApiRepositoryRemoteProbeModuleMock(
clientMocks,
await importOriginal<typeof GithubApiRepositoryRemoteProbeModule>()
)
)
import { getGitHubPRLookupRateLimitBlock } from './pr-lookup-rate-limit'
import { _resetOwnerRepoCache } from '../../repositories/github-repository-identity'
import { resetOriginRepositoryCache } from '../client-test-harness'
import type { RateLimitGuardResult } from '../client-test-mocks'
const {
execFileAsyncMock,
ghExecFileAsyncMock,
getOwnerRepoMock,
getRemoteUrlForRepoMock,
gitExecFileAsyncMock,
getRateLimitMock,
rateLimitGuardMock,
noteRateLimitSpendMock
} = clientMocks
describe('getGitHubPRLookupRateLimitBlock', () => {
beforeEach(() => {
resetOriginRepositoryCache()
execFileAsyncMock.mockReset()
ghExecFileAsyncMock.mockReset()
getOwnerRepoMock.mockReset()
getOwnerRepoMock.mockResolvedValue({ owner: 'acme', repo: 'widgets' })
getRemoteUrlForRepoMock.mockReset()
gitExecFileAsyncMock.mockReset()
getRateLimitMock.mockReset()
getRateLimitMock.mockResolvedValue(undefined)
rateLimitGuardMock.mockReset()
rateLimitGuardMock.mockReturnValue({ blocked: false })
noteRateLimitSpendMock.mockReset()
_resetOwnerRepoCache()
})
it('reports the latest reset when both lookup buckets are exhausted', async () => {
rateLimitGuardMock.mockImplementation(((bucket: string) => ({
blocked: true,
remaining: 4,
limit: 5000,
// Why: core resets first, so returning it would retry into graphql's block.
resetAt: bucket === 'core' ? 1_800_000_000 : 1_800_003_600
})) as () => RateLimitGuardResult)
await expect(getGitHubPRLookupRateLimitBlock('/repo-root')).resolves.toEqual({
resetAt: 1_800_003_600
})
})
it('fails open when the exempt rate-limit probe itself fails', async () => {
getRateLimitMock.mockRejectedValue(new Error('probe offline'))
await expect(getGitHubPRLookupRateLimitBlock('/repo-root')).resolves.toBeNull()
})
})
@@ -1,11 +1,16 @@
import { ghRepoExecOptions, githubRepoContext, type LocalGitExecOptions } from '../../gh-utils'
import { getOriginGitHubApiRepository, type GitHubApiRepository } from '../../github-api-repository'
import {
ghRepoExecOptions,
githubRepoContext,
type LocalGitExecOptions
} from '../../repositories/github-repository-identity'
import { getOriginGitHubApiRepository } from '../../repositories/github-api-repository-remote-probe'
import type { GitHubRepositoryIdentity as GitHubApiRepository } from '../../../../shared/github/repositories/repository-identity'
import {
getRateLimit,
repositoryRateLimitGuard,
spendsSharedGitHubComQuota,
type RateLimitBucketKind
} from '../../rate-limit'
} from '../../api/rate-limit'
import type { GhExecOptions } from './../github-exec-scope'
// REST branch lookup can fall back to GraphQL-backed gh commands; guard and debit both buckets.
export const PR_BRANCH_LOOKUP_BUCKETS = ['core', 'graphql'] as const
@@ -1,7 +1,12 @@
import { classifyPRRefreshError } from '../../pr-refresh-error-classification'
import { ghExecFileAsync } from '../../gh-utils'
import type { OwnerRepo, ghRepoExecOptions } from '../../gh-utils'
import { githubHostExecOptions, type GitHubApiRepository } from '../../github-api-repository'
import { classifyPRRefreshError } from '../../pull-requests/refresh/pr-refresh-error-classification'
import { ghExecFileAsync } from '../../../git/command-runner/hosted-cli/gh-exec-file'
import type {
GitHubRepositoryIdentity as OwnerRepo,
GitHubRepositoryIdentity as GitHubApiRepository
} from '../../../../shared/github/repositories/repository-identity'
import type { ghRepoExecOptions } from '../../repositories/github-repository-identity'
import { githubHostExecOptions } from '../../repositories/github-repository-host'
import {
isNoPullRequestError,
isNotFoundGhError,
@@ -17,8 +22,8 @@ import {
} from './pull-request-lookup-data'
import { hydratePullRequestLookupData } from './pull-request-lookup-hydration'
import { isGitObjectId, isUsableRestStackMetadata } from './rest-stack-metadata-validation'
import { revalidateGitHubRestJson } from '../../github-rest-revalidation'
import { noteRepositoryRateLimitSpend } from '../../rate-limit'
import { revalidateGitHubRestJson } from '../../api/github-rest-revalidation'
import { noteRepositoryRateLimitSpend } from '../../api/rate-limit'
import { ownerRepoFromPullRequestUrl, sameOwnerRepo } from '../github-exec-scope'
import {
readSharedPullRequestSidebarQuery,
@@ -2,14 +2,14 @@ import {
classifyPRCheckRollupResult,
summarizePRCheckRollup
} from '../../../../shared/source-control/checks/pr-check-status'
import type { PRRefreshOutcome } from '../../../../shared/github/pull-request-refresh-types'
import type { PRRefreshOutcome } from '../../../../shared/github/pull-requests/refresh/pull-request-refresh-types'
import type {
PRConflictSummary,
PRMergeableState,
GitHubPRStack
} from '../../../../shared/github/pull-request-types'
import { mapPRState } from '../../mappers'
import type { OwnerRepo } from '../../gh-utils'
} from '../../../../shared/github/pull-requests/pull-request-types'
import { mapPRState } from '../../api/github-read-response'
import type { GitHubRepositoryIdentity as OwnerRepo } from '../../../../shared/github/repositories/repository-identity'
import type { PullRequestLookupData } from './pull-request-lookup-data'
// Main-only: the full rollup stays off the renderer broadcast, beside the outcome it was read for.
@@ -1,8 +1,9 @@
import type { GitHubPRStack } from '../../../../shared/github/pull-request-types'
import type { ghRepoExecOptions } from '../../gh-utils'
import { githubHostExecOptions, type GitHubApiRepository } from '../../github-api-repository'
import { githubReadExecutionScope } from '../../github-read-execution-scope'
import { githubRepoIdentityKey } from '../../../../shared/github/repository-identity-key'
import type { GitHubPRStack } from '../../../../shared/github/pull-requests/pull-request-types'
import type { ghRepoExecOptions } from '../../repositories/github-repository-identity'
import { githubHostExecOptions } from '../../repositories/github-repository-host'
import type { GitHubRepositoryIdentity as GitHubApiRepository } from '../../../../shared/github/repositories/repository-identity'
import { codeHostReadExecutionScope as githubReadExecutionScope } from '../../../source-control/code-host-read-execution-scope'
import { githubRepoIdentityKey } from '../../../../shared/github/repositories/repository-identity-key'
import { getRestPRByNumber } from './pr-number-lookup'
export const PR_STACK_SUMMARY_CACHE_TTL_MS = 60_000
@@ -1,24 +1,28 @@
import { readCurrentReviewHead } from '../../../source-control/review-current-head'
import { shouldHideMergedImplicitReview } from '../../../source-control/branch-review-selection'
import type {
GitHubOwnerRepo,
GitHubRepositoryIdentity as GitHubOwnerRepo,
GitHubRepositoryIdentity as OwnerRepo
} from '../../../../shared/github/repositories/repository-identity'
import type {
GitHubPRMergeMethodSettings,
GitHubPRStack,
PRMergeableState,
PRReviewDecision
} from '../../../../shared/github/pull-request-types'
import { gitExecFileAsync, type OwnerRepo } from '../../gh-utils'
} from '../../../../shared/github/pull-requests/pull-request-types'
import { gitExecFileAsync } from '../../../git/command-runner/git-exec-file'
import type { GitAdmissionTier } from '../../../../shared/rpc-contract/git-admission-tier-params'
import type { HostedReviewExecutionOptions } from '../../../source-control/hosted-review-git-options'
import { mapPRState } from '../../mappers'
import type { MergedPRCommitMembership } from '../../merged-pr-commit-membership'
import { mapPRState } from '../../api/github-read-response'
import type { MergedReviewCommitMembership as MergedPRCommitMembership } from '../../../source-control/merged-review-membership-memo'
import {
normalizePRMergeable,
normalizeReviewDecision,
isAutoMergeEnabled
} from './../map/work-item-field-coercion'
import { ownerRepoFromPullRequestUrl, sameOwnerRepo } from '../github-exec-scope'
import { githubRepositoryWebHost } from '../../github-repository-host'
import { githubRepositoryWebHost } from '../../repositories/github-repository-host'
export type PullRequestLookupData = {
number: number
title: string
@@ -1,7 +1,7 @@
import type { OwnerRepo } from '../../gh-utils'
import type { GitHubRepositoryIdentity as OwnerRepo } from '../../../../shared/github/repositories/repository-identity'
import type { GhExecOptions } from './../github-exec-scope'
import { detectRepositoryMergeMetadata } from './../detect/repository-merge-metadata'
import { mapPRState } from '../../mappers'
import { mapPRState } from '../../api/github-read-response'
import {
normalizePullRequestLookupData,
type PullRequestLookupData
@@ -1,21 +1,19 @@
import { withGitHubRepoExecution } from '../../github-repository-identity'
import type { IssueSourcePreference } from '../../../../shared/repo-types'
import type { GitPushTarget } from '../../../../shared/worktree/types'
import {
ghExecFileAsync,
acquire,
release,
withGitHubRepoExecution,
ghRepoExecOptions,
githubRepoContext,
getRemoteUrlForRepo,
type LocalGitExecOptions
} from '../../gh-utils'
import {
getGitHubApiRepositoryForRemote,
githubHostExecOptions,
type GitHubApiRepository
} from '../../github-api-repository'
import { githubRepoIdentityKey } from '../../../../shared/github/repository-identity-key'
} from '../../repositories/github-repository-identity'
import type { IssueSourcePreference } from '../../../../shared/repo-types'
import type { GitPushTarget } from '../../../../shared/worktree/types'
import { ghExecFileAsync } from '../../../git/command-runner/hosted-cli/gh-exec-file'
import { acquire, release } from '../../execution/github-operation-slots'
import { getGitHubApiRepositoryForRemote } from '../../repositories/github-api-repository-remote-probe'
import { githubHostExecOptions } from '../../repositories/github-repository-host'
import type { GitHubRepositoryIdentity as GitHubApiRepository } from '../../../../shared/github/repositories/repository-identity'
import { githubRepoIdentityKey } from '../../../../shared/github/repositories/repository-identity-key'
import { isNotFoundGhError } from './../gh-error-predicates'
import { resolvePullRequestLookupCandidates } from './../pull-request-lookup-candidates'
export function pickPushRemoteUrl(args: {
@@ -1,27 +1,27 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
const { exec, spend } = vi.hoisted(() => ({ exec: vi.fn(), spend: vi.fn() }))
vi.mock('../git/command-runner/hosted-cli/gh-exec-file', async (importOriginal) => ({
vi.mock('../../../git/command-runner/hosted-cli/gh-exec-file', async (importOriginal) => ({
...(await importOriginal<Record<string, unknown>>()),
ghExecFileAsync: exec
}))
vi.mock('../git/command-runner/git-exec-file', async (importOriginal) => ({
vi.mock('../../../git/command-runner/git-exec-file', async (importOriginal) => ({
...(await importOriginal<Record<string, unknown>>()),
gitExecFileAsync: vi.fn()
}))
vi.mock('./rate-limit', () => ({
vi.mock('../../api/rate-limit', () => ({
noteRepositoryRateLimitSpend: spend,
repositoryRateLimitGuard: () => ({ blocked: false }),
spendsSharedGitHubComQuota: () => false
}))
import { getPRByNumber, lookupPRByNumber } from './client/lookup/pr-number-lookup'
import { _resetPRStackSummaryCacheForTests } from './client/lookup/pr-stack-summary-cache'
import { _resetMergeQueueCacheForTests } from './client/detect/repository-merge-metadata-cache'
import { readPullRequestSidebarQuery } from './client/lookup/pull-request-sidebar-query'
import { createImplicitDefaultBranchPRPolicy } from './client/lookup/implicit-default-branch-pr'
import { deriveCheckStatus } from './mappers'
import { getPRByNumber, lookupPRByNumber } from './pr-number-lookup'
import { _resetPRStackSummaryCacheForTests } from './pr-stack-summary-cache'
import { _resetMergeQueueCacheForTests } from '../detect/repository-merge-metadata-cache'
import { readPullRequestSidebarQuery } from './pull-request-sidebar-query'
import { createImplicitDefaultBranchPRPolicy } from './implicit-default-branch-pr'
import { derivePRCheckStatusFromRollup as deriveCheckStatus } from '../../../../shared/source-control/checks/pr-check-status'
const repository = { owner: 'acme', repo: 'widgets', host: 'github.com' }
const options = { cwd: '/folder-workspace' }
@@ -170,25 +170,6 @@ describe('sidebar PR query', () => {
})
}
)
it.each([
{ data: { repository: { pullRequest: { ...fields, number: 99 } } } },
{
data: { repository: { pullRequest: fields } },
errors: [{ path: ['repository', 'pullRequest', 'headRefOid'] }]
},
{ data: { repository: { pullRequest: fields } }, errors: [{ message: 'Unscoped failure' }] }
])('falls back to the rich CLI read for an unusable response', async (envelope) => {
exec.mockResolvedValueOnce({ stdout: JSON.stringify(envelope) }).mockResolvedValueOnce({
stdout: JSON.stringify({ ...fields, title: 'CLI fallback', statusCheckRollup: [] })
})
await expect(
getPRByNumber(repository, 42, options, 'local:host', undefined, { includeStackSummary: true })
).resolves.toMatchObject({
title: 'CLI fallback'
})
expect(exec.mock.calls[1][0].slice(0, 2)).toEqual(['pr', 'view'])
})
})
it.each([
@@ -225,63 +206,3 @@ it.each([
})
}
)
it.each([1, 10])(
'reuses combined exact hydration with independent context and suite populations at N=%i',
async (n) => {
const contextPages = 2 * n
const suitePages = 3 * n
exec.mockImplementation(async (args: string[]) => {
const contextCursor = args.find((arg) => arg.startsWith('checksCursor='))
const suiteCursor = args.find((arg) => arg.startsWith('suitesCursor='))
const contextIndex = contextCursor ? Number(contextCursor.split('=')[1]) : 0
const suiteIndex = suiteCursor ? Number(suiteCursor.split('=')[1]) : 0
const lastContext = contextIndex === contextPages - 1
const commit = {
oid: fields.headRefOid,
...(!suiteCursor
? {
statusCheckRollup: {
contexts: {
nodes: [
lastContext ? { __typename: 'StatusContext', state: 'FAILURE' } : successCheck
],
pageInfo: {
hasNextPage: !lastContext,
endCursor: lastContext ? null : String(contextIndex + 1)
}
}
}
}
: {}),
...(!contextCursor
? {
checkSuites: {
nodes: [{ databaseId: suiteIndex + 1, conclusion: 'SUCCESS' }],
pageInfo: {
hasNextPage: suiteIndex < suitePages - 1,
endCursor: suiteIndex < suitePages - 1 ? String(suiteIndex + 1) : null
}
}
}
: {})
}
return response({ ...fields, commits: { nodes: [{ commit }] } })
})
const known = {
...fields,
stack: undefined,
statusCheckRollup: [],
headDefaultBranchName: 'trunk'
}
const observed = await getPRByNumber(repository, 42, options, 'local:host', known)
expect(observed).toMatchObject({ headDefaultBranchName: 'trunk', checksUnavailable: false })
expect(observed?.statusCheckRollup).toHaveLength(contextPages)
expect(observed?.statusCheckRollup?.at(-1)).toEqual({
__typename: 'StatusContext',
state: 'FAILURE'
})
expect(exec).toHaveBeenCalledTimes(1 + (contextPages - 1) + (suitePages - 1))
expect(exec.mock.calls.every(([args]) => args[0] === 'api' && args[1] === 'graphql')).toBe(true)
}
)
@@ -1,8 +1,9 @@
import { z } from 'zod'
import { ghExecFileAsync } from '../../gh-utils'
import { ghExecFileAsync } from '../../../git/command-runner/hosted-cli/gh-exec-file'
import { extractExecError } from '../../../git/command-runner/process-capture/exec-error'
import { githubHostExecOptions, type GitHubApiRepository } from '../../github-api-repository'
import { noteRepositoryRateLimitSpend } from '../../rate-limit'
import { githubHostExecOptions } from '../../repositories/github-repository-host'
import type { GitHubRepositoryIdentity as GitHubApiRepository } from '../../../../shared/github/repositories/repository-identity'
import { noteRepositoryRateLimitSpend } from '../../api/rate-limit'
import type { GhExecOptions } from '../github-exec-scope'
import type { PullRequestLookupData } from './pull-request-lookup-data'
import {
@@ -3,8 +3,8 @@ import {
runCoalescedProbe,
type CoalescedProbes
} from '../../../git/remotes/coalesced-probe'
import type { GitHubApiRepository } from '../../github-api-repository'
import { githubReadExecutionScope } from '../../github-read-execution-scope'
import type { GitHubRepositoryIdentity as GitHubApiRepository } from '../../../../shared/github/repositories/repository-identity'
import { codeHostReadExecutionScope as githubReadExecutionScope } from '../../../source-control/code-host-read-execution-scope'
import { readCompletePRCheckObservation } from '../check/complete-pr-check-observation'
import type { GhExecOptions } from '../github-exec-scope'
import type { PullRequestLookupData } from './pull-request-lookup-data'
@@ -2,8 +2,8 @@ import {
readGitReviewBranchSources,
type GitReviewBranchSource
} from '../../../../shared/git/remotes/git-review-branch-source'
import { gitExecFileAsync } from '../../gh-utils'
import { readLocalGitConfigSignature } from '../../local-git-config-signature'
import { gitExecFileAsync } from '../../../git/command-runner/git-exec-file'
import { readLocalGitConfigSignature } from '../../repositories/local-git-config-signature'
import type { GitAdmissionTier } from '../../../../shared/rpc-contract/git-admission-tier-params'
import {
TRACKED_UPSTREAM_SNAPSHOT_CACHE_TTL_MS,

Some files were not shown because too many files have changed in this diff Show More