Show accurate PR status for local worktrees (#6460)
This commit is contained in:
parent
b899aef1f9
commit
d03e99c89a
|
|
@ -58,7 +58,8 @@ export function resolveHostedReviewForCheckRunDetailsFix(
|
|||
branch,
|
||||
settings,
|
||||
repo.connectionId,
|
||||
repo.executionHostId
|
||||
repo.executionHostId,
|
||||
true
|
||||
)
|
||||
const hostedReviewCacheKey = getHostedReviewCacheKey(
|
||||
repo.path,
|
||||
|
|
@ -66,7 +67,8 @@ export function resolveHostedReviewForCheckRunDetailsFix(
|
|||
settings,
|
||||
repo.id,
|
||||
repo.connectionId,
|
||||
repo.executionHostId
|
||||
repo.executionHostId,
|
||||
true
|
||||
)
|
||||
const pr = prCacheKey ? (store.prCache[prCacheKey]?.data ?? null) : null
|
||||
const hostedReview = hostedReviewCacheKey
|
||||
|
|
|
|||
|
|
@ -607,7 +607,8 @@ export default function ChecksPanel(): React.JSX.Element {
|
|||
branch,
|
||||
settings,
|
||||
repo.connectionId,
|
||||
repo.executionHostId
|
||||
repo.executionHostId,
|
||||
true
|
||||
)
|
||||
: ''
|
||||
const hostedReviewCacheKey =
|
||||
|
|
@ -618,7 +619,8 @@ export default function ChecksPanel(): React.JSX.Element {
|
|||
settings,
|
||||
repo.id,
|
||||
repo.connectionId,
|
||||
repo.executionHostId
|
||||
repo.executionHostId,
|
||||
true
|
||||
)
|
||||
: ''
|
||||
const refreshContextKey = `${activeWorktreeId ?? ''}::${prCacheKey}::${branch}`
|
||||
|
|
@ -726,7 +728,8 @@ export default function ChecksPanel(): React.JSX.Element {
|
|||
prChecksCacheSuffix(prNumber, pr?.prRepo),
|
||||
settings,
|
||||
repo.connectionId,
|
||||
repo.executionHostId
|
||||
repo.executionHostId,
|
||||
true
|
||||
)
|
||||
: ''
|
||||
const commentsCacheKey =
|
||||
|
|
@ -737,7 +740,8 @@ export default function ChecksPanel(): React.JSX.Element {
|
|||
prCommentsCacheSuffix(prNumber, pr?.prRepo),
|
||||
settings,
|
||||
repo.connectionId,
|
||||
repo.executionHostId
|
||||
repo.executionHostId,
|
||||
true
|
||||
)
|
||||
: ''
|
||||
const checksFetchedAt = useAppStore((s) =>
|
||||
|
|
|
|||
|
|
@ -1324,7 +1324,8 @@ function SourceControlInner(): React.JSX.Element {
|
|||
settings,
|
||||
activeRepo.id,
|
||||
activeRepo.connectionId,
|
||||
activeRepo.executionHostId
|
||||
activeRepo.executionHostId,
|
||||
true
|
||||
)
|
||||
: null
|
||||
const hostedReviewEntry = hostedReviewCacheKey
|
||||
|
|
@ -1338,7 +1339,8 @@ function SourceControlInner(): React.JSX.Element {
|
|||
branchName,
|
||||
settings,
|
||||
activeRepo.connectionId,
|
||||
activeRepo.executionHostId
|
||||
activeRepo.executionHostId,
|
||||
true
|
||||
)
|
||||
: null
|
||||
const activePrFromQueue = activePrCacheKey ? (prCache[activePrCacheKey]?.data ?? null) : null
|
||||
|
|
|
|||
|
|
@ -40,7 +40,8 @@ export function getActiveChecksStatus(state: ActiveChecksStatusState): CheckStat
|
|||
branch,
|
||||
state.settings,
|
||||
activeRepo.connectionId,
|
||||
activeRepo.executionHostId
|
||||
activeRepo.executionHostId,
|
||||
true
|
||||
)
|
||||
const hostedReviewCacheKey = getHostedReviewCacheKey(
|
||||
activeRepo.path,
|
||||
|
|
@ -48,7 +49,8 @@ export function getActiveChecksStatus(state: ActiveChecksStatusState): CheckStat
|
|||
state.settings,
|
||||
activeRepo.id,
|
||||
activeRepo.connectionId,
|
||||
activeRepo.executionHostId
|
||||
activeRepo.executionHostId,
|
||||
true
|
||||
)
|
||||
const hostedReview = state.hostedReviewCache?.[hostedReviewCacheKey]?.data ?? null
|
||||
if (hostedReview && hostedReview.provider !== 'github') {
|
||||
|
|
|
|||
|
|
@ -223,7 +223,8 @@ function getGitHubChecksEntry(
|
|||
prChecksCacheSuffix(review.number, prRepo, review.headSha),
|
||||
args.settings,
|
||||
args.repo.connectionId,
|
||||
args.repo.executionHostId
|
||||
args.repo.executionHostId,
|
||||
true
|
||||
)
|
||||
const withoutHead = getGitHubRepoCacheKey(
|
||||
args.repo.path,
|
||||
|
|
@ -231,7 +232,8 @@ function getGitHubChecksEntry(
|
|||
prChecksCacheSuffix(review.number, prRepo),
|
||||
args.settings,
|
||||
args.repo.connectionId,
|
||||
args.repo.executionHostId
|
||||
args.repo.executionHostId,
|
||||
true
|
||||
)
|
||||
return args.checksCache[withHead] ?? args.checksCache[withoutHead]
|
||||
}
|
||||
|
|
@ -246,7 +248,8 @@ function getHostedReviewKey(
|
|||
args.settings,
|
||||
args.repo.id,
|
||||
args.repo.connectionId,
|
||||
args.repo.executionHostId
|
||||
args.repo.executionHostId,
|
||||
true
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -257,7 +260,8 @@ function getPRKey(args: BuildParentPrChecksRowsArgs & { repo: Repo }, branch: st
|
|||
branch,
|
||||
args.settings,
|
||||
args.repo.connectionId,
|
||||
args.repo.executionHostId
|
||||
args.repo.executionHostId,
|
||||
true
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ const cacheTimerMocks = vi.hoisted(() => ({
|
|||
|
||||
let worktreeCardProperties: WorktreeCardProperty[] = ['status', 'ports']
|
||||
let hostedReviewCache: Record<string, unknown> = {}
|
||||
let issueCache: Record<string, unknown> = {}
|
||||
let projectGroups: unknown[] = []
|
||||
let workspacePortScan: { key: string; result: WorkspacePortScanResult } | null = null
|
||||
let settings: Partial<GlobalSettings> | null = { compactWorktreeCards: true }
|
||||
|
|
@ -39,7 +40,7 @@ vi.mock('@/store', () => ({
|
|||
fetchLinearIssue,
|
||||
gitConflictOperationByWorktree: {},
|
||||
hostedReviewCache,
|
||||
issueCache: {},
|
||||
issueCache,
|
||||
linearIssueCache: {},
|
||||
openModal,
|
||||
openTaskPage,
|
||||
|
|
@ -185,6 +186,7 @@ describe('WorktreeCard compact hover details', () => {
|
|||
vi.clearAllMocks()
|
||||
worktreeCardProperties = ['status', 'ports']
|
||||
hostedReviewCache = {}
|
||||
issueCache = {}
|
||||
projectGroups = []
|
||||
workspacePortScan = null
|
||||
settings = { compactWorktreeCards: true }
|
||||
|
|
@ -296,6 +298,38 @@ describe('WorktreeCard compact hover details', () => {
|
|||
expect(markup).not.toContain('data-worktree-card-meta-row=""')
|
||||
}, 30_000)
|
||||
|
||||
it('reads linked issue details from the local repo-owner cache while a runtime is focused', async () => {
|
||||
settings = {
|
||||
activeRuntimeEnvironmentId: 'env-1',
|
||||
compactWorktreeCards: true,
|
||||
experimentalNewWorktreeCardStyle: true
|
||||
}
|
||||
worktreeCardProperties = ['status']
|
||||
issueCache = {
|
||||
'repo-1::123': {
|
||||
data: { number: 123, title: 'Local owner issue', state: 'open', url: null },
|
||||
fetchedAt: Date.now()
|
||||
},
|
||||
'runtime:env-1::repo-1::123': {
|
||||
data: { number: 123, title: 'Runtime fallback issue', state: 'open', url: null },
|
||||
fetchedAt: Date.now()
|
||||
}
|
||||
}
|
||||
const { default: WorktreeCard } = await import('./WorktreeCard')
|
||||
|
||||
const markup = renderToStaticMarkup(
|
||||
<WorktreeCard
|
||||
worktree={makeWorktree({ linkedIssue: 123 })}
|
||||
repo={makeRepo()}
|
||||
isActive={false}
|
||||
/>
|
||||
)
|
||||
|
||||
expect(markup).toContain('Local owner issue')
|
||||
expect(markup).not.toContain('Runtime fallback issue')
|
||||
expect(markup).not.toContain('Loading issue')
|
||||
}, 30_000)
|
||||
|
||||
it('shows selected task and note metadata on the compact card title row', async () => {
|
||||
settings = { compactWorktreeCards: true, experimentalNewWorktreeCardStyle: true }
|
||||
worktreeCardProperties = ['status', 'issue', 'linear-issue', 'comment']
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ const updateWorktreeMeta = vi.fn()
|
|||
|
||||
let worktreeCardProperties: WorktreeCardProperty[] = ['status']
|
||||
let hostedReviewCache: Record<string, unknown> = {}
|
||||
let issueCache: Record<string, unknown> = {}
|
||||
let prCache: Record<string, unknown> = {}
|
||||
let workspacePortScan: WorkspacePortScanResult | null = null
|
||||
let settings: Partial<GlobalSettings> | null = null
|
||||
|
|
@ -33,7 +34,7 @@ vi.mock('@/store', () => ({
|
|||
fetchLinearIssue,
|
||||
gitConflictOperationByWorktree: {},
|
||||
hostedReviewCache,
|
||||
issueCache: {},
|
||||
issueCache,
|
||||
linearIssueCache: {},
|
||||
openModal,
|
||||
prCache,
|
||||
|
|
@ -161,6 +162,7 @@ describe('WorktreeCard linked PR display', () => {
|
|||
vi.clearAllMocks()
|
||||
worktreeCardProperties = ['status']
|
||||
hostedReviewCache = {}
|
||||
issueCache = {}
|
||||
prCache = {}
|
||||
workspacePortScan = null
|
||||
settings = null
|
||||
|
|
@ -669,6 +671,42 @@ describe('WorktreeCard linked PR display', () => {
|
|||
expect(markup).not.toContain('lucide-git-branch')
|
||||
})
|
||||
|
||||
it('reads the local branch PR cache for a known local repo while a runtime is focused', async () => {
|
||||
settings = {
|
||||
activeRuntimeEnvironmentId: 'env-win',
|
||||
experimentalNewWorktreeCardStyle: true
|
||||
}
|
||||
worktreeCardProperties = ['status']
|
||||
prCache = {
|
||||
'repo-1::feature/local-branch': {
|
||||
data: makePRInfo({
|
||||
number: 6341,
|
||||
title: 'Keep local PR status visible',
|
||||
state: 'open',
|
||||
checksStatus: 'pending'
|
||||
}),
|
||||
fetchedAt: Date.now()
|
||||
},
|
||||
'runtime:env-win::repo-1::feature/local-branch': {
|
||||
data: null,
|
||||
fetchedAt: Date.now()
|
||||
}
|
||||
}
|
||||
const { default: WorktreeCard } = await import('./WorktreeCard')
|
||||
|
||||
const markup = renderWorktreeCardMarkup(
|
||||
<WorktreeCard
|
||||
worktree={makeWorktree({ linkedPR: null })}
|
||||
repo={makeRepo()}
|
||||
isActive={false}
|
||||
/>
|
||||
)
|
||||
|
||||
expect(markup).toContain('PR checks: Pending')
|
||||
expect(markup).not.toContain('Branch')
|
||||
expect(markup).not.toContain('lucide-git-branch')
|
||||
})
|
||||
|
||||
it('keeps the detailed right-side PR badge during a transient hosted-review miss', async () => {
|
||||
settings = { compactWorktreeCards: false, experimentalNewWorktreeCardStyle: false }
|
||||
worktreeCardProperties = ['pr']
|
||||
|
|
|
|||
|
|
@ -412,7 +412,8 @@ const WorktreeCard = React.memo(function WorktreeCard({
|
|||
settings,
|
||||
repo.id,
|
||||
repo.connectionId,
|
||||
repo.executionHostId
|
||||
repo.executionHostId,
|
||||
true
|
||||
)
|
||||
: ''
|
||||
const prCacheKey =
|
||||
|
|
@ -423,7 +424,8 @@ const WorktreeCard = React.memo(function WorktreeCard({
|
|||
branch,
|
||||
settings,
|
||||
repo.connectionId,
|
||||
repo.executionHostId
|
||||
repo.executionHostId,
|
||||
true
|
||||
)
|
||||
: ''
|
||||
const issueCacheKey =
|
||||
|
|
@ -434,7 +436,8 @@ const WorktreeCard = React.memo(function WorktreeCard({
|
|||
worktree.linkedIssue,
|
||||
settings,
|
||||
repo.connectionId,
|
||||
repo.executionHostId
|
||||
repo.executionHostId,
|
||||
true
|
||||
)
|
||||
: ''
|
||||
// Why: use 'all' to fetch from all Linear workspaces. The issue might belong
|
||||
|
|
|
|||
|
|
@ -172,7 +172,8 @@ function getAttachedWorktreePrDisplay({
|
|||
settings,
|
||||
repo.id,
|
||||
repo.connectionId,
|
||||
repo.executionHostId
|
||||
repo.executionHostId,
|
||||
true
|
||||
)
|
||||
const hostedReviewEntry = hostedReviewCache?.[hostedReviewCacheKey] as
|
||||
| HostedReviewCacheEntry
|
||||
|
|
@ -241,10 +242,10 @@ function getCachedGitHubPr({
|
|||
branch,
|
||||
settings,
|
||||
repo.connectionId,
|
||||
repo.executionHostId
|
||||
repo.executionHostId,
|
||||
true
|
||||
)
|
||||
const canUseLegacyPRCache =
|
||||
!settings?.activeRuntimeEnvironmentId?.trim() && !repo.connectionId && !repo.executionHostId
|
||||
const canUseLegacyPRCache = !repo.connectionId && !repo.executionHostId
|
||||
const legacyRepoScopedCacheKey = canUseLegacyPRCache
|
||||
? getLegacyGitHubPRCacheKey(repo.path, repo.id, branch)
|
||||
: ''
|
||||
|
|
|
|||
|
|
@ -46,7 +46,8 @@ export function useWorktreeIssueLink(args: { worktreeId: string; issueInput: str
|
|||
issueNumber,
|
||||
s.settings,
|
||||
issueRepo.connectionId,
|
||||
issueRepo.executionHostId
|
||||
issueRepo.executionHostId,
|
||||
true
|
||||
)
|
||||
]?.data?.url ?? null
|
||||
)
|
||||
|
|
|
|||
|
|
@ -165,7 +165,7 @@ describe('getPRGroupKey', () => {
|
|||
expect(getPRGroupKey(worktree, repoMap, prCache)).toBe('closed')
|
||||
})
|
||||
|
||||
it('does not fall back to local PR cache while runtime scoped data is loading', () => {
|
||||
it('uses local PR cache for a known local repo while a runtime is focused', () => {
|
||||
const prCache = {
|
||||
'repo-1::feature/super-critical': {
|
||||
data: { state: 'merged' }
|
||||
|
|
@ -176,7 +176,7 @@ describe('getPRGroupKey', () => {
|
|||
getPRGroupKey(worktree, repoMap, prCache, {
|
||||
activeRuntimeEnvironmentId: 'env-1'
|
||||
} as never)
|
||||
).toBe('in-progress')
|
||||
).toBe('done')
|
||||
})
|
||||
|
||||
it('uses SSH-scoped PR cache entries instead of local entries for SSH repos', () => {
|
||||
|
|
|
|||
|
|
@ -361,11 +361,11 @@ export function getPRGroupKey(
|
|||
branch,
|
||||
settings,
|
||||
repo.connectionId,
|
||||
repo.executionHostId
|
||||
repo.executionHostId,
|
||||
true
|
||||
)
|
||||
: ''
|
||||
const canUseLegacyPRCache =
|
||||
repo !== undefined && !settings?.activeRuntimeEnvironmentId?.trim() && !repo.connectionId
|
||||
const canUseLegacyPRCache = repo !== undefined && !repo.connectionId && !repo.executionHostId
|
||||
const legacyRepoScopedCacheKey =
|
||||
canUseLegacyPRCache && branch ? getLegacyGitHubPRCacheKey(repo.path, repo.id, branch) : ''
|
||||
const legacyPathScopedCacheKey =
|
||||
|
|
|
|||
|
|
@ -183,7 +183,7 @@ function getBranchStatus(
|
|||
return parts.join(', ')
|
||||
}
|
||||
|
||||
function getWorkspaceDecisionDetails(
|
||||
export function getWorkspaceDecisionDetails(
|
||||
worktree: WorkspaceSpaceWorktree,
|
||||
inputs: WorkspaceDecisionInputs
|
||||
): WorkspaceDecisionDetails {
|
||||
|
|
@ -197,11 +197,15 @@ function getWorkspaceDecisionDetails(
|
|||
const branch = workspaceRecord
|
||||
? branchDisplayName(workspaceRecord.branch)
|
||||
: getWorkspaceSpaceBranchLabel(worktree)
|
||||
const repo = inputs.repoMap.get(worktree.repoId)
|
||||
const reviewCacheKey = getHostedReviewCacheKey(
|
||||
worktree.repoPath,
|
||||
branch,
|
||||
inputs.settings,
|
||||
worktree.repoId
|
||||
worktree.repoId,
|
||||
repo?.connectionId,
|
||||
repo?.executionHostId,
|
||||
repo !== undefined
|
||||
)
|
||||
const hostedReview = inputs.hostedReviewCache[reviewCacheKey]?.data
|
||||
const linkedPR = workspaceRecord?.linkedPR ?? null
|
||||
|
|
@ -214,7 +218,6 @@ function getWorkspaceDecisionDetails(
|
|||
? `PR #${linkedPR}`
|
||||
: null
|
||||
const linkedIssue = workspaceRecord?.linkedIssue ?? null
|
||||
const repo = inputs.repoMap.get(worktree.repoId)
|
||||
const issue =
|
||||
linkedIssue && repo
|
||||
? inputs.issueCache[
|
||||
|
|
@ -224,7 +227,8 @@ function getWorkspaceDecisionDetails(
|
|||
linkedIssue,
|
||||
inputs.settings,
|
||||
repo.connectionId,
|
||||
repo.executionHostId
|
||||
repo.executionHostId,
|
||||
true
|
||||
)
|
||||
]?.data
|
||||
: null
|
||||
|
|
|
|||
|
|
@ -16,7 +16,9 @@ import {
|
|||
resolveWorkspaceSpaceTreemapZoomWorktreeId,
|
||||
sortWorkspaceSpaceRows
|
||||
} from './workspace-space-presentation'
|
||||
import { getWorkspaceDecisionDetails } from './WorkspaceSpaceManagerPanel'
|
||||
import type { AgentStatusEntry } from '../../../../shared/agent-status-types'
|
||||
import type { Repo, Worktree } from '../../../../shared/types'
|
||||
|
||||
function row(overrides: Partial<WorkspaceSpaceWorktree>): WorkspaceSpaceWorktree {
|
||||
return {
|
||||
|
|
@ -74,6 +76,69 @@ function activeAgent(overrides: Partial<AgentStatusEntry> = {}): AgentStatusEntr
|
|||
}
|
||||
}
|
||||
|
||||
function repo(overrides: Partial<Repo> = {}): Repo {
|
||||
return {
|
||||
id: 'repo',
|
||||
path: '/repo',
|
||||
displayName: 'Repo',
|
||||
badgeColor: '#999999',
|
||||
addedAt: 1,
|
||||
...overrides
|
||||
}
|
||||
}
|
||||
|
||||
function worktreeRecord(overrides: Partial<Worktree> = {}): Worktree {
|
||||
return {
|
||||
id: 'wt',
|
||||
repoId: 'repo',
|
||||
path: '/workspace',
|
||||
displayName: 'workspace',
|
||||
branch: 'refs/heads/feature/local',
|
||||
head: 'abc123',
|
||||
isBare: false,
|
||||
isMainWorktree: false,
|
||||
comment: '',
|
||||
linkedIssue: null,
|
||||
linkedPR: null,
|
||||
linkedLinearIssue: null,
|
||||
isArchived: false,
|
||||
isUnread: false,
|
||||
isPinned: false,
|
||||
sortOrder: 0,
|
||||
lastActivityAt: 1,
|
||||
...overrides
|
||||
}
|
||||
}
|
||||
|
||||
function decisionInputs(
|
||||
overrides: Partial<Parameters<typeof getWorkspaceDecisionDetails>[1]> = {}
|
||||
): Parameters<typeof getWorkspaceDecisionDetails>[1] {
|
||||
const defaultRepo = repo()
|
||||
const defaultWorktree = worktreeRecord()
|
||||
return {
|
||||
repoMap: new Map([[defaultRepo.id, defaultRepo]]),
|
||||
worktreeMap: new Map([[defaultWorktree.id, defaultWorktree]]),
|
||||
tabsByWorktree: {},
|
||||
ptyIdsByTabId: {},
|
||||
agentStatusByPaneKey: {},
|
||||
migrationUnsupportedByPtyId: {},
|
||||
runtimePaneTitlesByTabId: {},
|
||||
retainedAgentsByPaneKey: {},
|
||||
openFiles: [],
|
||||
editorDrafts: {},
|
||||
browserTabsByWorktree: {},
|
||||
gitStatusByWorktree: {},
|
||||
remoteStatusesByWorktree: {},
|
||||
hostedReviewCache: {},
|
||||
issueCache: {},
|
||||
linearIssueCache: {},
|
||||
settings: null,
|
||||
activeWorktreeId: null,
|
||||
now: 1_000,
|
||||
...overrides
|
||||
}
|
||||
}
|
||||
|
||||
describe('workspace space presentation helpers', () => {
|
||||
it('sorts rows by the selected key and direction', () => {
|
||||
const rows = [
|
||||
|
|
@ -238,6 +303,37 @@ describe('workspace space presentation helpers', () => {
|
|||
).toBe(0)
|
||||
})
|
||||
|
||||
it('reads review and issue details from local owner cache while a runtime is focused', () => {
|
||||
const details = getWorkspaceDecisionDetails(
|
||||
row({ branch: 'refs/heads/feature/local' }),
|
||||
decisionInputs({
|
||||
settings: { activeRuntimeEnvironmentId: 'env-1' },
|
||||
hostedReviewCache: {
|
||||
'local::repo::feature/local': {
|
||||
data: { number: 12, state: 'open', status: 'success', title: 'Local owner PR' }
|
||||
},
|
||||
'runtime:env-1::repo::feature/local': {
|
||||
data: { number: 99, state: 'open', status: 'failure', title: 'Runtime fallback PR' }
|
||||
}
|
||||
},
|
||||
issueCache: {
|
||||
'repo::123': {
|
||||
data: { number: 123, title: 'Local owner issue', state: 'open' }
|
||||
},
|
||||
'runtime:env-1::repo::123': {
|
||||
data: { number: 123, title: 'Runtime fallback issue', state: 'closed' }
|
||||
}
|
||||
},
|
||||
worktreeMap: new Map([
|
||||
['wt', worktreeRecord({ branch: 'refs/heads/feature/local', linkedIssue: 123 })]
|
||||
])
|
||||
})
|
||||
)
|
||||
|
||||
expect(details.reviewLabel).toBe('PR #12 Open, success')
|
||||
expect(details.issueLabel).toBe('#123 open: Local owner issue')
|
||||
})
|
||||
|
||||
it('counts migration-unsupported agent entries by worktree id', () => {
|
||||
const count = countWorkspaceSpaceActiveAgents({
|
||||
worktreeId: 'wt',
|
||||
|
|
|
|||
|
|
@ -13,10 +13,11 @@ export function getGitHubRepoCacheKey(
|
|||
suffix: string,
|
||||
settings?: RuntimeFocusSettings,
|
||||
connectionId?: string | null,
|
||||
executionHostId?: string | null
|
||||
executionHostId?: string | null,
|
||||
hasRepoOwner = false
|
||||
): string {
|
||||
const owner = repoId ?? repoPath
|
||||
const scope = getGitHubCacheHostScope(settings, connectionId, executionHostId)
|
||||
const scope = getGitHubCacheHostScope(settings, connectionId, executionHostId, hasRepoOwner)
|
||||
// Why: runtime/SSH lookups can observe different remotes than the local repo
|
||||
// path, so cache keys include the repo's owning execution boundary.
|
||||
if (scope) {
|
||||
|
|
@ -28,18 +29,27 @@ export function getGitHubRepoCacheKey(
|
|||
function getGitHubCacheHostScope(
|
||||
settings?: RuntimeFocusSettings,
|
||||
connectionId?: string | null,
|
||||
executionHostId?: string | null
|
||||
executionHostId?: string | null,
|
||||
hasRepoOwner = false
|
||||
): string | null {
|
||||
const hostId = normalizeExecutionHostId(executionHostId)
|
||||
if (hostId) {
|
||||
return hostId === LOCAL_EXECUTION_HOST_ID ? null : hostId
|
||||
}
|
||||
const sshConnectionId = connectionId?.trim()
|
||||
if (sshConnectionId) {
|
||||
return toSshExecutionHostId(sshConnectionId)
|
||||
}
|
||||
// Why: an existing repo with no remote/runtime owner is local; only missing
|
||||
// owner context should inherit the focused runtime fallback.
|
||||
if (hasRepoOwner) {
|
||||
return null
|
||||
}
|
||||
const runtimeEnvironmentId = settings?.activeRuntimeEnvironmentId?.trim()
|
||||
if (runtimeEnvironmentId) {
|
||||
return `runtime:${encodeURIComponent(runtimeEnvironmentId)}`
|
||||
}
|
||||
const sshConnectionId = connectionId?.trim()
|
||||
return sshConnectionId ? toSshExecutionHostId(sshConnectionId) : null
|
||||
return null
|
||||
}
|
||||
|
||||
export function getLegacyGitHubRepoCacheKey(
|
||||
|
|
@ -56,9 +66,18 @@ export function getGitHubPRCacheKey(
|
|||
branch: string,
|
||||
settings?: RuntimeFocusSettings,
|
||||
connectionId?: string | null,
|
||||
executionHostId?: string | null
|
||||
executionHostId?: string | null,
|
||||
hasRepoOwner = false
|
||||
): string {
|
||||
return getGitHubRepoCacheKey(repoPath, repoId, branch, settings, connectionId, executionHostId)
|
||||
return getGitHubRepoCacheKey(
|
||||
repoPath,
|
||||
repoId,
|
||||
branch,
|
||||
settings,
|
||||
connectionId,
|
||||
executionHostId,
|
||||
hasRepoOwner
|
||||
)
|
||||
}
|
||||
|
||||
export function getLegacyGitHubPRCacheKey(
|
||||
|
|
|
|||
|
|
@ -44,4 +44,36 @@ describe('syncPRChecksStatus', () => {
|
|||
])
|
||||
expect(result?.prCache?.['repo-id::main']?.data?.checksStatus).toBe('success')
|
||||
})
|
||||
|
||||
it('updates the local repo key while a runtime is focused when repo owner is known', () => {
|
||||
const state = {
|
||||
prCache: {
|
||||
'repo-id::main': {
|
||||
fetchedAt: 0,
|
||||
data: { checksStatus: 'neutral' as const }
|
||||
},
|
||||
'runtime:env-win::repo-id::main': {
|
||||
fetchedAt: 0,
|
||||
data: { checksStatus: 'neutral' as const }
|
||||
}
|
||||
}
|
||||
} as unknown as AppState
|
||||
|
||||
const result = syncPRChecksStatus(
|
||||
state,
|
||||
'/repo',
|
||||
'repo-id',
|
||||
'main',
|
||||
[{ name: 'build', status: 'completed', conclusion: 'success', url: null }],
|
||||
undefined,
|
||||
undefined,
|
||||
{ activeRuntimeEnvironmentId: 'env-win' } as AppState['settings'],
|
||||
null,
|
||||
null,
|
||||
true
|
||||
)
|
||||
|
||||
expect(result?.prCache?.['repo-id::main']?.data?.checksStatus).toBe('success')
|
||||
expect(result?.prCache?.['runtime:env-win::repo-id::main']?.data?.checksStatus).toBe('neutral')
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -44,7 +44,8 @@ export function syncPRChecksStatus(
|
|||
prRepo?: GitHubOwnerRepo | null,
|
||||
settings?: AppState['settings'],
|
||||
connectionId?: string | null,
|
||||
executionHostId?: string | null
|
||||
executionHostId?: string | null,
|
||||
hasRepoOwner = false
|
||||
): Partial<AppState> | null {
|
||||
const normalized = branch ? normalizeBranchName(branch) : ''
|
||||
if (!normalized) {
|
||||
|
|
@ -57,7 +58,8 @@ export function syncPRChecksStatus(
|
|||
normalized,
|
||||
settings,
|
||||
connectionId,
|
||||
executionHostId
|
||||
executionHostId,
|
||||
hasRepoOwner
|
||||
)
|
||||
const prEntry = state.prCache[prCacheKey]
|
||||
if (!prEntry?.data) {
|
||||
|
|
|
|||
|
|
@ -76,6 +76,17 @@ describe('enqueueGitHubPRRefresh host guard', () => {
|
|||
expect(enqueuePRRefresh).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('enqueues the local handler for a known local repo while a runtime is focused', () => {
|
||||
const store = createTestStore()
|
||||
seed(store, { id: 'local-1', path: '/Users/me/code/local-1', name: 'local-1', kind: 'git' })
|
||||
store.setState({ settings: { activeRuntimeEnvironmentId: 'env-win' } as never })
|
||||
|
||||
store.getState().enqueueGitHubPRRefresh('wt-1', 'active', 80)
|
||||
|
||||
expect(enqueuePRRefresh).toHaveBeenCalledTimes(1)
|
||||
expect(mockApi.runtimeEnvironments.call).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('enqueues the local handler for a repo with an explicit local executionHostId', () => {
|
||||
const store = createTestStore()
|
||||
seed(store, {
|
||||
|
|
|
|||
|
|
@ -630,7 +630,15 @@ describe('createGitHubSlice.fetchPRChecks', () => {
|
|||
|
||||
store.setState({
|
||||
settings: { activeRuntimeEnvironmentId: 'env-1' } as AppState['settings'],
|
||||
repos: [{ id: repoId, path: repoPath, name: 'repo', kind: 'git' }],
|
||||
repos: [
|
||||
{
|
||||
id: repoId,
|
||||
path: repoPath,
|
||||
name: 'repo',
|
||||
kind: 'git',
|
||||
executionHostId: 'runtime:env-1'
|
||||
}
|
||||
],
|
||||
prCache: {
|
||||
[runtimePrCacheKey]: {
|
||||
data: makePR({ checksStatus: 'pending' }),
|
||||
|
|
@ -656,6 +664,51 @@ describe('createGitHubSlice.fetchPRChecks', () => {
|
|||
expect(store.getState().prCache[runtimePrCacheKey]?.data?.checksStatus).toBe('success')
|
||||
})
|
||||
|
||||
it('keeps known local repo checks on local cache keys when a runtime is focused', async () => {
|
||||
const store = createTestStore()
|
||||
const repoPath = '/repo'
|
||||
const repoId = 'repo-id'
|
||||
const branch = 'feature/local-checks'
|
||||
const localPrCacheKey = `${repoId}::${branch}`
|
||||
const localChecksCacheKey = `${repoId}::pr-checks::12`
|
||||
const runtimeChecksCacheKey = `runtime:env-1::${repoId}::pr-checks::12`
|
||||
|
||||
store.setState({
|
||||
settings: { activeRuntimeEnvironmentId: 'env-1' } as AppState['settings'],
|
||||
repos: [{ id: repoId, path: repoPath, name: 'repo', kind: 'git' }],
|
||||
prCache: {
|
||||
[localPrCacheKey]: {
|
||||
data: makePR({ checksStatus: 'pending' }),
|
||||
fetchedAt: 1
|
||||
}
|
||||
}
|
||||
} as unknown as Partial<AppState>)
|
||||
|
||||
mockApi.gh.prChecks.mockResolvedValueOnce([
|
||||
{ name: 'build', status: 'completed', conclusion: 'success', url: null }
|
||||
])
|
||||
|
||||
await store
|
||||
.getState()
|
||||
.fetchPRChecks(repoPath, 12, branch, undefined, null, { force: true, repoId })
|
||||
|
||||
expect(runtimeEnvironmentCall).not.toHaveBeenCalled()
|
||||
expect(mockApi.gh.prChecks).toHaveBeenCalledWith({
|
||||
repoPath,
|
||||
repoId,
|
||||
prNumber: 12,
|
||||
headSha: undefined,
|
||||
prRepo: null,
|
||||
noCache: true,
|
||||
sourceContext: undefined
|
||||
})
|
||||
expect(store.getState().checksCache[localChecksCacheKey]?.data).toEqual([
|
||||
{ name: 'build', status: 'completed', conclusion: 'success', url: null }
|
||||
])
|
||||
expect(store.getState().checksCache[runtimeChecksCacheKey]).toBeUndefined()
|
||||
expect(store.getState().prCache[localPrCacheKey]?.data?.checksStatus).toBe('success')
|
||||
})
|
||||
|
||||
it('marks the PR cache entry as failure when any check fails', async () => {
|
||||
const store = createTestStore()
|
||||
const repoPath = '/repo'
|
||||
|
|
@ -1064,7 +1117,15 @@ describe('createGitHubSlice.fetchPRComments', () => {
|
|||
|
||||
store.setState({
|
||||
settings: { activeRuntimeEnvironmentId: 'env-1' } as AppState['settings'],
|
||||
repos: [{ id: repoId, path: repoPath, name: 'repo', kind: 'git' }]
|
||||
repos: [
|
||||
{
|
||||
id: repoId,
|
||||
path: repoPath,
|
||||
name: 'repo',
|
||||
kind: 'git',
|
||||
executionHostId: 'runtime:env-1'
|
||||
}
|
||||
]
|
||||
} as unknown as Partial<AppState>)
|
||||
|
||||
await store.getState().fetchPRComments(repoPath, 12, {
|
||||
|
|
@ -1093,6 +1154,42 @@ describe('createGitHubSlice.fetchPRComments', () => {
|
|||
).toBeUndefined()
|
||||
})
|
||||
|
||||
it('keeps known local repo comments on local cache keys when a runtime is focused', async () => {
|
||||
const store = createTestStore()
|
||||
const repoPath = '/repo'
|
||||
const repoId = 'repo-id'
|
||||
|
||||
store.setState({
|
||||
settings: { activeRuntimeEnvironmentId: 'env-1' } as AppState['settings'],
|
||||
repos: [{ id: repoId, path: repoPath, name: 'repo', kind: 'git' }]
|
||||
} as unknown as Partial<AppState>)
|
||||
mockApi.gh.prComments.mockResolvedValueOnce([
|
||||
{ id: 1, author: 'local', authorAvatarUrl: '', body: '', createdAt: '', url: '' }
|
||||
])
|
||||
|
||||
await store.getState().fetchPRComments(repoPath, 12, {
|
||||
force: true,
|
||||
repoId,
|
||||
prRepo: { owner: 'Acme', repo: 'Widgets' }
|
||||
})
|
||||
|
||||
expect(runtimeEnvironmentCall).not.toHaveBeenCalled()
|
||||
expect(mockApi.gh.prComments).toHaveBeenCalledWith({
|
||||
repoPath,
|
||||
repoId,
|
||||
prNumber: 12,
|
||||
prRepo: { owner: 'Acme', repo: 'Widgets' },
|
||||
noCache: true,
|
||||
sourceContext: undefined
|
||||
})
|
||||
expect(
|
||||
store.getState().commentsCache[`${repoId}::pr-comments::acme/widgets::12`]?.data?.[0].author
|
||||
).toBe('local')
|
||||
expect(
|
||||
store.getState().commentsCache[`runtime:env-1::${repoId}::pr-comments::acme/widgets::12`]
|
||||
).toBeUndefined()
|
||||
})
|
||||
|
||||
it('routes explicit source-context PR comments through the source runtime', async () => {
|
||||
runtimeEnvironmentCall.mockResolvedValueOnce({
|
||||
id: 'rpc-source-comments',
|
||||
|
|
@ -1249,7 +1346,15 @@ describe('createGitHubSlice.fetchPRCheckDetails', () => {
|
|||
|
||||
store.setState({
|
||||
settings: { activeRuntimeEnvironmentId: 'env-1' } as AppState['settings'],
|
||||
repos: [{ id: repoId, path: repoPath, name: 'repo', kind: 'git' }]
|
||||
repos: [
|
||||
{
|
||||
id: repoId,
|
||||
path: repoPath,
|
||||
name: 'repo',
|
||||
kind: 'git',
|
||||
executionHostId: 'runtime:env-1'
|
||||
}
|
||||
]
|
||||
} as unknown as Partial<AppState>)
|
||||
|
||||
await store.getState().fetchPRCheckDetails(
|
||||
|
|
@ -1277,6 +1382,39 @@ describe('createGitHubSlice.fetchPRCheckDetails', () => {
|
|||
})
|
||||
expect(mockApi.gh.prCheckDetails).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('loads known local repo check details through local IPC when a runtime is focused', async () => {
|
||||
const store = createTestStore()
|
||||
const repoPath = '/repo'
|
||||
const repoId = 'repo-id'
|
||||
|
||||
store.setState({
|
||||
settings: { activeRuntimeEnvironmentId: 'env-1' } as AppState['settings'],
|
||||
repos: [{ id: repoId, path: repoPath, name: 'repo', kind: 'git' }]
|
||||
} as unknown as Partial<AppState>)
|
||||
|
||||
await store.getState().fetchPRCheckDetails(
|
||||
repoPath,
|
||||
{
|
||||
checkRunId: 123,
|
||||
checkName: 'build',
|
||||
prRepo: { owner: 'Acme', repo: 'Widgets' }
|
||||
},
|
||||
{ repoId }
|
||||
)
|
||||
|
||||
expect(runtimeEnvironmentCall).not.toHaveBeenCalled()
|
||||
expect(mockApi.gh.prCheckDetails).toHaveBeenCalledWith({
|
||||
repoPath,
|
||||
repoId,
|
||||
checkRunId: 123,
|
||||
workflowRunId: undefined,
|
||||
checkName: 'build',
|
||||
url: undefined,
|
||||
prRepo: { owner: 'Acme', repo: 'Widgets' },
|
||||
sourceContext: undefined
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('createGitHubSlice PR comment mutations', () => {
|
||||
|
|
@ -1428,7 +1566,15 @@ describe('createGitHubSlice PR comment mutations', () => {
|
|||
const repoId = 'repo-id'
|
||||
store.setState({
|
||||
settings: { activeRuntimeEnvironmentId: 'env-1' } as AppState['settings'],
|
||||
repos: [{ id: repoId, path: repoPath, name: 'repo', kind: 'git' }]
|
||||
repos: [
|
||||
{
|
||||
id: repoId,
|
||||
path: repoPath,
|
||||
name: 'repo',
|
||||
kind: 'git',
|
||||
executionHostId: 'runtime:env-1'
|
||||
}
|
||||
]
|
||||
} as unknown as Partial<AppState>)
|
||||
|
||||
await store.getState().addPRReviewCommentReply(repoPath, 12, 99, 'reply', {
|
||||
|
|
@ -2937,8 +3083,28 @@ describe('createGitHubSlice.fetchPRForBranch', () => {
|
|||
const settings = { activeRuntimeEnvironmentId: 'env-1' } as AppState['settings']
|
||||
const runtimeHostedReviewCacheKey = getHostedReviewCacheKey(repoPath, branch, settings, repoId)
|
||||
const localHostedReviewCacheKey = getHostedReviewCacheKey(repoPath, branch, null, repoId)
|
||||
const localChecksCacheKey = `${repoId}::${prChecksCacheSuffix(12, null, 'head-oid')}`
|
||||
const runtimeChecksCacheKey = `runtime:env-1::${repoId}::${prChecksCacheSuffix(
|
||||
12,
|
||||
null,
|
||||
'head-oid'
|
||||
)}`
|
||||
|
||||
store.setState({ settings } as Partial<AppState>)
|
||||
store.setState({
|
||||
settings,
|
||||
checksCache: {
|
||||
[localChecksCacheKey]: {
|
||||
data: [{ name: 'test', status: 'completed', conclusion: 'failure', url: null }],
|
||||
fetchedAt: 1,
|
||||
headSha: 'head-oid'
|
||||
},
|
||||
[runtimeChecksCacheKey]: {
|
||||
data: [{ name: 'test', status: 'completed', conclusion: 'success', url: null }],
|
||||
fetchedAt: 1,
|
||||
headSha: 'head-oid'
|
||||
}
|
||||
}
|
||||
} as Partial<AppState>)
|
||||
|
||||
store.getState().applyGitHubPRRefreshEvent({
|
||||
sequence: 1,
|
||||
|
|
@ -2946,14 +3112,15 @@ describe('createGitHubSlice.fetchPRForBranch', () => {
|
|||
reason: 'visible',
|
||||
outcome: {
|
||||
kind: 'found',
|
||||
pr: makePR({ number: 12, title: 'Local PR status' }),
|
||||
pr: makePR({ number: 12, title: 'Local PR status', checksStatus: 'pending' }),
|
||||
fetchedAt: 2
|
||||
}
|
||||
})
|
||||
|
||||
expect(store.getState().prCache[cacheKey]?.data).toMatchObject({
|
||||
number: 12,
|
||||
title: 'Local PR status'
|
||||
title: 'Local PR status',
|
||||
checksStatus: 'failure'
|
||||
})
|
||||
expect(store.getState().prRefreshSequences[cacheKey]).toBe(1)
|
||||
expect(store.getState().hostedReviewCache[localHostedReviewCacheKey]?.data).toMatchObject({
|
||||
|
|
@ -3653,12 +3820,23 @@ describe('createGitHubSlice.refreshGitHubForWorktreeIfStale', () => {
|
|||
{
|
||||
activeRuntimeEnvironmentId: 'env-1'
|
||||
} as AppState['settings'],
|
||||
'repo-1'
|
||||
'repo-1',
|
||||
null,
|
||||
'runtime:env-1',
|
||||
true
|
||||
)
|
||||
|
||||
store.setState({
|
||||
settings: { activeRuntimeEnvironmentId: 'env-1' } as AppState['settings'],
|
||||
repos: [{ id: 'repo-1', path: repoPath, name: 'repo', kind: 'git' }],
|
||||
repos: [
|
||||
{
|
||||
id: 'repo-1',
|
||||
path: repoPath,
|
||||
name: 'repo',
|
||||
kind: 'git',
|
||||
executionHostId: 'runtime:env-1'
|
||||
}
|
||||
],
|
||||
groupBy: 'pr-status',
|
||||
worktreeCardProperties: ['status'],
|
||||
worktreesByRepo: {
|
||||
|
|
@ -3934,7 +4112,15 @@ describe('createGitHubSlice.refreshAllGitHub', () => {
|
|||
|
||||
store.setState({
|
||||
settings: { activeRuntimeEnvironmentId: 'env-1' } as AppState['settings'],
|
||||
repos: [{ id: 'repo-1', path: repoPath, name: 'repo', kind: 'git' }],
|
||||
repos: [
|
||||
{
|
||||
id: 'repo-1',
|
||||
path: repoPath,
|
||||
name: 'repo',
|
||||
kind: 'git',
|
||||
executionHostId: 'runtime:env-1'
|
||||
}
|
||||
],
|
||||
groupBy: 'repo',
|
||||
worktreeCardProperties: ['comment'],
|
||||
activeWorktreeId: 'wt-1',
|
||||
|
|
@ -4062,7 +4248,15 @@ describe('createGitHubSlice.refreshGitHubForWorktree', () => {
|
|||
|
||||
store.setState({
|
||||
settings: { activeRuntimeEnvironmentId: 'env-1' } as AppState['settings'],
|
||||
repos: [{ id: 'repo-1', path: repoPath, name: 'repo', kind: 'git' }],
|
||||
repos: [
|
||||
{
|
||||
id: 'repo-1',
|
||||
path: repoPath,
|
||||
name: 'repo',
|
||||
kind: 'git',
|
||||
executionHostId: 'runtime:env-1'
|
||||
}
|
||||
],
|
||||
worktreesByRepo: {
|
||||
'repo-1': [
|
||||
{
|
||||
|
|
|
|||
|
|
@ -214,7 +214,7 @@ function settingsForGitHubRepoOwner(
|
|||
settings: AppState['settings'],
|
||||
repo: Pick<Repo, 'connectionId' | 'executionHostId'> | undefined
|
||||
): AppState['settings'] {
|
||||
if (!repo?.executionHostId && !repo?.connectionId) {
|
||||
if (!repo) {
|
||||
return settings
|
||||
}
|
||||
const parsed = parseExecutionHostId(getRepoExecutionHostId(repo))
|
||||
|
|
@ -230,6 +230,16 @@ function settingsForGitHubRepoOwner(
|
|||
: ({ activeRuntimeEnvironmentId: null } as AppState['settings'])
|
||||
}
|
||||
|
||||
function settingsForGitHubFocusedRepoOwner(
|
||||
settings: AppState['settings'],
|
||||
repo: Pick<Repo, 'connectionId' | 'executionHostId'> | undefined
|
||||
): AppState['settings'] {
|
||||
if (!repo?.executionHostId && !repo?.connectionId) {
|
||||
return settings
|
||||
}
|
||||
return settingsForGitHubRepoOwner(settings, repo)
|
||||
}
|
||||
|
||||
function getRefreshAliasExecutionHostId(alias: GitHubPRRefreshAlias): string {
|
||||
const explicitHostId = normalizeExecutionHostId(alias.executionHostId)
|
||||
if (explicitHostId) {
|
||||
|
|
@ -249,7 +259,7 @@ function findRepoForGitHubOwner(
|
|||
)
|
||||
}
|
||||
|
||||
function getGitHubRepoOwnerHostId(
|
||||
function getGitHubFocusedRepoOwnerHostId(
|
||||
settings: AppState['settings'],
|
||||
repo: Pick<Repo, 'connectionId' | 'executionHostId'> | undefined
|
||||
): string {
|
||||
|
|
@ -271,7 +281,7 @@ function getWorkItemsCacheKeyForOwner(
|
|||
repoId,
|
||||
limit,
|
||||
query,
|
||||
repo ? getGitHubRepoOwnerHostId(state.settings ?? null, repo) : undefined
|
||||
repo ? getGitHubFocusedRepoOwnerHostId(state.settings ?? null, repo) : undefined
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -284,7 +294,7 @@ function getGitHubWorkItemSourceHostId(
|
|||
return sourceContext.hostId
|
||||
}
|
||||
return repo
|
||||
? (normalizeExecutionHostId(getGitHubRepoOwnerHostId(state.settings, repo)) ?? undefined)
|
||||
? (normalizeExecutionHostId(getGitHubFocusedRepoOwnerHostId(state.settings, repo)) ?? undefined)
|
||||
: undefined
|
||||
}
|
||||
|
||||
|
|
@ -303,6 +313,20 @@ function getGitHubWorkItemSourceSettings(
|
|||
settings: AppState['settings'],
|
||||
repo: Pick<Repo, 'connectionId' | 'executionHostId'> | undefined,
|
||||
sourceContext?: TaskSourceContext | null
|
||||
): AppState['settings'] {
|
||||
if (sourceContext?.provider === 'github') {
|
||||
return {
|
||||
...settings,
|
||||
...getTaskSourceRuntimeSettings(sourceContext)
|
||||
} as AppState['settings']
|
||||
}
|
||||
return settingsForGitHubFocusedRepoOwner(settings, repo)
|
||||
}
|
||||
|
||||
function getGitHubRepoSourceSettings(
|
||||
settings: AppState['settings'],
|
||||
repo: Pick<Repo, 'connectionId' | 'executionHostId'> | undefined,
|
||||
sourceContext?: TaskSourceContext | null
|
||||
): AppState['settings'] {
|
||||
if (sourceContext?.provider === 'github') {
|
||||
return {
|
||||
|
|
@ -761,7 +785,8 @@ export function issueCacheKey(
|
|||
issueNumber: number | string,
|
||||
settings?: Pick<GlobalSettings, 'activeRuntimeEnvironmentId'> | null,
|
||||
connectionId?: string | null,
|
||||
executionHostId?: string | null
|
||||
executionHostId?: string | null,
|
||||
hasRepoOwner = false
|
||||
): string {
|
||||
return getGitHubRepoCacheKey(
|
||||
repoPath,
|
||||
|
|
@ -769,7 +794,8 @@ export function issueCacheKey(
|
|||
String(issueNumber),
|
||||
settings,
|
||||
connectionId,
|
||||
executionHostId
|
||||
executionHostId,
|
||||
hasRepoOwner
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -779,9 +805,18 @@ function runtimeScopedRepoCacheKey(
|
|||
suffix: string,
|
||||
settings?: AppState['settings'],
|
||||
connectionId?: string | null,
|
||||
executionHostId?: string | null
|
||||
executionHostId?: string | null,
|
||||
hasRepoOwner = false
|
||||
): string {
|
||||
return getGitHubRepoCacheKey(repoPath, repoId, suffix, settings, connectionId, executionHostId)
|
||||
return getGitHubRepoCacheKey(
|
||||
repoPath,
|
||||
repoId,
|
||||
suffix,
|
||||
settings,
|
||||
connectionId,
|
||||
executionHostId,
|
||||
hasRepoOwner
|
||||
)
|
||||
}
|
||||
|
||||
function sourceScopedRepoCacheKey(
|
||||
|
|
@ -791,7 +826,8 @@ function sourceScopedRepoCacheKey(
|
|||
settings?: AppState['settings'],
|
||||
connectionId?: string | null,
|
||||
executionHostId?: string | null,
|
||||
sourceContext?: TaskSourceContext | null
|
||||
sourceContext?: TaskSourceContext | null,
|
||||
hasRepoOwner = false
|
||||
): string {
|
||||
if (sourceContext?.provider === 'github') {
|
||||
return `${getTaskSourceCacheScope(sourceContext)}::${repoId ?? repoPath}::${suffix}`
|
||||
|
|
@ -802,7 +838,8 @@ function sourceScopedRepoCacheKey(
|
|||
suffix,
|
||||
settings,
|
||||
connectionId,
|
||||
executionHostId
|
||||
executionHostId,
|
||||
hasRepoOwner
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -812,9 +849,18 @@ function prCacheKey(
|
|||
branch: string,
|
||||
settings?: AppState['settings'],
|
||||
connectionId?: string | null,
|
||||
executionHostId?: string | null
|
||||
executionHostId?: string | null,
|
||||
hasRepoOwner = false
|
||||
): string {
|
||||
return getGitHubPRCacheKey(repoPath, repoId, branch, settings, connectionId, executionHostId)
|
||||
return getGitHubPRCacheKey(
|
||||
repoPath,
|
||||
repoId,
|
||||
branch,
|
||||
settings,
|
||||
connectionId,
|
||||
executionHostId,
|
||||
hasRepoOwner
|
||||
)
|
||||
}
|
||||
|
||||
function repoCacheKeyPrefixes(repoId: string, repoPath?: string): string[] {
|
||||
|
|
@ -978,7 +1024,8 @@ function buildPRRefreshCandidate(
|
|||
branch,
|
||||
settingsForGitHubRepoOwner(state.settings, repo),
|
||||
repo.connectionId,
|
||||
repo.executionHostId
|
||||
repo.executionHostId,
|
||||
true
|
||||
)
|
||||
const cachedPR = state.prCache[cacheKey]?.data ?? null
|
||||
const hostedReviewFallbackPRNumber = githubHostedReviewFallbackPRNumber(
|
||||
|
|
@ -987,7 +1034,8 @@ function buildPRRefreshCandidate(
|
|||
repo.id,
|
||||
branch,
|
||||
repo.connectionId,
|
||||
repo.executionHostId
|
||||
repo.executionHostId,
|
||||
true
|
||||
)
|
||||
const cachedFallbackPRNumber = cachedPR?.number ?? null
|
||||
const fallbackPRNumber =
|
||||
|
|
@ -1037,7 +1085,8 @@ function githubHostedReviewFallbackPRNumber(
|
|||
repoId: string | undefined,
|
||||
branch: string,
|
||||
connectionId?: string | null,
|
||||
executionHostId?: string | null
|
||||
executionHostId?: string | null,
|
||||
hasRepoOwner = false
|
||||
): number | null {
|
||||
const hostedReviewCacheKey = getHostedReviewCacheKey(
|
||||
repoPath,
|
||||
|
|
@ -1045,7 +1094,8 @@ function githubHostedReviewFallbackPRNumber(
|
|||
state.settings,
|
||||
repoId,
|
||||
connectionId,
|
||||
executionHostId
|
||||
executionHostId,
|
||||
hasRepoOwner
|
||||
)
|
||||
const hostedReview = state.hostedReviewCache[hostedReviewCacheKey]?.data
|
||||
return hostedReview?.provider === 'github' ? hostedReview.number : null
|
||||
|
|
@ -1109,6 +1159,7 @@ function syncHostedReviewCacheFromGitHubPRResult(args: {
|
|||
repoId?: string
|
||||
connectionId?: string | null
|
||||
executionHostId?: string | null
|
||||
hasRepoOwner?: boolean
|
||||
pr: PRInfo | null
|
||||
fetchedAt: number
|
||||
linkedPRNumber?: number | null
|
||||
|
|
@ -1123,7 +1174,8 @@ function syncHostedReviewCacheFromGitHubPRResult(args: {
|
|||
args.settings,
|
||||
args.repoId,
|
||||
args.connectionId,
|
||||
args.executionHostId
|
||||
args.executionHostId,
|
||||
args.hasRepoOwner === true
|
||||
)
|
||||
if (
|
||||
args.requestStartedAt !== undefined &&
|
||||
|
|
@ -1294,6 +1346,7 @@ function setGitHubPRResultCaches(
|
|||
repoId?: string
|
||||
connectionId?: string | null
|
||||
executionHostId?: string | null
|
||||
hasRepoOwner?: boolean
|
||||
pr: PRInfo | null
|
||||
fetchedAt: number
|
||||
worktreeId?: string
|
||||
|
|
@ -1312,6 +1365,7 @@ function setGitHubPRResultCaches(
|
|||
repoId: args.repoId,
|
||||
connectionId: args.connectionId,
|
||||
executionHostId: args.executionHostId,
|
||||
hasRepoOwner: args.hasRepoOwner,
|
||||
pr: args.pr,
|
||||
fetchedAt: args.fetchedAt,
|
||||
linkedPRNumber: args.linkedPRNumber,
|
||||
|
|
@ -1326,7 +1380,8 @@ function setGitHubPRResultCaches(
|
|||
args.settings,
|
||||
args.repoId,
|
||||
args.connectionId,
|
||||
args.executionHostId
|
||||
args.executionHostId,
|
||||
args.hasRepoOwner === true
|
||||
)
|
||||
const nextPRCache = applyPRCacheResult(
|
||||
state.prCache,
|
||||
|
|
@ -1368,6 +1423,7 @@ function applyGitHubPRResultToCaches(args: {
|
|||
repoId?: string
|
||||
connectionId?: string | null
|
||||
executionHostId?: string | null
|
||||
hasRepoOwner?: boolean
|
||||
pr: PRInfo | null
|
||||
fetchedAt: number
|
||||
state: AppState
|
||||
|
|
@ -1389,6 +1445,7 @@ function applyGitHubPRResultToCaches(args: {
|
|||
repoId: args.repoId,
|
||||
connectionId: args.connectionId,
|
||||
executionHostId: args.executionHostId,
|
||||
hasRepoOwner: args.hasRepoOwner,
|
||||
pr: args.pr,
|
||||
fetchedAt: args.fetchedAt,
|
||||
linkedPRNumber: args.linkedPRNumber,
|
||||
|
|
@ -1403,7 +1460,8 @@ function applyGitHubPRResultToCaches(args: {
|
|||
args.settings,
|
||||
args.repoId,
|
||||
args.connectionId,
|
||||
args.executionHostId
|
||||
args.executionHostId,
|
||||
args.hasRepoOwner === true
|
||||
)
|
||||
return {
|
||||
prCache: applyPRCacheResult(
|
||||
|
|
@ -2813,7 +2871,8 @@ export const createGitHubSlice: StateCreator<AppState, [], [], GitHubSlice> = (s
|
|||
branch,
|
||||
requestSettings,
|
||||
repo?.connectionId,
|
||||
repo?.executionHostId
|
||||
repo?.executionHostId,
|
||||
repo !== undefined
|
||||
)
|
||||
const cached = get().prCache[cacheKey]
|
||||
const hostedReviewCacheKey = getHostedReviewCacheKey(
|
||||
|
|
@ -2822,7 +2881,8 @@ export const createGitHubSlice: StateCreator<AppState, [], [], GitHubSlice> = (s
|
|||
requestSettings,
|
||||
repoId,
|
||||
repo?.connectionId,
|
||||
repo?.executionHostId
|
||||
repo?.executionHostId,
|
||||
repo !== undefined
|
||||
)
|
||||
// Why: if a prior caller without a linkedPR cached `null` for this branch,
|
||||
// the worktree-card lookup (which has a linked PR fallback) would otherwise
|
||||
|
|
@ -2836,7 +2896,8 @@ export const createGitHubSlice: StateCreator<AppState, [], [], GitHubSlice> = (s
|
|||
repoId,
|
||||
branch,
|
||||
repo?.connectionId,
|
||||
repo?.executionHostId
|
||||
repo?.executionHostId,
|
||||
repo !== undefined
|
||||
)
|
||||
const fallbackPRNumber =
|
||||
linkedPRNumber == null ? (explicitFallbackPRNumber ?? hostedReviewFallbackPRNumber) : null
|
||||
|
|
@ -2954,6 +3015,7 @@ export const createGitHubSlice: StateCreator<AppState, [], [], GitHubSlice> = (s
|
|||
repoId,
|
||||
connectionId: repo?.connectionId,
|
||||
executionHostId: repo?.executionHostId,
|
||||
hasRepoOwner: repo !== undefined,
|
||||
pr,
|
||||
fetchedAt: outcome.fetchedAt,
|
||||
worktreeId: options?.worktreeId,
|
||||
|
|
@ -3016,7 +3078,7 @@ export const createGitHubSlice: StateCreator<AppState, [], [], GitHubSlice> = (s
|
|||
fetchIssue: async (repoPath, number, options) => {
|
||||
const repo = findRepoForGitHubOwner(get(), options?.repoId, repoPath)
|
||||
const repoId = options?.repoId ?? repo?.id
|
||||
const requestSettings = getGitHubWorkItemSourceSettings(
|
||||
const requestSettings = getGitHubRepoSourceSettings(
|
||||
get().settings,
|
||||
repo,
|
||||
options?.sourceContext
|
||||
|
|
@ -3028,7 +3090,8 @@ export const createGitHubSlice: StateCreator<AppState, [], [], GitHubSlice> = (s
|
|||
requestSettings,
|
||||
repo?.connectionId,
|
||||
repo?.executionHostId,
|
||||
options?.sourceContext
|
||||
options?.sourceContext,
|
||||
repo !== undefined
|
||||
)
|
||||
const cached = get().issueCache[cacheKey]
|
||||
if (isFresh(cached)) {
|
||||
|
|
@ -3102,7 +3165,7 @@ export const createGitHubSlice: StateCreator<AppState, [], [], GitHubSlice> = (s
|
|||
options?.repoId ? candidate.id === options.repoId : candidate.path === repoPath
|
||||
)
|
||||
const repoId = options?.repoId ?? repo?.id
|
||||
const requestSettings = getGitHubWorkItemSourceSettings(
|
||||
const requestSettings = getGitHubRepoSourceSettings(
|
||||
get().settings,
|
||||
repo,
|
||||
options?.sourceContext
|
||||
|
|
@ -3114,7 +3177,8 @@ export const createGitHubSlice: StateCreator<AppState, [], [], GitHubSlice> = (s
|
|||
requestSettings,
|
||||
repo?.connectionId,
|
||||
repo?.executionHostId,
|
||||
options?.sourceContext
|
||||
options?.sourceContext,
|
||||
repo !== undefined
|
||||
)
|
||||
const legacyCacheKey = headSha
|
||||
? sourceScopedRepoCacheKey(
|
||||
|
|
@ -3124,7 +3188,8 @@ export const createGitHubSlice: StateCreator<AppState, [], [], GitHubSlice> = (s
|
|||
requestSettings,
|
||||
repo?.connectionId,
|
||||
repo?.executionHostId,
|
||||
options?.sourceContext
|
||||
options?.sourceContext,
|
||||
repo !== undefined
|
||||
)
|
||||
: cacheKey
|
||||
const inflightKey = cacheKey
|
||||
|
|
@ -3146,7 +3211,8 @@ export const createGitHubSlice: StateCreator<AppState, [], [], GitHubSlice> = (s
|
|||
prRepo,
|
||||
requestSettings,
|
||||
repo?.connectionId,
|
||||
repo?.executionHostId
|
||||
repo?.executionHostId,
|
||||
repo !== undefined
|
||||
)
|
||||
if (prStatusUpdate) {
|
||||
set(prStatusUpdate)
|
||||
|
|
@ -3218,7 +3284,8 @@ export const createGitHubSlice: StateCreator<AppState, [], [], GitHubSlice> = (s
|
|||
prRepo,
|
||||
requestSettings,
|
||||
repo?.connectionId,
|
||||
repo?.executionHostId
|
||||
repo?.executionHostId,
|
||||
repo !== undefined
|
||||
)
|
||||
if (prStatusUpdate?.prCache) {
|
||||
nextState.prCache = prStatusUpdate.prCache
|
||||
|
|
@ -3253,7 +3320,7 @@ export const createGitHubSlice: StateCreator<AppState, [], [], GitHubSlice> = (s
|
|||
options?.repoId ? candidate.id === options.repoId : candidate.path === repoPath
|
||||
)
|
||||
const repoId = options?.repoId ?? repo?.id
|
||||
const requestSettings = getGitHubWorkItemSourceSettings(
|
||||
const requestSettings = getGitHubRepoSourceSettings(
|
||||
get().settings,
|
||||
repo,
|
||||
options?.sourceContext
|
||||
|
|
@ -3296,7 +3363,7 @@ export const createGitHubSlice: StateCreator<AppState, [], [], GitHubSlice> = (s
|
|||
options?.repoId ? candidate.id === options.repoId : candidate.path === repoPath
|
||||
)
|
||||
const repoId = options?.repoId ?? repo?.id
|
||||
const requestSettings = getGitHubWorkItemSourceSettings(
|
||||
const requestSettings = getGitHubRepoSourceSettings(
|
||||
get().settings,
|
||||
repo,
|
||||
options?.sourceContext
|
||||
|
|
@ -3308,7 +3375,8 @@ export const createGitHubSlice: StateCreator<AppState, [], [], GitHubSlice> = (s
|
|||
requestSettings,
|
||||
repo?.connectionId,
|
||||
repo?.executionHostId,
|
||||
options?.sourceContext
|
||||
options?.sourceContext,
|
||||
repo !== undefined
|
||||
)
|
||||
const cached = get().commentsCache[cacheKey]
|
||||
if (!options?.force && isFresh(cached)) {
|
||||
|
|
@ -3374,7 +3442,7 @@ export const createGitHubSlice: StateCreator<AppState, [], [], GitHubSlice> = (s
|
|||
options?.repoId ? candidate.id === options.repoId : candidate.path === repoPath
|
||||
)
|
||||
const repoId = options?.repoId ?? repo?.id
|
||||
const requestSettings = getGitHubWorkItemSourceSettings(
|
||||
const requestSettings = getGitHubRepoSourceSettings(
|
||||
get().settings,
|
||||
repo,
|
||||
options?.sourceContext
|
||||
|
|
@ -3386,7 +3454,8 @@ export const createGitHubSlice: StateCreator<AppState, [], [], GitHubSlice> = (s
|
|||
requestSettings,
|
||||
repo?.connectionId,
|
||||
repo?.executionHostId,
|
||||
options?.sourceContext
|
||||
options?.sourceContext,
|
||||
repo !== undefined
|
||||
)
|
||||
const requestContext = getGitHubWorkItemRequestContext(
|
||||
get(),
|
||||
|
|
@ -3452,7 +3521,7 @@ export const createGitHubSlice: StateCreator<AppState, [], [], GitHubSlice> = (s
|
|||
options?.repoId ? candidate.id === options.repoId : candidate.path === repoPath
|
||||
)
|
||||
const repoId = options?.repoId ?? repo?.id
|
||||
const requestSettings = getGitHubWorkItemSourceSettings(
|
||||
const requestSettings = getGitHubRepoSourceSettings(
|
||||
get().settings,
|
||||
repo,
|
||||
options?.sourceContext
|
||||
|
|
@ -3464,7 +3533,8 @@ export const createGitHubSlice: StateCreator<AppState, [], [], GitHubSlice> = (s
|
|||
requestSettings,
|
||||
repo?.connectionId,
|
||||
repo?.executionHostId,
|
||||
options?.sourceContext
|
||||
options?.sourceContext,
|
||||
repo !== undefined
|
||||
)
|
||||
const requestContext = getGitHubWorkItemRequestContext(
|
||||
get(),
|
||||
|
|
@ -3542,7 +3612,7 @@ export const createGitHubSlice: StateCreator<AppState, [], [], GitHubSlice> = (s
|
|||
options?.repoId ? candidate.id === options.repoId : candidate.path === repoPath
|
||||
)
|
||||
const repoId = options?.repoId ?? repo?.id
|
||||
const requestSettings = getGitHubWorkItemSourceSettings(
|
||||
const requestSettings = getGitHubRepoSourceSettings(
|
||||
get().settings,
|
||||
repo,
|
||||
options?.sourceContext
|
||||
|
|
@ -3554,7 +3624,8 @@ export const createGitHubSlice: StateCreator<AppState, [], [], GitHubSlice> = (s
|
|||
requestSettings,
|
||||
repo?.connectionId,
|
||||
repo?.executionHostId,
|
||||
options?.sourceContext
|
||||
options?.sourceContext,
|
||||
repo !== undefined
|
||||
)
|
||||
|
||||
// Optimistic update: toggle isResolved on all comments in this thread immediately
|
||||
|
|
@ -3740,7 +3811,8 @@ export const createGitHubSlice: StateCreator<AppState, [], [], GitHubSlice> = (s
|
|||
prChecksCacheSuffix(pr.number, pr.prRepo, pr.headSha),
|
||||
s.settings,
|
||||
alias.connectionId,
|
||||
aliasExecutionHostId
|
||||
aliasExecutionHostId,
|
||||
true
|
||||
)
|
||||
]
|
||||
: []),
|
||||
|
|
@ -3750,7 +3822,8 @@ export const createGitHubSlice: StateCreator<AppState, [], [], GitHubSlice> = (s
|
|||
prChecksCacheSuffix(pr.number, pr.prRepo),
|
||||
s.settings,
|
||||
alias.connectionId,
|
||||
aliasExecutionHostId
|
||||
aliasExecutionHostId,
|
||||
true
|
||||
)
|
||||
]
|
||||
: []),
|
||||
|
|
@ -3762,7 +3835,8 @@ export const createGitHubSlice: StateCreator<AppState, [], [], GitHubSlice> = (s
|
|||
prChecksCacheSuffix(pr.number, pr.prRepo, pr.headSha),
|
||||
s.settings,
|
||||
alias.connectionId,
|
||||
aliasExecutionHostId
|
||||
aliasExecutionHostId,
|
||||
true
|
||||
)
|
||||
]
|
||||
: []),
|
||||
|
|
@ -3772,7 +3846,8 @@ export const createGitHubSlice: StateCreator<AppState, [], [], GitHubSlice> = (s
|
|||
prChecksCacheSuffix(pr.number, pr.prRepo),
|
||||
s.settings,
|
||||
alias.connectionId,
|
||||
aliasExecutionHostId
|
||||
aliasExecutionHostId,
|
||||
true
|
||||
),
|
||||
`${alias.repoPath}::pr-checks::${pr.number}`
|
||||
]
|
||||
|
|
@ -3807,6 +3882,7 @@ export const createGitHubSlice: StateCreator<AppState, [], [], GitHubSlice> = (s
|
|||
repoId: alias.repoId,
|
||||
connectionId: alias.connectionId,
|
||||
executionHostId: aliasExecutionHostId,
|
||||
hasRepoOwner: true,
|
||||
pr: data,
|
||||
fetchedAt: event.outcome.fetchedAt,
|
||||
state: s,
|
||||
|
|
@ -3834,7 +3910,8 @@ export const createGitHubSlice: StateCreator<AppState, [], [], GitHubSlice> = (s
|
|||
s.settings,
|
||||
alias.repoId,
|
||||
alias.connectionId,
|
||||
aliasExecutionHostId
|
||||
aliasExecutionHostId,
|
||||
true
|
||||
)
|
||||
setPRRefreshStartedHostedReviewEntry(
|
||||
prRefreshStartedEntryKey(event.sequence, alias.cacheKey),
|
||||
|
|
@ -3947,7 +4024,8 @@ export const createGitHubSlice: StateCreator<AppState, [], [], GitHubSlice> = (s
|
|||
wt.linkedIssue,
|
||||
ownerSettings,
|
||||
repo.connectionId,
|
||||
repo.executionHostId
|
||||
repo.executionHostId,
|
||||
true
|
||||
)
|
||||
const issueEntry = state.issueCache[issueKey]
|
||||
if (!issueEntry || now - issueEntry.fetchedAt >= CACHE_TTL) {
|
||||
|
|
@ -4014,7 +4092,8 @@ export const createGitHubSlice: StateCreator<AppState, [], [], GitHubSlice> = (s
|
|||
worktree.linkedIssue,
|
||||
ownerSettings,
|
||||
repo.connectionId,
|
||||
repo.executionHostId
|
||||
repo.executionHostId,
|
||||
true
|
||||
)
|
||||
: ''
|
||||
|
||||
|
|
@ -4257,7 +4336,8 @@ export const createGitHubSlice: StateCreator<AppState, [], [], GitHubSlice> = (s
|
|||
worktree.linkedIssue,
|
||||
ownerSettings,
|
||||
repo.connectionId,
|
||||
repo.executionHostId
|
||||
repo.executionHostId,
|
||||
true
|
||||
)
|
||||
const issueEntry = state.issueCache[issueKey]
|
||||
if (!issueEntry || now - issueEntry.fetchedAt >= CACHE_TTL) {
|
||||
|
|
|
|||
|
|
@ -20,16 +20,18 @@ export function getHostedReviewCacheKey(
|
|||
settings?: Pick<GlobalSettings, 'activeRuntimeEnvironmentId'> | null,
|
||||
repoId?: string | null,
|
||||
connectionId?: string | null,
|
||||
executionHostId?: string | null
|
||||
executionHostId?: string | null,
|
||||
hasRepoOwner = false
|
||||
): string {
|
||||
const scope = getHostedReviewCacheHostScope(settings, connectionId, executionHostId)
|
||||
const scope = getHostedReviewCacheHostScope(settings, connectionId, executionHostId, hasRepoOwner)
|
||||
return `${scope}::${repoId ?? repoPath}::${branch}`
|
||||
}
|
||||
|
||||
function getHostedReviewCacheHostScope(
|
||||
settings?: Pick<GlobalSettings, 'activeRuntimeEnvironmentId'> | null,
|
||||
connectionId?: string | null,
|
||||
executionHostId?: string | null
|
||||
executionHostId?: string | null,
|
||||
hasRepoOwner = false
|
||||
): string {
|
||||
const hostId = normalizeExecutionHostId(executionHostId)
|
||||
if (hostId) {
|
||||
|
|
@ -39,6 +41,11 @@ function getHostedReviewCacheHostScope(
|
|||
if (sshConnectionId) {
|
||||
return toSshExecutionHostId(sshConnectionId)
|
||||
}
|
||||
// Why: a known repo owner with no SSH/runtime marker is local; absent owner
|
||||
// context keeps the focused-runtime fallback for active-host operations.
|
||||
if (hasRepoOwner) {
|
||||
return 'local'
|
||||
}
|
||||
return getSettingsFocusedExecutionHostId(settings)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -90,7 +90,15 @@ describe('hosted review cache race protection', () => {
|
|||
})
|
||||
mockApi.hostedReview.forBranch.mockReturnValueOnce(fetch)
|
||||
const store = makeStore()
|
||||
const cacheKey = getHostedReviewCacheKey('/repo', 'feature/race')
|
||||
const cacheKey = getHostedReviewCacheKey(
|
||||
'/repo',
|
||||
'feature/race',
|
||||
null,
|
||||
'repo-1',
|
||||
null,
|
||||
null,
|
||||
true
|
||||
)
|
||||
|
||||
const request = store.getState().fetchHostedReviewForBranch('/repo', 'feature/race')
|
||||
vi.setSystemTime(200)
|
||||
|
|
@ -125,7 +133,15 @@ describe('hosted review cache race protection', () => {
|
|||
})
|
||||
mockApi.hostedReview.forBranch.mockReturnValueOnce(fetch)
|
||||
const store = makeStore()
|
||||
const cacheKey = getHostedReviewCacheKey('/repo', 'feature/error-race')
|
||||
const cacheKey = getHostedReviewCacheKey(
|
||||
'/repo',
|
||||
'feature/error-race',
|
||||
null,
|
||||
'repo-1',
|
||||
null,
|
||||
null,
|
||||
true
|
||||
)
|
||||
|
||||
try {
|
||||
const request = store.getState().fetchHostedReviewForBranch('/repo', 'feature/error-race')
|
||||
|
|
@ -167,7 +183,15 @@ describe('hosted review cache race protection', () => {
|
|||
})
|
||||
mockApi.hostedReview.forBranch.mockReturnValueOnce(fetch)
|
||||
const store = makeStore()
|
||||
const cacheKey = getHostedReviewCacheKey('/repo', 'feature/same-ms-race')
|
||||
const cacheKey = getHostedReviewCacheKey(
|
||||
'/repo',
|
||||
'feature/same-ms-race',
|
||||
null,
|
||||
'repo-1',
|
||||
null,
|
||||
null,
|
||||
true
|
||||
)
|
||||
|
||||
const request = store.getState().fetchHostedReviewForBranch('/repo', 'feature/same-ms-race')
|
||||
store.setState({
|
||||
|
|
@ -202,7 +226,15 @@ describe('hosted review cache race protection', () => {
|
|||
}
|
||||
mockApi.hostedReview.forBranch.mockResolvedValueOnce(freshReview)
|
||||
const store = makeStore()
|
||||
const cacheKey = getHostedReviewCacheKey('/repo', 'feature/same-ms-existing')
|
||||
const cacheKey = getHostedReviewCacheKey(
|
||||
'/repo',
|
||||
'feature/same-ms-existing',
|
||||
null,
|
||||
'repo-1',
|
||||
null,
|
||||
null,
|
||||
true
|
||||
)
|
||||
|
||||
store.setState({
|
||||
hostedReviewCache: {
|
||||
|
|
|
|||
|
|
@ -139,7 +139,15 @@ describe('hosted review cache revalidation', () => {
|
|||
).resolves.toEqual(review)
|
||||
|
||||
expect(mockApi.hostedReview.forBranch).toHaveBeenCalledTimes(2)
|
||||
const cacheKey = getHostedReviewCacheKey('/repo', 'feature/pr')
|
||||
const cacheKey = getHostedReviewCacheKey(
|
||||
'/repo',
|
||||
'feature/pr',
|
||||
null,
|
||||
'repo-1',
|
||||
null,
|
||||
null,
|
||||
true
|
||||
)
|
||||
expect(store.getState().hostedReviewCache[cacheKey]?.data).toEqual(review)
|
||||
|
||||
resolveRefresh(updatedReview)
|
||||
|
|
@ -192,11 +200,14 @@ describe('hosted review cache revalidation', () => {
|
|||
}
|
||||
|
||||
expect(
|
||||
store.getState().hostedReviewCache[getHostedReviewCacheKey('/repo', 'feature/cache-0')]
|
||||
store.getState().hostedReviewCache[
|
||||
getHostedReviewCacheKey('/repo', 'feature/cache-0', null, 'repo-1', null, null, true)
|
||||
]
|
||||
).toBeUndefined()
|
||||
expect(
|
||||
store.getState().hostedReviewCache[getHostedReviewCacheKey('/repo', 'feature/cache-500')]
|
||||
?.data
|
||||
store.getState().hostedReviewCache[
|
||||
getHostedReviewCacheKey('/repo', 'feature/cache-500', null, 'repo-1', null, null, true)
|
||||
]?.data
|
||||
).toMatchObject({ title: 'feature/cache-500' })
|
||||
expect(Object.keys(store.getState().hostedReviewCache)).toHaveLength(500)
|
||||
expect(_getHostedReviewRequestGenerationCountForTest()).toBe(0)
|
||||
|
|
|
|||
|
|
@ -159,6 +159,30 @@ describe('hosted review slice', () => {
|
|||
expect(store.getState().hostedReviewCache['local::repo-1::feature/gitlab']).toBeUndefined()
|
||||
})
|
||||
|
||||
it('uses local hosted-review IPC for a known local repo while a runtime is focused', async () => {
|
||||
mockApi.hostedReview.forBranch.mockResolvedValueOnce(review)
|
||||
const store = makeStore({
|
||||
activeRuntimeEnvironmentId: 'env-win'
|
||||
} as AppState['settings'])
|
||||
|
||||
await expect(
|
||||
store.getState().fetchHostedReviewForBranch('/repo', 'feature/local', {
|
||||
repoId: 'repo-1'
|
||||
})
|
||||
).resolves.toEqual(review)
|
||||
|
||||
expect(runtimeRpc.callRuntimeRpc).not.toHaveBeenCalled()
|
||||
expect(mockApi.hostedReview.forBranch).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ repoPath: '/repo', branch: 'feature/local' })
|
||||
)
|
||||
expect(store.getState().hostedReviewCache['local::repo-1::feature/local']).toMatchObject({
|
||||
data: review
|
||||
})
|
||||
expect(
|
||||
store.getState().hostedReviewCache['runtime:env-win::repo-1::feature/local']
|
||||
).toBeUndefined()
|
||||
})
|
||||
|
||||
it('routes active runtime review lookups through runtime RPC', async () => {
|
||||
runtimeRpc.callRuntimeRpc.mockResolvedValueOnce(review)
|
||||
const store = makeStore({
|
||||
|
|
|
|||
|
|
@ -135,7 +135,7 @@ function settingsForHostedReviewRepoOwner(
|
|||
settings: AppState['settings'],
|
||||
repo: Pick<Repo, 'connectionId' | 'executionHostId'> | undefined
|
||||
): AppState['settings'] {
|
||||
if (!repo?.executionHostId && !repo?.connectionId) {
|
||||
if (!repo) {
|
||||
return settings
|
||||
}
|
||||
const parsed = parseExecutionHostId(getRepoExecutionHostId(repo))
|
||||
|
|
@ -151,6 +151,16 @@ function settingsForHostedReviewRepoOwner(
|
|||
: ({ activeRuntimeEnvironmentId: null } as AppState['settings'])
|
||||
}
|
||||
|
||||
function settingsForHostedReviewActionOwner(
|
||||
settings: AppState['settings'],
|
||||
repo: Pick<Repo, 'connectionId' | 'executionHostId'> | undefined
|
||||
): AppState['settings'] {
|
||||
if (!repo?.executionHostId && !repo?.connectionId) {
|
||||
return settings
|
||||
}
|
||||
return settingsForHostedReviewRepoOwner(settings, repo)
|
||||
}
|
||||
|
||||
export type HostedReviewSlice = {
|
||||
hostedReviewCache: Record<string, CacheEntry<HostedReviewInfo>>
|
||||
getHostedReviewCreationEligibility: (
|
||||
|
|
@ -205,7 +215,7 @@ export const createHostedReviewSlice: StateCreator<AppState, [], [], HostedRevie
|
|||
getHostedReviewCreationEligibility: async (args) => {
|
||||
const settings = get().settings
|
||||
const repo = findHostedReviewRepoByPath(get().repos, args.repoPath, args.repoId)
|
||||
const ownerSettings = settingsForHostedReviewRepoOwner(settings, repo)
|
||||
const ownerSettings = settingsForHostedReviewActionOwner(settings, repo)
|
||||
const target = getActiveRuntimeTarget(ownerSettings)
|
||||
if (target.kind === 'environment') {
|
||||
const { repoPath: _repoPath, worktreePath, ...runtimeArgs } = args
|
||||
|
|
@ -231,7 +241,7 @@ export const createHostedReviewSlice: StateCreator<AppState, [], [], HostedRevie
|
|||
createHostedReview: async (repoPath, input) => {
|
||||
const settings = get().settings
|
||||
const repo = findHostedReviewRepoByPath(get().repos, repoPath, input.repoId)
|
||||
const ownerSettings = settingsForHostedReviewRepoOwner(settings, repo)
|
||||
const ownerSettings = settingsForHostedReviewActionOwner(settings, repo)
|
||||
const target = getActiveRuntimeTarget(ownerSettings)
|
||||
const { repoId: inputRepoId, ...hostedReviewInput } = input
|
||||
if (target.kind === 'environment') {
|
||||
|
|
@ -271,9 +281,10 @@ export const createHostedReviewSlice: StateCreator<AppState, [], [], HostedRevie
|
|||
repoPath,
|
||||
branch,
|
||||
ownerSettings,
|
||||
options?.repoId,
|
||||
repoId,
|
||||
repo?.connectionId,
|
||||
repo?.executionHostId
|
||||
repo?.executionHostId,
|
||||
repo !== undefined
|
||||
)
|
||||
const cached = get().hostedReviewCache[cacheKey]
|
||||
const hintKey = linkedReviewHintKey(options)
|
||||
|
|
@ -341,7 +352,8 @@ export const createHostedReviewSlice: StateCreator<AppState, [], [], HostedRevie
|
|||
branch,
|
||||
ownerSettings,
|
||||
repo?.connectionId,
|
||||
repo?.executionHostId
|
||||
repo?.executionHostId,
|
||||
repo !== undefined
|
||||
),
|
||||
getLegacyGitHubPRCacheKey(repoPath, repoId, branch),
|
||||
getLegacyGitHubPRCacheKey(repoPath, undefined, branch)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,42 @@
|
|||
import { describe, expect, it } from 'vitest'
|
||||
import { getGitHubPRCacheKey } from './github-cache-key'
|
||||
import { getHostedReviewCacheKey } from './hosted-review-cache-identity'
|
||||
|
||||
const focusedRuntime = { activeRuntimeEnvironmentId: 'env-focused' }
|
||||
|
||||
describe('repo owner cache identity', () => {
|
||||
it('uses local PR and hosted-review keys for known local repos while a runtime is focused', () => {
|
||||
expect(
|
||||
getGitHubPRCacheKey('/repo', 'repo-1', 'feature/local', focusedRuntime, null, null, true)
|
||||
).toBe('repo-1::feature/local')
|
||||
expect(
|
||||
getHostedReviewCacheKey('/repo', 'feature/local', focusedRuntime, 'repo-1', null, null, true)
|
||||
).toBe('local::repo-1::feature/local')
|
||||
})
|
||||
|
||||
it('preserves focused-runtime fallback when repo owner context is missing', () => {
|
||||
expect(getGitHubPRCacheKey('/repo', 'repo-1', 'feature/local', focusedRuntime)).toBe(
|
||||
'runtime:env-focused::repo-1::feature/local'
|
||||
)
|
||||
expect(getHostedReviewCacheKey('/repo', 'feature/local', focusedRuntime, 'repo-1')).toBe(
|
||||
'runtime:env-focused::repo-1::feature/local'
|
||||
)
|
||||
})
|
||||
|
||||
it('keeps explicit runtime and SSH owners scoped to their owner host', () => {
|
||||
expect(
|
||||
getGitHubPRCacheKey(
|
||||
'/repo',
|
||||
'repo-1',
|
||||
'feature/remote',
|
||||
null,
|
||||
null,
|
||||
'runtime:env-owner',
|
||||
true
|
||||
)
|
||||
).toBe('runtime:env-owner::repo-1::feature/remote')
|
||||
expect(
|
||||
getHostedReviewCacheKey('/repo', 'feature/ssh', focusedRuntime, 'repo-1', 'ssh-1', null, true)
|
||||
).toBe('ssh:ssh-1::repo-1::feature/ssh')
|
||||
})
|
||||
})
|
||||
|
|
@ -19,6 +19,7 @@ import {
|
|||
type RuntimeEnvironmentCallRequest
|
||||
} from '../../runtime/runtime-compatibility-test-fixture'
|
||||
import { clearRuntimeCompatibilityCacheForTests } from '../../runtime/runtime-rpc-client'
|
||||
import { LOCAL_EXECUTION_HOST_ID } from '../../../../shared/execution-host'
|
||||
|
||||
vi.mock('sonner', () => ({
|
||||
toast: {
|
||||
|
|
@ -3257,7 +3258,14 @@ describe('worktree remote runtime mutations', () => {
|
|||
})
|
||||
store.setState({
|
||||
repos: [
|
||||
{ id: 'repo1', path: '/repo1', displayName: 'Repo 1', badgeColor: '#000', addedAt: 0 }
|
||||
{
|
||||
id: 'repo1',
|
||||
path: '/repo1',
|
||||
displayName: 'Repo 1',
|
||||
badgeColor: '#000',
|
||||
addedAt: 0,
|
||||
executionHostId: LOCAL_EXECUTION_HOST_ID
|
||||
}
|
||||
],
|
||||
worktreesByRepo: { repo1: [wt] }
|
||||
} as Partial<AppState>)
|
||||
|
|
@ -4146,8 +4154,42 @@ describe('worktree remote runtime mutations', () => {
|
|||
linkedPR: 456
|
||||
})
|
||||
const fetchHostedReviewForBranch = vi.fn().mockResolvedValue(null)
|
||||
const cacheKey = getHostedReviewCacheKey('/repo1', 'pr-branch', undefined, 'repo1')
|
||||
const prCacheKey = getGitHubPRCacheKey('/repo1', 'repo1', 'pr-branch')
|
||||
runtimeEnvironmentCall.mockImplementation(({ method }: RuntimeEnvironmentCallRequest) => ({
|
||||
id: `test-${method}`,
|
||||
ok: true,
|
||||
result: method === 'worktrees.list' ? [] : null
|
||||
}))
|
||||
const focusedRuntimeSettings = { activeRuntimeEnvironmentId: 'env-win' } as AppState['settings']
|
||||
const cacheKey = getHostedReviewCacheKey(
|
||||
'/repo1',
|
||||
'pr-branch',
|
||||
focusedRuntimeSettings,
|
||||
'repo1',
|
||||
null,
|
||||
null,
|
||||
true
|
||||
)
|
||||
const runtimeCacheKey = getHostedReviewCacheKey(
|
||||
'/repo1',
|
||||
'pr-branch',
|
||||
focusedRuntimeSettings,
|
||||
'repo1'
|
||||
)
|
||||
const prCacheKey = getGitHubPRCacheKey(
|
||||
'/repo1',
|
||||
'repo1',
|
||||
'pr-branch',
|
||||
focusedRuntimeSettings,
|
||||
null,
|
||||
null,
|
||||
true
|
||||
)
|
||||
const runtimePRCacheKey = getGitHubPRCacheKey(
|
||||
'/repo1',
|
||||
'repo1',
|
||||
'pr-branch',
|
||||
focusedRuntimeSettings
|
||||
)
|
||||
const legacyRepoPRCacheKey = getLegacyGitHubPRCacheKey('/repo1', 'repo1', 'pr-branch')
|
||||
const legacyPathPRCacheKey = getLegacyGitHubPRCacheKey('/repo1', undefined, 'pr-branch')
|
||||
const prData = {
|
||||
|
|
@ -4160,6 +4202,7 @@ describe('worktree remote runtime mutations', () => {
|
|||
mergeable: 'MERGEABLE' as const
|
||||
}
|
||||
store.setState({
|
||||
settings: focusedRuntimeSettings,
|
||||
repos: [
|
||||
{ id: 'repo1', path: '/repo1', displayName: 'Repo 1', badgeColor: '#000', addedAt: 0 }
|
||||
],
|
||||
|
|
@ -4177,6 +4220,10 @@ describe('worktree remote runtime mutations', () => {
|
|||
mergeable: 'MERGEABLE'
|
||||
},
|
||||
fetchedAt: Date.now()
|
||||
},
|
||||
[runtimeCacheKey]: {
|
||||
data: null,
|
||||
fetchedAt: Date.now()
|
||||
}
|
||||
},
|
||||
prCache: {
|
||||
|
|
@ -4184,6 +4231,10 @@ describe('worktree remote runtime mutations', () => {
|
|||
data: prData,
|
||||
fetchedAt: Date.now()
|
||||
},
|
||||
[runtimePRCacheKey]: {
|
||||
data: { ...prData, title: 'Focused runtime PR' },
|
||||
fetchedAt: Date.now()
|
||||
},
|
||||
[legacyRepoPRCacheKey]: {
|
||||
data: { ...prData, title: 'Legacy repo-scoped PR' },
|
||||
fetchedAt: Date.now()
|
||||
|
|
@ -4200,7 +4251,9 @@ describe('worktree remote runtime mutations', () => {
|
|||
|
||||
expect(store.getState().worktreesByRepo.repo1[0]?.linkedPR).toBeNull()
|
||||
expect(store.getState().hostedReviewCache[cacheKey]).toBeUndefined()
|
||||
expect(store.getState().hostedReviewCache[runtimeCacheKey]).toBeDefined()
|
||||
expect(store.getState().prCache[prCacheKey]).toBeUndefined()
|
||||
expect(store.getState().prCache[runtimePRCacheKey]).toBeDefined()
|
||||
expect(store.getState().prCache[legacyRepoPRCacheKey]).toBeUndefined()
|
||||
expect(store.getState().prCache[legacyPathPRCacheKey]).toBeUndefined()
|
||||
expect(fetchHostedReviewForBranch).toHaveBeenCalledWith('/repo1', 'pr-branch', {
|
||||
|
|
|
|||
|
|
@ -2955,7 +2955,8 @@ export const createWorktreeSlice: StateCreator<AppState, [], [], WorktreeSlice>
|
|||
s.settings,
|
||||
reviewRepo.id,
|
||||
reviewRepo.connectionId,
|
||||
reviewRepo.executionHostId
|
||||
reviewRepo.executionHostId,
|
||||
true
|
||||
)
|
||||
: null
|
||||
const prCacheKey =
|
||||
|
|
@ -2966,7 +2967,8 @@ export const createWorktreeSlice: StateCreator<AppState, [], [], WorktreeSlice>
|
|||
reviewBranch,
|
||||
s.settings,
|
||||
reviewRepo.connectionId,
|
||||
reviewRepo.executionHostId
|
||||
reviewRepo.executionHostId,
|
||||
true
|
||||
)
|
||||
: null
|
||||
const prCacheKeys =
|
||||
|
|
|
|||
Loading…
Reference in New Issue