Files
orca/src/main/runtime/runtime-git-generation-admission.test.ts
T

253 lines
9.8 KiB
TypeScript

import { setImmediate as nextTurn } from 'node:timers/promises'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import type { GlobalSettings } from '../../shared/global-settings-types'
import type { RuntimeGitCommandHost, RuntimeGitTarget } from './runtime-git-command-target'
const mocks = vi.hoisted(() => ({
getStagedCommitContext: vi.fn(),
gitExecFileAsync: vi.fn(),
getPullRequestDraftContext: vi.fn(),
loadPullRequestLinkedIssue: vi.fn(),
prepareLocalCommitMessageAgentEnv: vi.fn(),
generateCommitMessageFromContext: vi.fn(),
generatePullRequestFieldsFromContext: vi.fn(),
resolveHostedReviewBodyForGeneration: vi.fn()
}))
const linkedLookup = vi.hoisted(() => ({ run: (): Promise<null> => Promise.resolve(null) }))
vi.mock('../git/status', () => ({ getStagedCommitContext: mocks.getStagedCommitContext }))
vi.mock('../git/runner', () => ({ gitExecFileAsync: mocks.gitExecFileAsync }))
vi.mock('../text-generation/pull-request-context', () => ({
getPullRequestDraftContext: mocks.getPullRequestDraftContext
}))
vi.mock('../source-control/pull-request-linked-issue', () => ({
loadPullRequestLinkedIssue: (...args: unknown[]) => {
mocks.loadPullRequestLinkedIssue(...args)
// A mock return observer would handle the rejection being tested.
return linkedLookup.run()
}
}))
vi.mock('../text-generation/commit-message-agent-environment', () => ({
prepareLocalCommitMessageAgentEnv: mocks.prepareLocalCommitMessageAgentEnv
}))
vi.mock('../text-generation/commit-message-text-generation', () => ({
generateCommitMessageFromContext: mocks.generateCommitMessageFromContext,
generatePullRequestFieldsFromContext: mocks.generatePullRequestFieldsFromContext
}))
vi.mock('../source-control/pull-request-template', () => ({
resolveHostedReviewBodyForGeneration: mocks.resolveHostedReviewBodyForGeneration
}))
import { SSH_HOST_NOT_SERVED_MESSAGE } from '../providers/ssh-host-not-served-error'
import { RuntimeGitGenerationCommands } from './runtime-git-generation-commands'
const params = { agentId: 'codex' as const, model: 'gpt-5.5' }
const settingsOverride = { sourceControlAiResolvedParams: params }
const pullRequestContext = {
base: 'main',
branch: 'feature/admission',
commitSummary: 'abc123 feat: test',
changeSummary: 'M README.md',
patch: '+hello',
currentTitle: '',
currentBody: '',
currentDraft: false
}
function makeTarget(path: string, overrides: Partial<RuntimeGitTarget> = {}): RuntimeGitTarget {
return {
worktree: {
id: 'wt-1',
repoId: 'repo-1',
path,
git: { path, branch: 'main', isBare: false, isMainWorktree: false, head: 'a'.repeat(40) }
} as RuntimeGitTarget['worktree'],
executionHostId: 'local',
...overrides
}
}
function makeCommands(target: RuntimeGitTarget): RuntimeGitGenerationCommands {
const host: RuntimeGitCommandHost = {
resolveRuntimeGitTarget: async () => target,
getRuntimeSettings: () => ({}) as GlobalSettings
}
return new RuntimeGitGenerationCommands(host)
}
describe('RuntimeGitGenerationCommands admission', () => {
beforeEach(() => {
vi.clearAllMocks()
mocks.gitExecFileAsync.mockResolvedValue({ stdout: '', stderr: '' })
linkedLookup.run = () => Promise.resolve(null)
mocks.resolveHostedReviewBodyForGeneration.mockImplementation(async ({ body }) => body)
mocks.prepareLocalCommitMessageAgentEnv.mockResolvedValue({ ok: true, env: {} })
mocks.generateCommitMessageFromContext.mockResolvedValue({ success: true, message: 'feat' })
mocks.generatePullRequestFieldsFromContext.mockResolvedValue({ success: true, fields: {} })
})
it('marks local staged commit context as interactive without dropping WSL routing', async () => {
mocks.getStagedCommitContext.mockResolvedValue({
branch: 'main',
stagedSummary: 'M README.md',
stagedPatch: '+hello'
})
const commands = makeCommands(
makeTarget('C:\\repo', { localGitOptions: { wslDistro: 'Ubuntu' } })
)
await commands.generateRuntimeCommitMessage('id:wt-1', settingsOverride)
expect(mocks.getStagedCommitContext).toHaveBeenCalledWith('C:\\repo', {
wslDistro: 'Ubuntu',
admissionTier: 'interactive'
})
})
it('marks local pull-request context and linked-issue reads as interactive', async () => {
mocks.getPullRequestDraftContext.mockImplementation(async (execute) => {
await execute(['fetch', 'origin', 'main'], { timeout: 123 })
await execute(['show-ref', '--verify'], { maxBuffer: 456, timeoutMs: 789 })
return pullRequestContext
})
const commands = makeCommands(
makeTarget('C:\\repo', { localGitOptions: { wslDistro: 'Ubuntu' } })
)
await commands.generateRuntimePullRequestFields(
'id:wt-1',
{ base: 'main', title: '', body: '', draft: false },
settingsOverride
)
expect(mocks.gitExecFileAsync).toHaveBeenCalledWith(['fetch', 'origin', 'main'], {
cwd: 'C:\\repo',
wslDistro: 'Ubuntu',
timeout: 123,
admissionTier: 'interactive'
})
expect(mocks.gitExecFileAsync).toHaveBeenCalledWith(['show-ref', '--verify'], {
cwd: 'C:\\repo',
wslDistro: 'Ubuntu',
maxBuffer: 456,
timeout: 789,
admissionTier: 'interactive'
})
expect(mocks.loadPullRequestLinkedIssue).toHaveBeenCalledWith(
expect.objectContaining({
localGitOptions: { wslDistro: 'Ubuntu', admissionTier: 'interactive' }
})
)
})
it('reports an SSH pull-request target unserved without reading git anywhere', async () => {
const commands = makeCommands(makeTarget('/remote/repo', { executionHostId: 'ssh:conn-1' }))
await expect(
commands.generateRuntimePullRequestFields(
'id:wt-1',
{ base: 'main', title: '', body: '', draft: false },
settingsOverride
)
).resolves.toEqual({ success: false, error: SSH_HOST_NOT_SERVED_MESSAGE })
expect(mocks.getPullRequestDraftContext).not.toHaveBeenCalled()
expect(mocks.gitExecFileAsync).not.toHaveBeenCalled()
expect(mocks.loadPullRequestLinkedIssue).not.toHaveBeenCalled()
})
describe('linked-issue lifetime', () => {
function commands(): RuntimeGitGenerationCommands {
return makeCommands(makeTarget('/repo'))
}
const input = { base: 'main', title: '', body: '', draft: false, provider: 'gitlab' as const }
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('preparation failed')
mocks.getPullRequestDraftContext.mockResolvedValue(pullRequestContext)
if (outcome === 'no changes') {
mocks.getPullRequestDraftContext.mockResolvedValue(null)
} else if (outcome === 'context error') {
mocks.getPullRequestDraftContext.mockRejectedValue(preparationError)
} else {
mocks.resolveHostedReviewBodyForGeneration.mockRejectedValue(preparationError)
}
const unhandled = vi.fn()
process.on('unhandledRejection', unhandled)
try {
await expect(
commands().generateRuntimePullRequestFields('id:wt-1', input, settingsOverride)
).resolves.toEqual({
success: false,
error:
outcome === 'no changes'
? 'No branch changes to summarize.'
: preparationError.message
})
expect(mocks.loadPullRequestLinkedIssue).toHaveBeenCalledOnce()
issue.reject(new Error('Timed out waiting for a GitLab operation slot.'))
await nextTurn()
expect(unhandled).not.toHaveBeenCalled()
expect(mocks.generatePullRequestFieldsFromContext).not.toHaveBeenCalled()
} finally {
issue.resolve(null)
void issue.promise.catch(() => undefined)
process.off('unhandledRejection', unhandled)
}
}
)
it('observes rejection during preparation and preserves the later caller error', async () => {
const issue = Promise.withResolvers<null>()
const preparation = Promise.withResolvers<typeof pullRequestContext>()
const failure = new Error('lookup admission failed')
linkedLookup.run = () => issue.promise
mocks.getPullRequestDraftContext.mockReturnValue(preparation.promise)
const unhandled = vi.fn()
process.on('unhandledRejection', unhandled)
const result = commands()
.generateRuntimePullRequestFields('id:wt-1', input, settingsOverride)
.catch((error: unknown) => error)
try {
await nextTurn()
expect(mocks.getPullRequestDraftContext).toHaveBeenCalledOnce()
issue.reject(failure)
await nextTurn()
expect(unhandled).not.toHaveBeenCalled()
preparation.resolve(pullRequestContext)
expect(await result).toBe(failure)
expect(mocks.generatePullRequestFieldsFromContext).not.toHaveBeenCalled()
} finally {
issue.resolve(null)
preparation.resolve(pullRequestContext)
await result
process.off('unhandledRejection', unhandled)
}
})
it('still rejects the caller after successful preparation when the lookup fails', async () => {
const issue = Promise.withResolvers<null>()
const failure = new Error('lookup failed')
linkedLookup.run = () => issue.promise
mocks.getPullRequestDraftContext.mockResolvedValue(pullRequestContext)
const result = commands()
.generateRuntimePullRequestFields('id:wt-1', input, settingsOverride)
.catch((error: unknown) => error)
try {
await nextTurn()
issue.reject(failure)
expect(await result).toBe(failure)
expect(mocks.generatePullRequestFieldsFromContext).not.toHaveBeenCalled()
} finally {
issue.resolve(null)
await result
}
})
})
})