fix(mobile): parse classified PR lookup outcomes (#12659)
This commit is contained in:
parent
2539889197
commit
86b878cfd6
|
|
@ -353,15 +353,51 @@ describe('fetch wrappers', () => {
|
|||
})
|
||||
|
||||
it('fetchPRForBranch threads linkedPRNumber as authoritative resolver', async () => {
|
||||
const { client, sendRequest } = mockClient(okResponse({ number: 4, state: 'open' }))
|
||||
const { client, sendRequest } = mockClient(
|
||||
okResponse({
|
||||
kind: 'found',
|
||||
pr: { number: 4, state: 'merged' },
|
||||
fetchedAt: 1
|
||||
})
|
||||
)
|
||||
const out = await fetchPRForBranch(client, WORKTREE_ID, { branch: 'feat', linkedPRNumber: 4 })
|
||||
expect(out.ok).toBe(true)
|
||||
expect(out.ok && out.result).toMatchObject({ number: 4, state: 'merged' })
|
||||
const [method, params] = sendRequest.mock.calls[0]!
|
||||
expect(method).toBe('github.prForBranch')
|
||||
expect(params).toMatchObject({ branch: 'feat', linkedPRNumber: 4 })
|
||||
expect('prRepo' in (params as object)).toBe(false)
|
||||
})
|
||||
|
||||
it('fetchPRForBranch preserves legacy flat responses', async () => {
|
||||
const { client } = mockClient(okResponse({ number: 4, state: 'open' }))
|
||||
const out = await fetchPRForBranch(client, WORKTREE_ID, { branch: 'feat' })
|
||||
expect(out.ok && out.result).toMatchObject({ number: 4, state: 'open' })
|
||||
})
|
||||
|
||||
it('fetchPRForBranch maps a classified no-pr response to null', async () => {
|
||||
const { client } = mockClient(okResponse({ kind: 'no-pr', fetchedAt: 1 }))
|
||||
await expect(fetchPRForBranch(client, WORKTREE_ID, { branch: 'feat' })).resolves.toEqual({
|
||||
ok: true,
|
||||
result: null
|
||||
})
|
||||
})
|
||||
|
||||
it('fetchPRForBranch propagates classified upstream errors', async () => {
|
||||
const { client } = mockClient(
|
||||
okResponse({
|
||||
kind: 'upstream-error',
|
||||
errorType: 'network',
|
||||
message: 'network unavailable',
|
||||
fetchedAt: 1
|
||||
})
|
||||
)
|
||||
await expect(fetchPRForBranch(client, WORKTREE_ID, { branch: 'feat' })).resolves.toEqual({
|
||||
ok: false,
|
||||
error: 'network unavailable'
|
||||
})
|
||||
})
|
||||
|
||||
it('fetchPRChecks forwards headSha + prRepo', async () => {
|
||||
const { client, sendRequest } = mockClient(okResponse([]))
|
||||
await fetchPRChecks(client, WORKTREE_ID, {
|
||||
|
|
|
|||
|
|
@ -6,6 +6,10 @@ import type {
|
|||
PRInfo
|
||||
} from '../../../src/shared/types'
|
||||
import type { HostedReviewInfo } from '../../../src/shared/hosted-review'
|
||||
import {
|
||||
normalizeGitHubPRForBranchOutcome,
|
||||
type GitHubPRForBranchResponse
|
||||
} from '../../../src/shared/github-pr-for-branch-outcome'
|
||||
import type { RpcClient } from '../transport/rpc-client'
|
||||
import type { RpcSuccess } from '../transport/types'
|
||||
import { mobileRepoSelectorFromWorktreeId } from '../source-control/mobile-pr-create'
|
||||
|
|
@ -162,7 +166,20 @@ export async function fetchPRForBranch(
|
|||
branch: args.branch,
|
||||
linkedPRNumber: args.linkedPRNumber ?? null
|
||||
}),
|
||||
readPRForBranch
|
||||
(value) => {
|
||||
const outcome = normalizeGitHubPRForBranchOutcome(value as GitHubPRForBranchResponse)
|
||||
if (outcome.kind === 'upstream-error') {
|
||||
throw new Error(outcome.message)
|
||||
}
|
||||
if (outcome.kind === 'no-pr') {
|
||||
return null
|
||||
}
|
||||
const pr = readPRForBranch(outcome.pr)
|
||||
if (!pr) {
|
||||
throw new Error('GitHub returned an invalid pull request response.')
|
||||
}
|
||||
return pr
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -73,6 +73,7 @@ import {
|
|||
getTaskSourceRuntimeSettings,
|
||||
type TaskSourceContext
|
||||
} from '../../../../shared/task-source-context'
|
||||
import { normalizeGitHubPRForBranchOutcome } from '../../../../shared/github-pr-for-branch-outcome'
|
||||
|
||||
// ─── ProjectV2 cache types ────────────────────────────────────────────
|
||||
// Why: separate from CacheEntry<T> — project-view has a single GraphQL source (no issue/PR fallback) and a distinct error union.
|
||||
|
|
@ -2040,18 +2041,6 @@ export type GitHubSlice = {
|
|||
patchProjectRowContent: (cacheKey: string, rowId: string, patch: ProjectRowContentPatch) => void
|
||||
}
|
||||
|
||||
/** Normalizes `github.prForBranch` into a {@link PRRefreshOutcome}: preserves a runtime `upstream-error` instead of collapsing to a false "no PR"; a legacy host returning `PRInfo | null` maps to `found`/`no-pr`. */
|
||||
function normalizeRuntimePRForBranchOutcome(
|
||||
result: PRRefreshOutcome | PRInfo | null
|
||||
): PRRefreshOutcome {
|
||||
if (result && typeof result === 'object' && 'kind' in result) {
|
||||
return result
|
||||
}
|
||||
return result
|
||||
? { kind: 'found', pr: result, fetchedAt: Date.now() }
|
||||
: { kind: 'no-pr', fetchedAt: Date.now() }
|
||||
}
|
||||
|
||||
export const createGitHubSlice: StateCreator<AppState, [], [], GitHubSlice> = (set, get) => ({
|
||||
prCache: {},
|
||||
issueCache: {},
|
||||
|
|
@ -3077,7 +3066,7 @@ export const createGitHubSlice: StateCreator<AppState, [], [], GitHubSlice> = (s
|
|||
: {})
|
||||
},
|
||||
{ timeoutMs: 30_000 }
|
||||
).then((result) => normalizeRuntimePRForBranchOutcome(result))
|
||||
).then((result) => normalizeGitHubPRForBranchOutcome(result))
|
||||
: await (async () => {
|
||||
const candidate: GitHubPRRefreshCandidate = {
|
||||
repoId: repoId ?? '',
|
||||
|
|
@ -3099,24 +3088,18 @@ export const createGitHubSlice: StateCreator<AppState, [], [], GitHubSlice> = (s
|
|||
cachedMergeable: cached?.data?.mergeable ?? null,
|
||||
cachedMergeStateStatus: cached?.data?.mergeStateStatus ?? null
|
||||
}
|
||||
return window.api.gh.refreshPRNow
|
||||
const response = window.api.gh.refreshPRNow
|
||||
? await window.api.gh.refreshPRNow({ candidate })
|
||||
: await window.api.gh
|
||||
.prForBranch({
|
||||
repoPath,
|
||||
repoId,
|
||||
branch,
|
||||
linkedPRNumber,
|
||||
fallbackPRNumber,
|
||||
acceptMergedFallbackPR:
|
||||
fallbackPRNumber !== null && fallbackPRSource !== null,
|
||||
currentHeadOid: requestHeadOid
|
||||
})
|
||||
.then((pr) =>
|
||||
pr
|
||||
? ({ kind: 'found', pr, fetchedAt: Date.now() } as const)
|
||||
: ({ kind: 'no-pr', fetchedAt: Date.now() } as const)
|
||||
)
|
||||
: await window.api.gh.prForBranch({
|
||||
repoPath,
|
||||
repoId,
|
||||
branch,
|
||||
linkedPRNumber,
|
||||
fallbackPRNumber,
|
||||
acceptMergedFallbackPR: fallbackPRNumber !== null && fallbackPRSource !== null,
|
||||
currentHeadOid: requestHeadOid
|
||||
})
|
||||
return normalizeGitHubPRForBranchOutcome(response)
|
||||
})()
|
||||
const pr: PRInfo | null =
|
||||
outcome.kind === 'found' ? outcome.pr : outcome.kind === 'no-pr' ? null : null
|
||||
|
|
|
|||
|
|
@ -0,0 +1,42 @@
|
|||
import { describe, expect, it } from 'vitest'
|
||||
import type { PRInfo, PRRefreshOutcome } from './types'
|
||||
import { normalizeGitHubPRForBranchOutcome } from './github-pr-for-branch-outcome'
|
||||
|
||||
const PR = {
|
||||
number: 42,
|
||||
title: 'Feature',
|
||||
state: 'merged',
|
||||
url: 'https://github.com/acme/orca/pull/42',
|
||||
checksStatus: 'success',
|
||||
updatedAt: '2026-08-04T22:46:08Z',
|
||||
mergeable: 'UNKNOWN'
|
||||
} as PRInfo
|
||||
|
||||
describe('normalizeGitHubPRForBranchOutcome', () => {
|
||||
it('preserves current classified outcomes', () => {
|
||||
const outcome: PRRefreshOutcome = { kind: 'found', pr: PR, fetchedAt: 10 }
|
||||
expect(normalizeGitHubPRForBranchOutcome(outcome, 20)).toBe(outcome)
|
||||
})
|
||||
|
||||
it('normalizes legacy PRInfo and null responses', () => {
|
||||
expect(normalizeGitHubPRForBranchOutcome(PR, 20)).toEqual({
|
||||
kind: 'found',
|
||||
pr: PR,
|
||||
fetchedAt: 20
|
||||
})
|
||||
expect(normalizeGitHubPRForBranchOutcome(null, 20)).toEqual({
|
||||
kind: 'no-pr',
|
||||
fetchedAt: 20
|
||||
})
|
||||
})
|
||||
|
||||
it('preserves classified upstream errors', () => {
|
||||
const outcome: PRRefreshOutcome = {
|
||||
kind: 'upstream-error',
|
||||
errorType: 'network',
|
||||
message: 'network unavailable',
|
||||
fetchedAt: 10
|
||||
}
|
||||
expect(normalizeGitHubPRForBranchOutcome(outcome, 20)).toBe(outcome)
|
||||
})
|
||||
})
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
import type { PRInfo, PRRefreshOutcome } from './types'
|
||||
|
||||
export type GitHubPRForBranchResponse = PRRefreshOutcome | PRInfo | null
|
||||
|
||||
// Legacy hosts return PRInfo|null; current hosts return a classified refresh outcome.
|
||||
export function normalizeGitHubPRForBranchOutcome(
|
||||
response: GitHubPRForBranchResponse,
|
||||
fetchedAt = Date.now()
|
||||
): PRRefreshOutcome {
|
||||
if (response && typeof response === 'object' && 'kind' in response) {
|
||||
return response
|
||||
}
|
||||
return response ? { kind: 'found', pr: response, fetchedAt } : { kind: 'no-pr', fetchedAt }
|
||||
}
|
||||
Loading…
Reference in New Issue