mirror of
https://github.com/stablyai/orca.git
synced 2026-10-22 16:03:47 +00:00
* Clarify PR panel guidance: classify errors and confirm-only composer Replace the ambiguous GitHub hosted-review boolean with a four-state evidence model (found/positive_unresolved/not_found/unknown) so "No PR found" never appears without an accepted lookup result. Classify GitHub refresh failures into types (rate_limited, auth, network, permission, repo_unavailable, gh_unavailable, unknown) for stable, honest copy. Confirmed-only composer: preserve drafts across transient failures; hide Create during hard errors and positive-unresolved evidence. Hard errors clear only when an eligibility request starts after the error and returns an accepted outcome. Propagate error types and unified retry schedule through the store. Sync mobile parity with shouldOpenChecksPanelCreateComposer gating. Localize all new copy. * Clarify PR panel guidance: classify errors and confirm-only composer Add reviewLookupOutcome to hosted-review eligibility and thread it through the panel so it never claims "No PR found" without accepted evidence. A failed lookup is unavailable, not a settled no-PR. Fail closed on positive unresolved evidence, hard refresh errors, and unavailable lookups. Add structured GitHub refresh-error classification with Retry-After parsing. Implement confirmed-only composer gating based on fresh, matching-context eligibility with hard-error clearing. Mobile gates on reviewLookupOutcome to prevent false Create claims. Surface throwOnFailure variants for each provider so transport failures cross the RPC boundary instead of collapsing to null. (Design success criteria 1–4; invariant 8.) * Add exec-error helpers for subprocess error classification Extracts stderr/stdout parsing and Retry-After detection into a lightweight module that can be imported without pulling in the heavier runner machinery. Supports PR-refresh error classification and proper rate-limit handling for gh commands. * test(mobile): include reviewLookupOutcome in create eligibility fixtures Create / Push & Create now fails closed unless the lookup is not_found. Update mobile test fixtures so accepted-no-PR cases can still proceed. * Add OrThrow mock variants to forge-provider test mocks forge-provider resolves branch reviews via the OrThrow variant so lookup failures surface as unavailable instead of "no PR found".
184 lines
5.8 KiB
TypeScript
184 lines
5.8 KiB
TypeScript
import { describe, expect, it } from 'vitest'
|
|
import {
|
|
resolveSourceControlCommitAreaPrimaryActionDecision,
|
|
resolveSourceControlPrimaryActionDecision,
|
|
type SourceControlPrimaryActionDecisionInputs
|
|
} from './source-control-primary-action-decision'
|
|
|
|
function inputs(
|
|
overrides: Partial<SourceControlPrimaryActionDecisionInputs> = {}
|
|
): SourceControlPrimaryActionDecisionInputs {
|
|
return {
|
|
stagedCount: 0,
|
|
hasUnstagedChanges: false,
|
|
hasStageableChanges: false,
|
|
hasPartiallyStagedChanges: false,
|
|
hasMessage: false,
|
|
hasUnresolvedConflicts: false,
|
|
isCommitting: false,
|
|
isRemoteOperationActive: false,
|
|
upstreamStatus: undefined,
|
|
...overrides
|
|
}
|
|
}
|
|
|
|
describe('source-control primary action decision', () => {
|
|
it('returns Stage All for a dirty tree before remote actions', () => {
|
|
const result = resolveSourceControlCommitAreaPrimaryActionDecision(
|
|
inputs({
|
|
hasUnstagedChanges: true,
|
|
hasStageableChanges: true,
|
|
upstreamStatus: { hasUpstream: true, ahead: 0, behind: 4 }
|
|
})
|
|
)
|
|
expect(result.kind).toBe('stage')
|
|
expect(result.titleIntent).toBe('stage_all_changes')
|
|
expect(result.disabled).toBe(false)
|
|
})
|
|
|
|
it('returns enabled Commit for staged changes with a message', () => {
|
|
const result = resolveSourceControlCommitAreaPrimaryActionDecision(
|
|
inputs({ stagedCount: 2, hasMessage: true })
|
|
)
|
|
expect(result).toMatchObject({
|
|
kind: 'commit',
|
|
titleIntent: 'commit_staged_changes',
|
|
disabled: false
|
|
})
|
|
})
|
|
|
|
it('blocks commits while unresolved conflicts exist', () => {
|
|
const result = resolveSourceControlCommitAreaPrimaryActionDecision(
|
|
inputs({ stagedCount: 1, hasMessage: true, hasUnresolvedConflicts: true })
|
|
)
|
|
expect(result).toMatchObject({
|
|
kind: 'commit',
|
|
titleIntent: 'resolve_conflicts_before_commit',
|
|
disabled: true
|
|
})
|
|
})
|
|
|
|
it('returns remote push, pull, and sync decisions for clean tracked branches', () => {
|
|
expect(
|
|
resolveSourceControlCommitAreaPrimaryActionDecision(
|
|
inputs({ upstreamStatus: { hasUpstream: true, ahead: 2, behind: 0 } })
|
|
)
|
|
).toMatchObject({ kind: 'push', titleIntent: 'push_count', count: 2 })
|
|
expect(
|
|
resolveSourceControlCommitAreaPrimaryActionDecision(
|
|
inputs({ upstreamStatus: { hasUpstream: true, ahead: 0, behind: 3 } })
|
|
)
|
|
).toMatchObject({ kind: 'pull', titleIntent: 'pull_count', count: 3 })
|
|
expect(
|
|
resolveSourceControlCommitAreaPrimaryActionDecision(
|
|
inputs({ upstreamStatus: { hasUpstream: true, ahead: 2, behind: 3 } })
|
|
)
|
|
).toMatchObject({ kind: 'sync', titleIntent: 'sync_counts', ahead: 2, behind: 3 })
|
|
})
|
|
|
|
it('returns Publish Branch for clean unpublished branches with a current branch', () => {
|
|
const result = resolveSourceControlCommitAreaPrimaryActionDecision(
|
|
inputs({
|
|
upstreamStatus: { hasUpstream: false, ahead: 0, behind: 0 },
|
|
hasCurrentBranch: true
|
|
})
|
|
)
|
|
expect(result).toMatchObject({
|
|
kind: 'publish',
|
|
titleIntent: 'publish_branch',
|
|
disabled: false
|
|
})
|
|
})
|
|
|
|
it('blocks unpublished branch publishing when HEAD is detached', () => {
|
|
const result = resolveSourceControlCommitAreaPrimaryActionDecision(
|
|
inputs({
|
|
upstreamStatus: { hasUpstream: false, ahead: 0, behind: 0 },
|
|
hasCurrentBranch: false
|
|
})
|
|
)
|
|
expect(result).toMatchObject({
|
|
kind: 'commit',
|
|
titleIntent: 'checkout_branch_before_publish',
|
|
disabled: true
|
|
})
|
|
})
|
|
|
|
it('mirrors in-flight remote operation semantics', () => {
|
|
const result = resolveSourceControlCommitAreaPrimaryActionDecision(
|
|
inputs({
|
|
isRemoteOperationActive: true,
|
|
inFlightRemoteOpKind: 'pull',
|
|
upstreamStatus: { hasUpstream: true, ahead: 3, behind: 0 }
|
|
})
|
|
)
|
|
expect(result).toMatchObject({
|
|
kind: 'pull',
|
|
labelIntent: 'pull',
|
|
titleIntent: 'action_in_progress',
|
|
disabled: true
|
|
})
|
|
})
|
|
|
|
it('marks patch-equivalent diverged branches as force-push-with-lease decisions', () => {
|
|
const result = resolveSourceControlCommitAreaPrimaryActionDecision(
|
|
inputs({
|
|
branchCommitsAhead: 4,
|
|
upstreamStatus: {
|
|
hasUpstream: true,
|
|
upstreamName: 'origin/feature',
|
|
ahead: 14,
|
|
behind: 3,
|
|
behindCommitsArePatchEquivalent: true
|
|
}
|
|
})
|
|
)
|
|
expect(result).toMatchObject({
|
|
kind: 'push',
|
|
labelIntent: 'force_push',
|
|
titleIntent: 'force_push_with_lease',
|
|
requiresForceWithLease: true,
|
|
count: 4,
|
|
upstreamName: 'origin/feature'
|
|
})
|
|
})
|
|
|
|
it('keeps review creation out of commit-area decisions', () => {
|
|
const input = inputs({
|
|
upstreamStatus: { hasUpstream: true, ahead: 0, behind: 0 },
|
|
hostedReviewCreation: {
|
|
provider: 'gitlab',
|
|
review: null,
|
|
canCreate: true,
|
|
blockedReason: null,
|
|
nextAction: null,
|
|
reviewLookupOutcome: 'not_found'
|
|
}
|
|
})
|
|
expect(resolveSourceControlPrimaryActionDecision(input).kind).toBe('create_pr')
|
|
expect(resolveSourceControlCommitAreaPrimaryActionDecision(input).kind).toBe('commit')
|
|
})
|
|
|
|
it('returns disabled create review while hosted-review creation eligibility is loading', () => {
|
|
const result = resolveSourceControlPrimaryActionDecision(
|
|
inputs({
|
|
upstreamStatus: { hasUpstream: true, ahead: 0, behind: 0 },
|
|
hostedReviewCreation: {
|
|
provider: 'gitlab',
|
|
review: null,
|
|
canCreate: false,
|
|
blockedReason: null,
|
|
nextAction: null,
|
|
reviewLookupOutcome: 'not_found'
|
|
},
|
|
isHostedReviewCreationLoading: true
|
|
})
|
|
)
|
|
expect(result).toMatchObject({
|
|
kind: 'create_pr',
|
|
titleIntent: 'checking_review_creation',
|
|
disabled: true
|
|
})
|
|
})
|
|
})
|