Files
orca/src/main/github/client-pr-local-runtime.test.ts
T
Jinjing dd0fa77882 Enhance PR auto-merge controls and switch to GraphQL mutation (#6405)
- Centralize and align auto-merge eligibility logic across web and
  mobile clients.
- Use the `enablePullRequestAutoMerge` GraphQL mutation instead of
  `gh pr merge --auto` to prevent immediate merges on clean branches.
- Fall back to `gh pr merge --auto` when a merge queue is required on
  the base branch.
- Hide the auto-merge control when only optional checks are pending.
2026-06-25 22:00:34 -07:00

243 lines
7.4 KiB
TypeScript

import { beforeEach, describe, expect, it, vi } from 'vitest'
type RateLimitGuardResult =
| { blocked: false }
| { blocked: true; remaining: number; limit: number; resetAt: number }
const {
execFileAsyncMock,
ghExecFileAsyncMock,
getOwnerRepoMock,
getIssueOwnerRepoMock,
getOwnerRepoForRemoteMock,
resolvePRRepositoryCandidatesMock,
getRemoteUrlForRepoMock,
gitExecFileAsyncMock,
getRateLimitMock,
rateLimitGuardMock,
noteRateLimitSpendMock,
acquireMock,
releaseMock
} = vi.hoisted(() => ({
execFileAsyncMock: vi.fn(),
ghExecFileAsyncMock: vi.fn(),
getOwnerRepoMock: vi.fn(),
getIssueOwnerRepoMock: vi.fn(),
getOwnerRepoForRemoteMock: vi.fn(),
resolvePRRepositoryCandidatesMock: vi.fn(),
getRemoteUrlForRepoMock: vi.fn(),
gitExecFileAsyncMock: vi.fn(),
getRateLimitMock: vi.fn(),
rateLimitGuardMock: vi.fn<() => RateLimitGuardResult>(() => ({ blocked: false })),
noteRateLimitSpendMock: vi.fn(),
acquireMock: vi.fn(),
releaseMock: vi.fn()
}))
vi.mock('./gh-utils', () => ({
execFileAsync: execFileAsyncMock,
ghExecFileAsync: ghExecFileAsyncMock,
gitExecFileAsync: gitExecFileAsyncMock,
extractExecError: (err: unknown) => ({
stderr: err instanceof Error ? err.message : String(err),
stdout: ''
}),
acquire: acquireMock,
release: releaseMock,
getOwnerRepo: getOwnerRepoMock,
getIssueOwnerRepo: getIssueOwnerRepoMock,
getOwnerRepoForRemote: getOwnerRepoForRemoteMock,
resolvePRRepositoryCandidates: resolvePRRepositoryCandidatesMock,
resolveIssueSource: vi.fn(),
classifyGhError: (message: string) => ({ type: 'unknown', message }),
classifyListIssuesError: (message: string) => ({ type: 'unknown', message }),
ghRepoExecOptions: (context: {
repoPath: string
connectionId?: string | null
wslDistro?: string
}) =>
context.connectionId
? {}
: { cwd: context.repoPath, ...(context.wslDistro ? { wslDistro: context.wslDistro } : {}) },
githubRepoContext: (
repoPath: string,
connectionId?: string | null,
localGitOptions: { wslDistro?: string } = {}
) => ({
repoPath,
connectionId: connectionId ?? null,
...localGitOptions
}),
getRemoteUrlForRepo: getRemoteUrlForRepoMock,
_resetOwnerRepoCache: vi.fn()
}))
vi.mock('../git/runner', () => ({
gitExecFileAsync: gitExecFileAsyncMock
}))
vi.mock('../providers/ssh-git-dispatch', () => ({
getSshGitProvider: vi.fn()
}))
vi.mock('./conflict-summary', () => ({
getPRConflictSummary: vi.fn()
}))
vi.mock('./rate-limit', () => ({
getRateLimit: getRateLimitMock,
rateLimitGuard: rateLimitGuardMock,
noteRateLimitSpend: noteRateLimitSpendMock
}))
import {
addPRReviewComment,
addPRReviewCommentReply,
getPRComments,
mergePR,
removePRReviewers,
requestPRReviewers,
resolveReviewThread,
setPRAutoMerge,
updatePRDetails,
updatePRState,
updatePRTitle
} from './client'
describe('GitHub PR local runtime routing', () => {
beforeEach(() => {
execFileAsyncMock.mockReset()
ghExecFileAsyncMock.mockReset()
getOwnerRepoMock.mockReset()
getIssueOwnerRepoMock.mockReset()
getOwnerRepoForRemoteMock.mockReset()
resolvePRRepositoryCandidatesMock.mockReset()
getRemoteUrlForRepoMock.mockReset()
gitExecFileAsyncMock.mockReset()
getRateLimitMock.mockReset()
getRateLimitMock.mockResolvedValue({ resources: {} })
rateLimitGuardMock.mockReset()
rateLimitGuardMock.mockReturnValue({ blocked: false })
noteRateLimitSpendMock.mockReset()
acquireMock.mockReset()
releaseMock.mockReset()
acquireMock.mockResolvedValue(undefined)
})
it('routes PR details and mutations through the selected WSL distro', async () => {
const localGitOptions = { wslDistro: 'Ubuntu' }
const prRepo = { owner: 'acme', repo: 'orca' }
getOwnerRepoMock.mockResolvedValue(prRepo)
ghExecFileAsyncMock.mockImplementation(async (args: string[]) => {
const endpoint = args.find((arg) => arg.startsWith('repos/acme/orca/')) ?? ''
const query = args.find((arg) => arg.startsWith('query=')) ?? ''
if (args[0] === 'pr' && args[1] === 'view') {
return {
stdout: JSON.stringify({
id: 'PR_kwDO123',
number: 7,
title: 'PR',
state: 'OPEN',
url: 'https://github.com/acme/orca/pull/7',
statusCheckRollup: [],
updatedAt: '2026-04-01T00:00:00Z',
isDraft: false,
mergeable: 'MERGEABLE',
reviewDecision: 'APPROVED',
mergeStateStatus: 'CLEAN',
autoMergeRequest: null,
baseRefName: 'main',
baseRefOid: 'base-oid',
headRefOid: 'head-oid'
})
}
}
if (query.includes('reviewThreads')) {
return {
stdout: JSON.stringify({
data: {
repository: {
pullRequest: {
reviewThreads: { nodes: [] },
comments: { nodes: [] }
}
}
}
})
}
}
if (endpoint.endsWith('/issues/7/comments?per_page=100')) {
return { stdout: '[]' }
}
if (endpoint.endsWith('/pulls/7/reviews?per_page=100')) {
return { stdout: '[]' }
}
if (endpoint.endsWith('/pulls/7/comments/11/replies')) {
return { stdout: JSON.stringify({ id: 12, user: null, body: 'Reply' }) }
}
if (endpoint.endsWith('/pulls/7/comments')) {
return { stdout: JSON.stringify({ id: 13, user: null, body: 'Inline' }) }
}
return { stdout: '', stderr: '' }
})
await getPRComments('/repo-root', 7, { prRepo }, null, localGitOptions)
await expect(
resolveReviewThread('/repo-root', 'thread-1', true, null, localGitOptions)
).resolves.toBe(true)
await expect(
addPRReviewCommentReply(
'/repo-root',
7,
11,
'Reply',
'thread-1',
'src/app.ts',
10,
null,
prRepo,
localGitOptions
)
).resolves.toMatchObject({ ok: true })
await expect(
addPRReviewComment({
repoPath: '/repo-root',
connectionId: null,
localGitOptions,
prNumber: 7,
body: 'Inline',
commitId: 'head-oid',
path: 'src/app.ts',
line: 10
})
).resolves.toMatchObject({ ok: true })
await expect(
updatePRTitle('/repo-root', 7, 'New title', null, prRepo, localGitOptions)
).resolves.toBe(true)
await expect(
updatePRDetails('/repo-root', 7, { body: 'New body' }, null, prRepo, localGitOptions)
).resolves.toEqual({ ok: true })
await expect(
updatePRState('/repo-root', 7, { state: 'closed' }, null, localGitOptions)
).resolves.toEqual({ ok: true })
await expect(
requestPRReviewers('/repo-root', 7, ['octo'], null, localGitOptions)
).resolves.toEqual({ ok: true })
await expect(
removePRReviewers('/repo-root', 7, ['octo'], null, localGitOptions)
).resolves.toEqual({ ok: true })
await expect(
setPRAutoMerge('/repo-root', 7, true, 'squash', null, prRepo, localGitOptions)
).resolves.toEqual({ ok: true })
await expect(
mergePR('/repo-root', 7, 'squash', null, prRepo, localGitOptions)
).resolves.toEqual({ ok: true })
expect(getOwnerRepoMock).toHaveBeenCalledWith('/repo-root', null, localGitOptions)
expect(ghExecFileAsyncMock.mock.calls.every((call) => call[1]?.wslDistro === 'Ubuntu')).toBe(
true
)
})
})