Files
orca/src/main/github/issues.test.ts
T

142 lines
4.5 KiB
TypeScript

import { beforeEach, describe, expect, it, vi } from 'vitest'
import type * as GhUtils from './gh-utils'
const {
ghExecFileAsyncMock,
getIssueOwnerRepoMock,
resolveIssueSourceMock,
acquireMock,
releaseMock
} = vi.hoisted(() => ({
ghExecFileAsyncMock: vi.fn(),
getIssueOwnerRepoMock: vi.fn(),
resolveIssueSourceMock: vi.fn(),
acquireMock: vi.fn(),
releaseMock: vi.fn()
}))
vi.mock('./gh-utils', async () => {
const actual = await vi.importActual<typeof GhUtils>('./gh-utils')
return {
...actual,
ghExecFileAsync: ghExecFileAsyncMock,
getIssueOwnerRepo: getIssueOwnerRepoMock,
resolveIssueSource: resolveIssueSourceMock,
acquire: acquireMock,
release: releaseMock
}
})
import { createIssue, getIssue, listIssues, updateIssue } from './issues'
describe('issue source operations', () => {
beforeEach(() => {
ghExecFileAsyncMock.mockReset()
getIssueOwnerRepoMock.mockReset()
resolveIssueSourceMock.mockReset()
acquireMock.mockReset()
releaseMock.mockReset()
acquireMock.mockResolvedValue(undefined)
// Why: preference-aware paths call resolveIssueSource instead of
// getIssueOwnerRepo. Route through the same mock so existing tests that
// set up getIssueOwnerRepoMock continue to work.
resolveIssueSourceMock.mockImplementation(async () => ({
source: await getIssueOwnerRepoMock(),
fellBack: false
}))
})
it('gets a single issue from the issue owner/repo', async () => {
getIssueOwnerRepoMock.mockResolvedValueOnce({ owner: 'stablyai', repo: 'orca' })
ghExecFileAsyncMock.mockResolvedValueOnce({
stdout: JSON.stringify({
number: 923,
title: 'Use upstream issues',
state: 'open',
html_url: 'https://github.com/stablyai/orca/issues/923',
labels: []
})
})
await expect(getIssue('/repo-root', 923)).resolves.toMatchObject({ number: 923 })
expect(ghExecFileAsyncMock).toHaveBeenCalledWith(
['api', '--cache', '300s', 'repos/stablyai/orca/issues/923'],
{ cwd: '/repo-root' }
)
})
it('lists issues from the issue owner/repo', async () => {
getIssueOwnerRepoMock.mockResolvedValueOnce({ owner: 'stablyai', repo: 'orca' })
ghExecFileAsyncMock.mockResolvedValueOnce({ stdout: '[]' })
await expect(listIssues('/repo-root', 5)).resolves.toEqual({ items: [] })
expect(ghExecFileAsyncMock).toHaveBeenCalledWith(
[
'api',
'--cache',
'120s',
'repos/stablyai/orca/issues?per_page=5&state=open&sort=updated&direction=desc'
],
{ cwd: '/repo-root' }
)
})
it('surfaces a classified permission_denied error instead of collapsing to empty', async () => {
// Why: parent design doc §3 — a 403 on a private upstream must not
// masquerade as "No issues". The envelope carries an error the UI can
// render as a banner with retry, not a silent empty list.
getIssueOwnerRepoMock.mockResolvedValueOnce({ owner: 'stablyai', repo: 'orca' })
ghExecFileAsyncMock.mockRejectedValueOnce(
new Error('HTTP 403: Resource not accessible by integration')
)
const result = await listIssues('/repo-root', 5)
expect(result.items).toEqual([])
expect(result.error?.type).toBe('permission_denied')
})
it('creates issues in the issue owner/repo', async () => {
getIssueOwnerRepoMock.mockResolvedValueOnce({ owner: 'stablyai', repo: 'orca' })
ghExecFileAsyncMock.mockResolvedValueOnce({
stdout: JSON.stringify({
number: 924,
html_url: 'https://github.com/stablyai/orca/issues/924'
})
})
await expect(createIssue('/repo-root', 'New issue', 'Body')).resolves.toEqual({
ok: true,
number: 924,
url: 'https://github.com/stablyai/orca/issues/924'
})
expect(ghExecFileAsyncMock).toHaveBeenCalledWith(
[
'api',
'-X',
'POST',
'repos/stablyai/orca/issues',
'--raw-field',
'title=New issue',
'--raw-field',
'body=Body'
],
{ cwd: '/repo-root' }
)
})
it('updates issue body through the REST issue endpoint', async () => {
getIssueOwnerRepoMock.mockResolvedValueOnce({ owner: 'stablyai', repo: 'orca' })
ghExecFileAsyncMock.mockResolvedValueOnce({ stdout: '' })
await expect(updateIssue('/repo-root', 924, { body: 'Updated body' })).resolves.toEqual({
ok: true
})
expect(ghExecFileAsyncMock).toHaveBeenCalledWith(
['api', '-X', 'PATCH', 'repos/stablyai/orca/issues/924', '--raw-field', 'body=Updated body'],
{ cwd: '/repo-root' }
)
})
})