mirror of
https://github.com/stablyai/orca.git
synced 2026-10-31 00:57:45 +00:00
* refactor(providers): delete the SSH git and filesystem providers and relay-only wiring * refactor(ssh): drop relay-protocol exports only the deleted stream readers used --------- Co-authored-by: m4air <m4air@Mac.localdomain>
163 lines
5.9 KiB
TypeScript
163 lines
5.9 KiB
TypeScript
import { setImmediate as nextTurn } from 'node:timers/promises'
|
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
import {
|
|
handlers,
|
|
store,
|
|
WORKTREE_FEATURE_PATH,
|
|
resolveCommitMessageSettingsMock,
|
|
generatePullRequestFieldsFromContextMock,
|
|
getPullRequestDraftContextMock,
|
|
resolveHostedReviewBodyForGenerationMock,
|
|
loadPullRequestLinkedIssueMock,
|
|
resetFilesystemIpcMocks
|
|
} from './filesystem-test-harness'
|
|
|
|
const linkedLookup = vi.hoisted(() => ({ run: (): Promise<null> => Promise.resolve(null) }))
|
|
|
|
vi.mock('electron', async () => (await import('./filesystem-test-harness')).electronMock)
|
|
vi.mock('fs/promises', async () => (await import('./filesystem-test-harness')).fsPromisesMock)
|
|
vi.mock(
|
|
'../wsl-unc-delete',
|
|
async () => (await import('./filesystem-test-harness')).wslUncDeleteMock
|
|
)
|
|
vi.mock(
|
|
'../crash-reporting/crash-breadcrumb-store',
|
|
async () => (await import('./filesystem-test-harness')).crashBreadcrumbMock
|
|
)
|
|
vi.mock(
|
|
'../git/status',
|
|
async () => (await import('./filesystem-test-harness')).gitStatusModuleMock
|
|
)
|
|
vi.mock(
|
|
'../git/check-ignored-paths',
|
|
async () => (await import('./filesystem-test-harness')).gitIgnoredPathsMock
|
|
)
|
|
vi.mock('../git/worktree', async () => (await import('./filesystem-test-harness')).gitWorktreeMock)
|
|
vi.mock(
|
|
'../providers/ssh-filesystem-dispatch',
|
|
async () => (await import('./filesystem-test-harness')).sshFilesystemDispatchMock
|
|
)
|
|
vi.mock(
|
|
'../text-generation/commit-message-text-generation',
|
|
async () => (await import('./filesystem-test-harness')).textGenerationModuleMock
|
|
)
|
|
vi.mock(
|
|
'../text-generation/pull-request-context',
|
|
async () => (await import('./filesystem-test-harness')).pullRequestContextMock
|
|
)
|
|
vi.mock(
|
|
'../source-control/pull-request-template',
|
|
async () => (await import('./filesystem-test-harness')).pullRequestTemplateMock
|
|
)
|
|
vi.mock('../source-control/pull-request-linked-issue', async () => {
|
|
const { loadPullRequestLinkedIssueMock } = await import('./filesystem-test-harness')
|
|
return {
|
|
loadPullRequestLinkedIssue: (...args: unknown[]) => {
|
|
loadPullRequestLinkedIssueMock(...args)
|
|
// A vi.fn return observer would itself handle the rejection under test.
|
|
return linkedLookup.run()
|
|
}
|
|
}
|
|
})
|
|
|
|
import { registerFilesystemHandlers } from './filesystem'
|
|
import { invalidateAuthorizedRootsCache } from './registered-worktree-roots-cache'
|
|
|
|
describe('PR linked-issue lifetime on a local worktree', () => {
|
|
const draftContext = {
|
|
base: 'main',
|
|
branch: 'feature/ai',
|
|
branchChangedByPreparation: false,
|
|
commitSummary: 'a1b2c3d Add generation',
|
|
changeSummary: 'README.md | 2 +-',
|
|
patch: '+hello',
|
|
currentTitle: '',
|
|
currentBody: '',
|
|
currentDraft: false
|
|
}
|
|
const request = {
|
|
base: 'main',
|
|
title: '',
|
|
body: '',
|
|
draft: false,
|
|
worktreePath: WORKTREE_FEATURE_PATH
|
|
}
|
|
|
|
beforeEach(() => {
|
|
resetFilesystemIpcMocks()
|
|
linkedLookup.run = () => Promise.resolve(null)
|
|
invalidateAuthorizedRootsCache()
|
|
resolveCommitMessageSettingsMock.mockReturnValue({
|
|
ok: true,
|
|
params: { agentId: 'codex', model: 'gpt-5.4-mini' }
|
|
})
|
|
resolveHostedReviewBodyForGenerationMock.mockResolvedValue('')
|
|
getPullRequestDraftContextMock.mockResolvedValue(draftContext)
|
|
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: The existing IPC harness provides every Store member reached by this handler.
|
|
registerFilesystemHandlers(store as never)
|
|
})
|
|
|
|
it.each(['no changes', 'context error', 'template error'])(
|
|
'observes a late lookup rejection after %s',
|
|
async (outcome) => {
|
|
const issue = Promise.withResolvers<null>()
|
|
linkedLookup.run = () => issue.promise
|
|
const preparationError = new Error('branch preparation failed')
|
|
if (outcome === 'no changes') {
|
|
getPullRequestDraftContextMock.mockResolvedValue(null)
|
|
} else if (outcome === 'context error') {
|
|
getPullRequestDraftContextMock.mockRejectedValue(preparationError)
|
|
} else {
|
|
resolveHostedReviewBodyForGenerationMock.mockRejectedValue(preparationError)
|
|
}
|
|
const unhandled = vi.fn()
|
|
process.on('unhandledRejection', unhandled)
|
|
try {
|
|
await expect(
|
|
handlers.get('git:generatePullRequestFields')!(null, request)
|
|
).resolves.toEqual({
|
|
success: false,
|
|
error:
|
|
outcome === 'no changes' ? 'No branch changes to summarize.' : preparationError.message
|
|
})
|
|
expect(loadPullRequestLinkedIssueMock).toHaveBeenCalledTimes(1)
|
|
issue.reject(new Error('Timed out waiting for a GitLab operation slot.'))
|
|
await nextTurn()
|
|
expect(unhandled).not.toHaveBeenCalled()
|
|
expect(generatePullRequestFieldsFromContextMock).not.toHaveBeenCalled()
|
|
} finally {
|
|
void issue.promise.catch(() => undefined)
|
|
process.off('unhandledRejection', unhandled)
|
|
}
|
|
}
|
|
)
|
|
|
|
it('observes a lookup rejection while preparation is pending and preserves the later error', async () => {
|
|
const issue = Promise.withResolvers<null>()
|
|
const preparation = Promise.withResolvers<typeof draftContext>()
|
|
const failure = new Error('lookup admission failed')
|
|
linkedLookup.run = () => issue.promise
|
|
getPullRequestDraftContextMock.mockReturnValue(preparation.promise)
|
|
const unhandled = vi.fn()
|
|
process.on('unhandledRejection', unhandled)
|
|
const result = Promise.resolve(
|
|
handlers.get('git:generatePullRequestFields')!(null, request)
|
|
).catch((error: unknown) => error)
|
|
try {
|
|
await nextTurn()
|
|
expect(getPullRequestDraftContextMock).toHaveBeenCalledTimes(1)
|
|
issue.reject(failure)
|
|
await nextTurn()
|
|
expect(unhandled).not.toHaveBeenCalled()
|
|
preparation.resolve(draftContext)
|
|
expect(await result).toBe(failure)
|
|
expect(generatePullRequestFieldsFromContextMock).not.toHaveBeenCalled()
|
|
} finally {
|
|
issue.resolve(null)
|
|
preparation.resolve(draftContext)
|
|
await result
|
|
process.off('unhandledRejection', unhandled)
|
|
}
|
|
})
|
|
})
|