Carry the worktree head on PR-refresh candidates so fallback preservation is head-gated

Live validation caught a self-sustaining loop the unit seams missed: the
Checks panel passes its displayed PR number as the refresh fallback, and
the candidate path carried no worktree head, so main preserved a merged
branch PR head-blind — the merged view survived new local commits instead
of yielding to 'No pull request found'.

Thread currentHeadOid through GitHubPRRefreshCandidate, both renderer
candidate builders, the gh:prForBranch IPC, and the runtime RPC method —
the same shape the hosted-review path already used. Main's explicit-head
gate (equality or confirmed commit membership) now applies to every
renderer-driven lookup; true no-context callers keep the designed
deleted-head preservation.

Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
brennanb2025
2026-07-03 18:17:04 -07:00
co-authored by Orca
parent 84b99b28ae
commit b66e9728c7
10 changed files with 69 additions and 11 deletions
@@ -94,6 +94,28 @@ describe('pr-refresh-coordinator', () => {
vi.useRealTimers()
})
it('forwards the candidate worktree head into the branch lookup options', async () => {
const { reportVisiblePRRefreshCandidates } = await import('./pr-refresh-coordinator')
getPRForBranchOutcomeMock.mockResolvedValueOnce({
kind: 'no-pr',
fetchedAt: Date.now()
})
reportVisiblePRRefreshCandidates([makeCandidate({ currentHeadOid: 'worktree-head-oid' })], 1, 1)
await vi.runOnlyPendingTimersAsync()
// Why: without the head, a panel-supplied fallback number preserves a
// merged PR head-blind after the branch moves on to new work.
expect(getPRForBranchOutcomeMock).toHaveBeenCalledWith(
'/repo',
'feature/test',
null,
null,
null,
expect.objectContaining({ currentHeadOid: 'worktree-head-oid' })
)
})
it('does not show visible background refreshes as queued', async () => {
const { reportVisiblePRRefreshCandidates } = await import('./pr-refresh-coordinator')
getPRForBranchOutcomeMock.mockResolvedValueOnce({
+4 -1
View File
@@ -34,7 +34,7 @@ type PRRefreshOutcomeObserver = (
type PRBranchLookupCandidate = Pick<
GitHubPRRefreshCandidate,
'localGitOptions' | 'linkedPRNumber' | 'fallbackPRNumber' | 'fallbackPRSource'
'localGitOptions' | 'linkedPRNumber' | 'fallbackPRNumber' | 'fallbackPRSource' | 'currentHeadOid'
>
function shouldAcceptMergedFallbackPR(candidate: PRBranchLookupCandidate): boolean {
@@ -55,6 +55,9 @@ function hostedReviewOptionArgs(
if (shouldAcceptMergedFallbackPR(candidate)) {
options.acceptMergedFallbackPR = true
}
if (typeof candidate.currentHeadOid === 'string' && candidate.currentHeadOid.trim().length > 0) {
options.currentHeadOid = candidate.currentHeadOid
}
return Object.keys(options).length > 0 ? [options] : []
}