mirror of
https://github.com/stablyai/orca.git
synced 2026-10-14 08:02:56 +00:00
Keeps a fork issue’s details, metadata and edits bound to the repository the user opened, including same-number issues in fork and upstream. Repairs selected-assignee leakage and delayed failed edits repainting another issue. Fixes #24378 Incorporates and cross-reviews contributor PR #24379, including its source-resolver correction and regression material. Covers the contributor PR’s Project-row identity and retained-dialog mutation findings. The final published head passes focused tests, hidden macOS rendering and current CI; the callback-timing bot thread has an evidence-based response. Co-authored-by: Katsuma Takehisa <k.takehisa@nissogr.com>
179 lines
5.6 KiB
TypeScript
179 lines
5.6 KiB
TypeScript
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, resolveIssueSourceMock, acquireMock, releaseMock } = vi.hoisted(
|
|
() => ({
|
|
ghExecFileAsyncMock: 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,
|
|
acquire: acquireMock,
|
|
release: releaseMock
|
|
}
|
|
})
|
|
|
|
vi.mock('./github-api-repository', async (importOriginal) => ({
|
|
...(await importOriginal<typeof GithubApiRepositoryModule>()),
|
|
resolveIssueGitHubApiRepositorySource: resolveIssueSourceMock
|
|
}))
|
|
|
|
import { addIssueComment } from './issue-comment'
|
|
import { updateIssue } from './issue-update'
|
|
|
|
const ORIGIN = { owner: 'fork-owner', repo: 'widgets', host: 'github.com' }
|
|
const UPSTREAM = { owner: 'upstream-owner', repo: 'widgets', host: 'github.com' }
|
|
|
|
describe('issue mutations follow the issue source preference', () => {
|
|
beforeEach(() => {
|
|
ghExecFileAsyncMock.mockReset()
|
|
acquireMock.mockReset()
|
|
acquireMock.mockResolvedValue(undefined)
|
|
releaseMock.mockReset()
|
|
resolveIssueSourceMock.mockReset()
|
|
resolveIssueSourceMock.mockImplementation(async (_repoPath: string, preference: unknown) => ({
|
|
source: preference === 'origin' ? ORIGIN : UPSTREAM,
|
|
fellBack: false
|
|
}))
|
|
})
|
|
|
|
it('posts issue comments to origin when origin is selected', async () => {
|
|
ghExecFileAsyncMock.mockResolvedValueOnce({
|
|
stdout: JSON.stringify({ id: 1, user: { login: 'octo', avatar_url: '' }, body: 'hi' })
|
|
})
|
|
|
|
await expect(
|
|
addIssueComment('/repo-root', 5, 'hi', null, null, {}, 'origin')
|
|
).resolves.toMatchObject({ ok: true })
|
|
|
|
expect(resolveIssueSourceMock).toHaveBeenCalledWith('/repo-root', 'origin', null, {})
|
|
expect(ghExecFileAsyncMock.mock.calls[0][0]).toContain(
|
|
'repos/fork-owner/widgets/issues/5/comments'
|
|
)
|
|
})
|
|
|
|
it('keeps an explicit owner/repo override ahead of the preference', async () => {
|
|
ghExecFileAsyncMock.mockResolvedValueOnce({
|
|
stdout: JSON.stringify({ id: 2, user: { login: 'octo', avatar_url: '' }, body: 'hi' })
|
|
})
|
|
|
|
await addIssueComment('/repo-root', 7, 'hi', null, UPSTREAM, {}, 'origin')
|
|
|
|
expect(resolveIssueSourceMock).not.toHaveBeenCalled()
|
|
expect(ghExecFileAsyncMock.mock.calls[0][0]).toContain(
|
|
'repos/upstream-owner/widgets/issues/7/comments'
|
|
)
|
|
})
|
|
|
|
it('edits issues on origin when origin is selected', async () => {
|
|
ghExecFileAsyncMock.mockResolvedValue({ stdout: '' })
|
|
|
|
await expect(
|
|
updateIssue('/repo-root', 5, { body: 'Updated', addAssignees: ['octo'] }, null, {}, 'origin')
|
|
).resolves.toEqual({ ok: true })
|
|
|
|
expect(resolveIssueSourceMock).toHaveBeenCalledWith('/repo-root', 'origin', null, {})
|
|
expect(ghExecFileAsyncMock.mock.calls[0][0]).toContain('repos/fork-owner/widgets/issues/5')
|
|
expect(ghExecFileAsyncMock.mock.calls[1][0]).toEqual([
|
|
'issue',
|
|
'edit',
|
|
'5',
|
|
'--repo',
|
|
'fork-owner/widgets',
|
|
'--add-assignee',
|
|
'octo'
|
|
])
|
|
})
|
|
|
|
it('keeps the upstream-first default when no preference is set', async () => {
|
|
ghExecFileAsyncMock.mockResolvedValue({ stdout: '' })
|
|
|
|
await updateIssue('/repo-root', 5, { body: 'Updated' })
|
|
|
|
expect(resolveIssueSourceMock).toHaveBeenCalledWith('/repo-root', undefined, undefined, {})
|
|
expect(ghExecFileAsyncMock.mock.calls[0][0]).toContain('repos/upstream-owner/widgets/issues/5')
|
|
})
|
|
|
|
it('keeps edits on the opened issue when the saved preference changes', async () => {
|
|
ghExecFileAsyncMock.mockResolvedValue({ stdout: '' })
|
|
|
|
await updateIssue(
|
|
'/repo-root',
|
|
5,
|
|
{ body: 'Origin edit', state: 'closed', addLabels: ['bug'], addAssignees: ['octo'] },
|
|
null,
|
|
{},
|
|
'upstream',
|
|
ORIGIN
|
|
)
|
|
|
|
expect(resolveIssueSourceMock).not.toHaveBeenCalled()
|
|
expect(ghExecFileAsyncMock.mock.calls.map(([args]) => args)).toEqual([
|
|
['issue', 'close', '5', '--repo', 'fork-owner/widgets'],
|
|
[
|
|
'api',
|
|
'-X',
|
|
'PATCH',
|
|
'repos/fork-owner/widgets/issues/5',
|
|
'--raw-field',
|
|
'body=Origin edit'
|
|
],
|
|
[
|
|
'issue',
|
|
'edit',
|
|
'5',
|
|
'--repo',
|
|
'fork-owner/widgets',
|
|
'--add-label',
|
|
'bug',
|
|
'--add-assignee',
|
|
'octo'
|
|
]
|
|
])
|
|
})
|
|
|
|
it('preserves local WSL execution with an explicit issue repository', async () => {
|
|
ghExecFileAsyncMock.mockResolvedValue({ stdout: '' })
|
|
|
|
await updateIssue(
|
|
'/home/fixture/widgets',
|
|
5,
|
|
{ body: 'Origin edit' },
|
|
null,
|
|
{ wslDistro: 'Ubuntu' },
|
|
'upstream',
|
|
ORIGIN
|
|
)
|
|
|
|
expect(ghExecFileAsyncMock).toHaveBeenCalledWith(
|
|
expect.arrayContaining(['repos/fork-owner/widgets/issues/5']),
|
|
expect.objectContaining({
|
|
cwd: '/home/fixture/widgets',
|
|
wslDistro: 'Ubuntu',
|
|
host: 'github.com'
|
|
})
|
|
)
|
|
})
|
|
|
|
it('refuses unresolved Origin mutations instead of using ambient gh', async () => {
|
|
resolveIssueSourceMock.mockResolvedValue({ source: null, fellBack: false })
|
|
|
|
await expect(
|
|
updateIssue('/repo-root', 5, { body: 'Origin edit' }, null, {}, 'origin')
|
|
).resolves.toMatchObject({ ok: false })
|
|
await expect(
|
|
addIssueComment('/repo-root', 5, 'Origin reply', null, null, {}, 'origin')
|
|
).resolves.toMatchObject({ ok: false })
|
|
|
|
expect(ghExecFileAsyncMock).not.toHaveBeenCalled()
|
|
})
|
|
})
|