diff --git a/src/main/github/client.test.ts b/src/main/github/client.test.ts index 45a39c749..2f1919896 100644 --- a/src/main/github/client.test.ts +++ b/src/main/github/client.test.ts @@ -98,6 +98,7 @@ import { mergePR, resolveReviewThread, setPRAutoMerge, + updatePRState, updatePRTitle, _resetOwnerRepoCache, _resetMergeQueueCacheForTests @@ -1278,6 +1279,65 @@ describe('getPRForBranch', () => { }) }) +describe('updatePRState', () => { + beforeEach(() => { + ghExecFileAsyncMock.mockReset() + getOwnerRepoMock.mockReset() + ghRepoExecOptionsMock.mockClear() + githubRepoContextMock.mockClear() + acquireMock.mockReset() + releaseMock.mockReset() + acquireMock.mockResolvedValue(undefined) + _resetOwnerRepoCache() + }) + + it('reopens pull requests through the gh PR command', async () => { + getOwnerRepoMock.mockResolvedValueOnce({ owner: 'stablyai', repo: 'orca' }) + ghExecFileAsyncMock.mockResolvedValueOnce({ stdout: '', stderr: '' }) + + await expect(updatePRState('/repo-root', 3977, { state: 'open' })).resolves.toEqual({ + ok: true + }) + + expect(ghExecFileAsyncMock).toHaveBeenCalledWith( + ['pr', 'reopen', '3977', '--repo', 'stablyai/orca'], + { cwd: '/repo-root' } + ) + expect(acquireMock).toHaveBeenCalledTimes(1) + expect(releaseMock).toHaveBeenCalledTimes(1) + }) + + it('closes pull requests through the gh PR command', async () => { + getOwnerRepoMock.mockResolvedValueOnce({ owner: 'stablyai', repo: 'orca' }) + ghExecFileAsyncMock.mockResolvedValueOnce({ stdout: '', stderr: '' }) + + await expect(updatePRState('/repo-root', 3977, { state: 'closed' })).resolves.toEqual({ + ok: true + }) + + expect(ghExecFileAsyncMock).toHaveBeenCalledWith( + ['pr', 'close', '3977', '--repo', 'stablyai/orca'], + { cwd: '/repo-root' } + ) + }) + + it('reopens SSH-backed pull requests without local cwd options', async () => { + getOwnerRepoMock.mockResolvedValueOnce({ owner: 'stablyai', repo: 'orca' }) + ghExecFileAsyncMock.mockResolvedValueOnce({ stdout: '', stderr: '' }) + + await expect( + updatePRState('/remote/repo-root', 3977, { state: 'open' }, 'ssh-1') + ).resolves.toEqual({ + ok: true + }) + + expect(ghExecFileAsyncMock).toHaveBeenCalledWith( + ['pr', 'reopen', '3977', '--repo', 'stablyai/orca'], + {} + ) + }) +}) + describe('GitHub GraphQL rate-limit guard', () => { beforeEach(() => { execFileAsyncMock.mockReset() diff --git a/src/main/github/client.ts b/src/main/github/client.ts index 883e6cab6..834cfe7d9 100644 --- a/src/main/github/client.ts +++ b/src/main/github/client.ts @@ -3254,16 +3254,14 @@ export async function updatePRState( await acquire() try { + const cmd = updates.state === 'closed' ? 'close' : 'reopen' + // Why: GitHub can reject REST pull state PATCHes for reopen paths with a + // generic 422; gh's PR commands use GitHub's supported reopen flow. await ghExecFileAsync( - [ - 'api', - '-X', - 'PATCH', - `repos/${ownerRepo.owner}/${ownerRepo.repo}/pulls/${prNumber}`, - '--raw-field', - `state=${updates.state}` - ], - ghOptions + ['pr', cmd, String(prNumber), '--repo', `${ownerRepo.owner}/${ownerRepo.repo}`], + { + ...ghOptions + } ) return { ok: true } } catch (err) {