Cover imported detached PR sidebar status

Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
brennanb2025
2026-06-26 22:29:01 -07:00
co-authored by Orca
parent 09ad23b0a8
commit 27185dad09
4 changed files with 125 additions and 14 deletions
@@ -193,6 +193,57 @@ describe('WorktreeCard merged PR fallback display', () => {
expect(markup).not.toContain('Detached HEAD @ merge-c')
})
it('shows detached PRs from legacy path-scoped cache entries', async () => {
hostedReviewCache = {
'local::repo-1::__detached_head__:merge-commit': {
data: null,
fetchedAt: 200
}
}
prCache = {
'/repo::__detached_head__:merge-commit': {
data: makePRInfo({
headSha: 'pr-head-before-merge'
}),
fetchedAt: 100
}
}
const { default: WorktreeCard } = await import('./WorktreeCard')
const markup = renderWorktreeCardMarkup(
<WorktreeCard
worktree={makeWorktree({ branch: '', linkedPR: null, head: 'merge-commit' })}
repo={makeRepo()}
isActive={false}
/>
)
expect(markup).toContain('Linked PR #6340')
expect(markup).not.toContain('Detached HEAD @ merge-c')
})
it('hides detached HEAD badge when the parent list supplies PR status', async () => {
const { default: WorktreeCard } = await import('./WorktreeCard')
const markup = renderWorktreeCardMarkup(
<WorktreeCard
worktree={makeWorktree({ branch: '', linkedPR: null, head: 'merge-commit' })}
repo={makeRepo()}
isActive={false}
statusPrDisplay={{
provider: 'github',
number: 24,
title: 'Detached PR from parent status',
state: 'merged',
status: 'failure',
url: 'https://github.com/acme/orca/pull/24'
}}
/>
)
expect(markup).not.toContain('Detached HEAD @ merge-c')
})
it('suppresses cached merged PR after a newer hosted-review miss when the worktree head moved', async () => {
prCache = {
'repo-1::feature/local-branch': {
@@ -3,7 +3,11 @@ import React, { useEffect, useCallback, useState } from 'react'
import { useAppStore } from '@/store'
import { getHostedReviewCacheKey } from '@/store/slices/hosted-review'
import { issueCacheKey as getIssueCacheKey } from '@/store/slices/github'
import { getGitHubPRCacheBranch, getGitHubPRCacheKey } from '@/store/slices/github-cache-key'
import {
getGitHubPRCacheBranch,
getGitHubPRCacheKey,
getLegacyGitHubPRCacheKey
} from '@/store/slices/github-cache-key'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import { Tooltip, TooltipTrigger, TooltipContent } from '@/components/ui/tooltip'
@@ -435,6 +439,15 @@ const WorktreeCard = React.memo(function WorktreeCard({
true
)
: ''
const canUseLegacyPRCache = repo !== undefined && !repo.connectionId && !repo.executionHostId
const legacyRepoScopedPRCacheKey =
canUseLegacyPRCache && prCacheBranch
? getLegacyGitHubPRCacheKey(repo.path, repo.id, prCacheBranch)
: ''
const legacyPathScopedPRCacheKey =
canUseLegacyPRCache && prCacheBranch
? getLegacyGitHubPRCacheKey(repo.path, undefined, prCacheBranch)
: ''
const issueCacheKey =
repo && worktree.linkedIssue
? getIssueCacheKey(
@@ -455,7 +468,12 @@ const WorktreeCard = React.memo(function WorktreeCard({
const hostedReviewEntry = useAppStore((s) =>
hostedReviewCacheKey ? s.hostedReviewCache[hostedReviewCacheKey] : undefined
)
const prCacheEntry = useAppStore((s) => (prCacheKey ? s.prCache?.[prCacheKey] : undefined))
const prCacheEntry = useAppStore(
(s) =>
(prCacheKey ? s.prCache?.[prCacheKey] : undefined) ??
(legacyRepoScopedPRCacheKey ? s.prCache?.[legacyRepoScopedPRCacheKey] : undefined) ??
(legacyPathScopedPRCacheKey ? s.prCache?.[legacyPathScopedPRCacheKey] : undefined)
)
const issueEntry = useAppStore((s) => (issueCacheKey ? s.issueCache[issueCacheKey] : undefined))
const linearIssueEntry = useAppStore((s) =>
linearIssueCacheKey ? s.linearIssueCache[linearIssueCacheKey] : undefined
@@ -1171,7 +1189,7 @@ const WorktreeCard = React.memo(function WorktreeCard({
// Why: a detached merge commit can still have PR context; once shown, the PR
// status is the higher-signal sidebar identity than the raw commit badge.
const showDetachedHeadInMetaRow =
!compactCards && !isFolder && detachedHeadDisplay !== null && hoverReview === null
!compactCards && !isFolder && detachedHeadDisplay !== null && statusLaneReview === null
const showBranch =
!isFolder &&
branch.length > 0 &&
@@ -145,6 +145,37 @@ describe('getFolderWorkspaceCardPrDisplay', () => {
expect(display).toMatchObject({ number: 4, status: 'success' })
})
it('uses detached HEAD-scoped PR cache for attached worktrees', () => {
const detached = makeWorktree({
id: 'detached',
branch: '',
head: 'merge-commit',
linkedPR: null
})
const display = getFolderWorkspaceCardPrDisplay({
folderWorkspaceId: 'folder-1',
workspaceLineageByChildKey: { [detached.id]: makeWorkspaceLineage(detached) },
worktreeLineageById: {},
worktreeMap: new Map([[detached.id, detached]]),
repoMap: new Map([[repo.id, repo]]),
hostedReviewCache: {
'local::repo-1::__detached_head__:merge-commit': { data: null }
},
prCache: {
'repo-1::__detached_head__:merge-commit': {
data: makePr(24, 'failure')
}
}
})
expect(display).toMatchObject({
provider: 'github',
number: 24,
status: 'failure'
})
})
})
function makePr(number: number, checksStatus: CheckStatus) {
@@ -1,6 +1,10 @@
import type { AppState } from '@/store/types'
import { getHostedReviewCacheKey } from '@/store/slices/hosted-review'
import { getGitHubPRCacheKey, getLegacyGitHubPRCacheKey } from '@/store/slices/github-cache-key'
import {
getGitHubPRCacheBranch,
getGitHubPRCacheKey,
getLegacyGitHubPRCacheKey
} from '@/store/slices/github-cache-key'
import { branchName } from '@/lib/git-utils'
import type { HostedReviewInfo } from '../../../../shared/hosted-review'
import type {
@@ -162,13 +166,14 @@ function getAttachedWorktreePrDisplay({
}
const branch = branchName(worktree.branch).trim()
if (!branch) {
const cacheBranch = getGitHubPRCacheBranch(branch, worktree.head)
if (!cacheBranch) {
return getLinkedReviewDisplay(worktree)
}
const hostedReviewCacheKey = getHostedReviewCacheKey(
repo.path,
branch,
cacheBranch,
settings,
repo.id,
repo.connectionId,
@@ -193,7 +198,7 @@ function getAttachedWorktreePrDisplay({
return hostedReviewDisplay
}
const cachedGitHubPr = getCachedGitHubPr({ worktree, repo, branch, prCache, settings })
const cachedGitHubPr = getCachedGitHubPr({ worktree, repo, cacheBranch, prCache, settings })
if (cachedGitHubPr) {
return {
provider: 'github',
@@ -222,24 +227,24 @@ function getLinkedReviewDisplay(worktree: Worktree): WorktreeCardPrDisplay | nul
function getCachedGitHubPr({
worktree,
repo,
branch,
cacheBranch,
prCache,
settings
}: {
worktree: Worktree
repo: Repo
branch: string
cacheBranch: string
prCache: Record<string, unknown> | null
settings?: AppState['settings']
}): PRInfo | null {
if (!prCache || worktree.linkedPR === null) {
if (!prCache || (worktree.linkedPR === null && !cacheBranch.startsWith('__detached_head__:'))) {
return null
}
const cacheKey = getGitHubPRCacheKey(
repo.path,
repo.id,
branch,
cacheBranch,
settings,
repo.connectionId,
repo.executionHostId,
@@ -247,10 +252,10 @@ function getCachedGitHubPr({
)
const canUseLegacyPRCache = !repo.connectionId && !repo.executionHostId
const legacyRepoScopedCacheKey = canUseLegacyPRCache
? getLegacyGitHubPRCacheKey(repo.path, repo.id, branch)
? getLegacyGitHubPRCacheKey(repo.path, repo.id, cacheBranch)
: ''
const legacyPathScopedCacheKey = canUseLegacyPRCache
? getLegacyGitHubPRCacheKey(repo.path, undefined, branch)
? getLegacyGitHubPRCacheKey(repo.path, undefined, cacheBranch)
: ''
const entry =
(prCache[cacheKey] as PrCacheEntry | undefined) ??
@@ -261,7 +266,13 @@ function getCachedGitHubPr({
? (prCache[legacyPathScopedCacheKey] as PrCacheEntry | undefined)
: undefined)
const pr = entry?.data ?? null
return pr?.number === worktree.linkedPR ? pr : null
if (!pr) {
return null
}
if (cacheBranch.startsWith('__detached_head__:')) {
return pr
}
return pr.number === worktree.linkedPR ? pr : null
}
function compareReviewDisplays(left: WorktreeCardPrDisplay, right: WorktreeCardPrDisplay): number {