mirror of
https://github.com/stablyai/orca.git
synced 2026-10-27 08:02:51 +00:00
Abort Create PR intent on failed detail generation instead of silently c
- Previously a swallowed generation error would fall through and auto-submit a PR with blank/template fields; now failures, cancels, and success are resolved explicitly and surfaced to the user. - Adds resolveCreatePrIntentGeneratedFields/resolveCreatePrIntentGenerationErrorDetail helpers with test coverage, plus a new localized failure notice string across all locales.
This commit is contained in:
@@ -258,6 +258,8 @@ import {
|
||||
createPrIntentRunTokenMatches,
|
||||
getCreatePrIntentCommitFailureNoticeMessage,
|
||||
getCreatePrIntentStagePaths,
|
||||
resolveCreatePrIntentGeneratedFields,
|
||||
resolveCreatePrIntentGenerationErrorDetail,
|
||||
resolveCreatePrIntentReviewBase,
|
||||
resolveCreatePrIntentRemoteStep,
|
||||
type CreatePrIntentRunToken
|
||||
@@ -3523,13 +3525,26 @@ function SourceControlInner(): React.JSX.Element {
|
||||
)
|
||||
})
|
||||
const target = getCreatePrIntentOperationTarget(token)
|
||||
// Why: the intent auto-submits, so a swallowed failure would ship a
|
||||
// blank-template PR; abort and surface the error instead.
|
||||
const surfaceCreatePrIntentDetailGenerationFailure = (errorDetail: string): void => {
|
||||
setCreatePrIntentNoticeForWorktree(token.worktreeId, {
|
||||
tone: 'destructive',
|
||||
message: translate(
|
||||
'auto.components.right.sidebar.SourceControl.createPrIntentDetailGenerationFailed',
|
||||
'Couldn’t generate {{value0}} details: {{value1}} Retry Create PR.',
|
||||
{ value0: hostedReviewCreateCopy.reviewLabel, value1: errorDetail }
|
||||
)
|
||||
})
|
||||
}
|
||||
try {
|
||||
const generated = await generateRuntimePullRequestFields(target, {
|
||||
...fields,
|
||||
provider: eligibility.provider,
|
||||
useTemplate: resolvedPrCreationDefaults.useTemplate
|
||||
})
|
||||
if (generated.branchChangedByPreparation) {
|
||||
const resolution = resolveCreatePrIntentGeneratedFields({ generated, fallback: fields })
|
||||
if (resolution.outcome === 'branchChanged') {
|
||||
setCreatePrIntentNoticeForWorktree(token.worktreeId, {
|
||||
tone: 'muted',
|
||||
message: translate(
|
||||
@@ -3539,18 +3554,20 @@ function SourceControlInner(): React.JSX.Element {
|
||||
})
|
||||
return false
|
||||
}
|
||||
if (generated.success) {
|
||||
fields = {
|
||||
// Why: Create PR intent auto-submits; generated details should
|
||||
// not retarget the review without user confirmation.
|
||||
base: fields.base,
|
||||
title: generated.fields.title.trim() || fields.title,
|
||||
body: generated.fields.body,
|
||||
draft: generated.fields.draft
|
||||
}
|
||||
if (resolution.outcome === 'applied') {
|
||||
fields = resolution.fields
|
||||
} else if (resolution.outcome === 'canceled') {
|
||||
setCreatePrIntentNoticeForWorktree(token.worktreeId, null)
|
||||
return false
|
||||
} else {
|
||||
surfaceCreatePrIntentDetailGenerationFailure(resolution.errorDetail)
|
||||
return false
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('[SourceControl] Create PR intent detail generation failed', error)
|
||||
surfaceCreatePrIntentDetailGenerationFailure(
|
||||
resolveCreatePrIntentGenerationErrorDetail(error)
|
||||
)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+63
@@ -7,6 +7,8 @@ import {
|
||||
createPrIntentRunTokenMatches,
|
||||
getCreatePrIntentCommitFailureNoticeMessage,
|
||||
getCreatePrIntentStagePaths,
|
||||
resolveCreatePrIntentGeneratedFields,
|
||||
resolveCreatePrIntentGenerationErrorDetail,
|
||||
resolveCreatePrIntentReviewBase,
|
||||
resolveCreatePrIntentRemoteStep
|
||||
} from './source-control-create-pr-intent-flow'
|
||||
@@ -278,4 +280,65 @@ describe('source-control Create PR intent flow helpers', () => {
|
||||
})
|
||||
).toBe('localized Pre-commit hook failed.')
|
||||
})
|
||||
|
||||
it('resolves generated PR details without auto-submitting a blank template on failure', () => {
|
||||
const fallback = { base: 'main', title: 'fallback title', body: 'fallback body', draft: false }
|
||||
|
||||
expect(
|
||||
resolveCreatePrIntentGeneratedFields({
|
||||
generated: { success: true, branchChangedByPreparation: true },
|
||||
fallback
|
||||
})
|
||||
).toEqual({ outcome: 'branchChanged' })
|
||||
|
||||
expect(
|
||||
resolveCreatePrIntentGeneratedFields({
|
||||
generated: {
|
||||
success: true,
|
||||
fields: { title: ' Generated title ', body: 'Generated body', draft: true }
|
||||
},
|
||||
fallback
|
||||
})
|
||||
).toEqual({
|
||||
outcome: 'applied',
|
||||
fields: { base: 'main', title: 'Generated title', body: 'Generated body', draft: true }
|
||||
})
|
||||
|
||||
// Why: a blank generated title must not overwrite the seeded fallback title.
|
||||
expect(
|
||||
resolveCreatePrIntentGeneratedFields({
|
||||
generated: { success: true, fields: { title: ' ', body: 'Generated body', draft: true } },
|
||||
fallback
|
||||
})
|
||||
).toEqual({
|
||||
outcome: 'applied',
|
||||
fields: { base: 'main', title: 'fallback title', body: 'Generated body', draft: true }
|
||||
})
|
||||
|
||||
expect(
|
||||
resolveCreatePrIntentGeneratedFields({
|
||||
generated: { success: false, canceled: true },
|
||||
fallback
|
||||
})
|
||||
).toEqual({ outcome: 'canceled' })
|
||||
|
||||
expect(
|
||||
resolveCreatePrIntentGeneratedFields({
|
||||
generated: { success: false, error: 'Model request timed out' },
|
||||
fallback
|
||||
})
|
||||
).toEqual({ outcome: 'failed', errorDetail: 'Model request timed out' })
|
||||
|
||||
expect(
|
||||
resolveCreatePrIntentGeneratedFields({ generated: { success: false }, fallback })
|
||||
).toEqual({ outcome: 'failed', errorDetail: '' })
|
||||
})
|
||||
|
||||
it('stringifies both Error and non-Error thrown values for the failure notice', () => {
|
||||
expect(resolveCreatePrIntentGenerationErrorDetail(new Error('boom'))).toBe('boom')
|
||||
expect(resolveCreatePrIntentGenerationErrorDetail('raw string throw')).toBe('raw string throw')
|
||||
expect(resolveCreatePrIntentGenerationErrorDetail({ code: 'ECONNRESET' })).toBe(
|
||||
'[object Object]'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -152,6 +152,58 @@ export function resolveCreatePrIntentRemoteStep({
|
||||
return 'none'
|
||||
}
|
||||
|
||||
export type CreatePrIntentGeneratedFields = {
|
||||
base: string
|
||||
title: string
|
||||
body: string
|
||||
draft: boolean
|
||||
}
|
||||
|
||||
export type CreatePrIntentGeneratedFieldsResolution =
|
||||
| { outcome: 'branchChanged' }
|
||||
| { outcome: 'canceled' }
|
||||
| { outcome: 'failed'; errorDetail: string }
|
||||
| { outcome: 'applied'; fields: CreatePrIntentGeneratedFields }
|
||||
|
||||
export function resolveCreatePrIntentGeneratedFields({
|
||||
generated,
|
||||
fallback
|
||||
}: {
|
||||
generated: {
|
||||
success: boolean
|
||||
error?: string
|
||||
canceled?: boolean
|
||||
branchChangedByPreparation?: boolean
|
||||
fields?: { title: string; body: string; draft: boolean }
|
||||
}
|
||||
fallback: CreatePrIntentGeneratedFields
|
||||
}): CreatePrIntentGeneratedFieldsResolution {
|
||||
if (generated.branchChangedByPreparation) {
|
||||
return { outcome: 'branchChanged' }
|
||||
}
|
||||
if (generated.success && generated.fields) {
|
||||
return {
|
||||
outcome: 'applied',
|
||||
fields: {
|
||||
// Why: Create PR intent auto-submits; generated details should
|
||||
// not retarget the review without user confirmation.
|
||||
base: fallback.base,
|
||||
title: generated.fields.title.trim() || fallback.title,
|
||||
body: generated.fields.body,
|
||||
draft: generated.fields.draft
|
||||
}
|
||||
}
|
||||
}
|
||||
if (generated.canceled) {
|
||||
return { outcome: 'canceled' }
|
||||
}
|
||||
return { outcome: 'failed', errorDetail: generated.error ?? '' }
|
||||
}
|
||||
|
||||
export function resolveCreatePrIntentGenerationErrorDetail(error: unknown): string {
|
||||
return error instanceof Error ? error.message : String(error)
|
||||
}
|
||||
|
||||
export function getCreatePrIntentCommitFailureNoticeMessage(
|
||||
commitError: string | null | undefined,
|
||||
copy: {
|
||||
|
||||
@@ -9644,7 +9644,8 @@
|
||||
"a9bf7c171a": "Push Failed",
|
||||
"834cb3f23d": "Fix with AI",
|
||||
"783a808870": "Close"
|
||||
}
|
||||
},
|
||||
"createPrIntentDetailGenerationFailed": "Couldn’t generate {{value0}} details: {{value1}} Retry Create PR."
|
||||
},
|
||||
"SourceControlAgentActionDialog": {
|
||||
"8e856842d1": "Could not start the selected agent.",
|
||||
|
||||
@@ -9644,7 +9644,8 @@
|
||||
"a9bf7c171a": "Push fallido",
|
||||
"834cb3f23d": "Corregir con AI",
|
||||
"783a808870": "Cerrar"
|
||||
}
|
||||
},
|
||||
"createPrIntentDetailGenerationFailed": "Couldn’t generate {{value0}} details: {{value1}} Retry Create PR."
|
||||
},
|
||||
"SourceControlAgentActionDialog": {
|
||||
"8e856842d1": "No se pudo iniciar el agente seleccionado.",
|
||||
|
||||
@@ -9644,7 +9644,8 @@
|
||||
"a9bf7c171a": "プッシュ失敗",
|
||||
"834cb3f23d": "AIで修正",
|
||||
"783a808870": "閉じる"
|
||||
}
|
||||
},
|
||||
"createPrIntentDetailGenerationFailed": "Couldn’t generate {{value0}} details: {{value1}} Retry Create PR."
|
||||
},
|
||||
"SourceControlAgentActionDialog": {
|
||||
"8e856842d1": "選択した agent を開始できませんでした。",
|
||||
|
||||
@@ -9644,7 +9644,8 @@
|
||||
"a9bf7c171a": "푸시 실패",
|
||||
"834cb3f23d": "AI로 수정",
|
||||
"783a808870": "닫기"
|
||||
}
|
||||
},
|
||||
"createPrIntentDetailGenerationFailed": "Couldn’t generate {{value0}} details: {{value1}} Retry Create PR."
|
||||
},
|
||||
"SourceControlAgentActionDialog": {
|
||||
"8e856842d1": "선택한 agent를 시작할 수 없습니다.",
|
||||
|
||||
@@ -9644,7 +9644,8 @@
|
||||
"a9bf7c171a": "推送失败",
|
||||
"834cb3f23d": "使用 AI 修复",
|
||||
"783a808870": "关闭"
|
||||
}
|
||||
},
|
||||
"createPrIntentDetailGenerationFailed": "Couldn’t generate {{value0}} details: {{value1}} Retry Create PR."
|
||||
},
|
||||
"SourceControlAgentActionDialog": {
|
||||
"8e856842d1": "无法启动选定的智能体。",
|
||||
|
||||
Reference in New Issue
Block a user