mirror of
https://github.com/stablyai/orca.git
synced 2026-11-15 10:27:33 +00:00
refactor(github): organize concrete feature owners (#41601)
This commit is contained in:
+7
-4
@@ -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,
|
||||
+1
-1
@@ -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
|
||||
+3
-3
@@ -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,
|
||||
+4
-4
@@ -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
|
||||
+24
-24
@@ -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?.())
|
||||
+10
-10
@@ -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[]
|
||||
+29
-92
@@ -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(() =>
|
||||
+2
-2
@@ -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)
|
||||
}
|
||||
+6
-19
@@ -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"'))
|
||||
+7
-9
@@ -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
|
||||
+8
-3
@@ -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)
|
||||
})
|
||||
})
|
||||
@@ -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'
|
||||
|
||||
+4
-4
@@ -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'
|
||||
|
||||
+26
-14
@@ -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,
|
||||
|
||||
@@ -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'
|
||||
|
||||
+10
-10
@@ -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()
|
||||
+85
-42
@@ -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(
|
||||
|
||||
+26
-51
@@ -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
|
||||
|
||||
|
||||
+6
-4
@@ -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'
|
||||
|
||||
|
||||
+35
-15
@@ -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 {
|
||||
+30
-12
@@ -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>
|
||||
+69
-22
@@ -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(() => {
|
||||
+46
-16
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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'
|
||||
|
||||
+43
-22
@@ -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'
|
||||
])
|
||||
})
|
||||
+5
-5
@@ -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
|
||||
|
||||
+47
-17
@@ -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,
|
||||
+45
-15
@@ -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,
|
||||
|
||||
+45
-15
@@ -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: {
|
||||
|
||||
+9
-88
@@ -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
Reference in New Issue
Block a user