mirror of
https://github.com/stablyai/orca.git
synced 2026-10-21 16:03:26 +00:00
- 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.
243 lines
7.4 KiB
TypeScript
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
|
|
)
|
|
})
|
|
})
|