mirror of
https://github.com/stablyai/orca.git
synced 2026-10-20 00:03:45 +00:00
[P1] fix(routing): keep the paired-HUB owner for the active workspace selection
#11346 made `activeWorkspaceExecutionHostId` authoritative for the active workspace, but the short-circuits that read it never see the owner record, so a project reached through a paired runtime HUB (`hostId: ssh:<target>` plus `runtimeOwnerEnvironmentId: <hub>`) loses its transport and its repo: - `resolveWorktreeOperationRouteResult` returned `runtimeEnvironmentId: null` for any non-`runtime:` selection, bypassing the HUB recovery in `resolveExactWorktreeRoute` — every owner-routed terminal/git/filesystem operation on the active workspace lost the HUB that proxies the SSH target. - `selectRepoByIdForActiveWorkspace` filtered `state.repos` by the selected host with no fallback, but `withRepoHostOwnership` deliberately keeps the SSH host on the worktree while the repo row stays HUB-owned — so the active repo read as `null` and `useGitStatusPolling` stopped polling the whole workspace. Route resolution now recovers the HUB owner from the owner rows on the selected host, and drops it only when rival HUBs project the same host. The repo selector opens exactly one hole in #11346's fail-closed rule — an `ssh:` selection over a `runtime:` repo row — and every other host mismatch still returns `null`. `worktree-owner-route.ts` is a pure extraction of the existing owner-route helpers; no behavior moved with it.
This commit is contained in:
@@ -241,6 +241,98 @@ describe('resolveWorktreeOperationRouteResult', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('active workspace host selection', () => {
|
||||
it('keeps the HUB transport when the paired SSH worktree is the active workspace', () => {
|
||||
expect(
|
||||
resolveWorktreeOperationRouteResult(
|
||||
{
|
||||
activeWorktreeId: WORKTREE_ID,
|
||||
activeWorkspaceExecutionHostId: 'ssh:hub-private-target',
|
||||
worktreesByRepo: { 'repo-1': [worktree('ssh:hub-private-target', 'hub-a')] }
|
||||
},
|
||||
WORKTREE_ID
|
||||
)
|
||||
).toEqual({
|
||||
kind: 'resolved',
|
||||
route: { executionHostId: 'ssh:hub-private-target', runtimeEnvironmentId: 'hub-a' }
|
||||
})
|
||||
})
|
||||
|
||||
it('recovers the HUB transport from the repo for an active mixed-version SSH publication', () => {
|
||||
expect(
|
||||
resolveWorktreeOperationRouteResult(
|
||||
{
|
||||
activeWorktreeId: WORKTREE_ID,
|
||||
activeWorkspaceExecutionHostId: 'ssh:hub-private-target',
|
||||
repos: [
|
||||
{
|
||||
id: 'repo-1',
|
||||
connectionId: 'hub-private-target',
|
||||
executionHostId: 'runtime:hub-a'
|
||||
}
|
||||
],
|
||||
detectedWorktreesByRepo: {
|
||||
'repo-1': { worktrees: [worktree('ssh:hub-private-target')] }
|
||||
}
|
||||
},
|
||||
WORKTREE_ID
|
||||
)
|
||||
).toEqual({
|
||||
kind: 'resolved',
|
||||
route: { executionHostId: 'ssh:hub-private-target', runtimeEnvironmentId: 'hub-a' }
|
||||
})
|
||||
})
|
||||
|
||||
it('keeps the selected host authoritative when the same ID exists on two hosts', () => {
|
||||
expect(
|
||||
resolveWorktreeOperationRouteResult(
|
||||
{
|
||||
activeWorktreeId: WORKTREE_ID,
|
||||
activeWorkspaceExecutionHostId: 'runtime:hub-a',
|
||||
worktreesByRepo: { 'repo-1': [worktree('local'), worktree('runtime:hub-a')] }
|
||||
},
|
||||
WORKTREE_ID
|
||||
)
|
||||
).toEqual({
|
||||
kind: 'resolved',
|
||||
route: { executionHostId: 'runtime:hub-a', runtimeEnvironmentId: 'hub-a' }
|
||||
})
|
||||
})
|
||||
|
||||
it('drops the HUB transport when two HUBs project the active SSH worktree', () => {
|
||||
expect(
|
||||
resolveWorktreeOperationRouteResult(
|
||||
{
|
||||
activeWorktreeId: WORKTREE_ID,
|
||||
activeWorkspaceExecutionHostId: 'ssh:same-private-target',
|
||||
worktreesByRepo: {
|
||||
'repo-1': [
|
||||
worktree('ssh:same-private-target', 'hub-a'),
|
||||
worktree('ssh:same-private-target', 'hub-b')
|
||||
]
|
||||
}
|
||||
},
|
||||
WORKTREE_ID
|
||||
)
|
||||
).toEqual({
|
||||
kind: 'resolved',
|
||||
route: { executionHostId: 'ssh:same-private-target', runtimeEnvironmentId: null }
|
||||
})
|
||||
})
|
||||
|
||||
it('keeps an unknown active worktree on its selected host', () => {
|
||||
expect(
|
||||
resolveWorktreeOperationRouteResult(
|
||||
{ activeWorktreeId: WORKTREE_ID, activeWorkspaceExecutionHostId: 'ssh:ssh-1' },
|
||||
WORKTREE_ID
|
||||
)
|
||||
).toEqual({
|
||||
kind: 'resolved',
|
||||
route: { executionHostId: 'ssh:ssh-1', runtimeEnvironmentId: null }
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('folder workspaces', () => {
|
||||
const FOLDER_WORKSPACE_ID = 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee'
|
||||
const FOLDER_KEY = `folder:${FOLDER_WORKSPACE_ID}`
|
||||
|
||||
@@ -2,11 +2,11 @@ import type { AppState } from '@/store/types'
|
||||
import {
|
||||
getRepoExecutionHostId,
|
||||
parseExecutionHostId,
|
||||
toSshExecutionHostId,
|
||||
type ExecutionHostId
|
||||
} from '../../../shared/execution-host'
|
||||
import { parseWorkspaceKey } from '../../../shared/workspace-scope'
|
||||
import { getRepoIdFromWorktreeId } from '@/store/slices/worktree-helpers'
|
||||
import { addRoute, resolveExactWorktreeRoute, routeForOwner } from './worktree-owner-route'
|
||||
import { resolveIndexedWorktreeOwner } from './worktree-runtime-owner-index'
|
||||
import {
|
||||
findFolderWorkspaceOwner,
|
||||
@@ -24,7 +24,7 @@ export type WorktreeOperationRouteResolution =
|
||||
| { kind: 'ambiguous' }
|
||||
| { kind: 'missing' }
|
||||
|
||||
type WorktreeOperationOwnerRecord = {
|
||||
export type WorktreeOperationOwnerRecord = {
|
||||
id: string
|
||||
repoId: string
|
||||
hostId?: ExecutionHostId
|
||||
@@ -32,7 +32,7 @@ type WorktreeOperationOwnerRecord = {
|
||||
}
|
||||
|
||||
// settings/runtimeEnvironments come from FolderWorkspaceRuntimeOwnerState's legacy-owner base.
|
||||
type WorktreeOperationRouteState = FolderWorkspaceRuntimeOwnerState & {
|
||||
export type WorktreeOperationRouteState = FolderWorkspaceRuntimeOwnerState & {
|
||||
repos?: readonly Pick<AppState['repos'][number], 'id' | 'connectionId' | 'executionHostId'>[]
|
||||
worktreesByRepo?: Record<string, readonly WorktreeOperationOwnerRecord[]>
|
||||
detectedWorktreesByRepo?: Record<string, { worktrees: readonly WorktreeOperationOwnerRecord[] }>
|
||||
@@ -45,83 +45,67 @@ const repoOperationRouteIndexCache = new WeakMap<
|
||||
ReadonlyMap<string, WorktreeOperationRouteResolution>
|
||||
>()
|
||||
|
||||
function routeForOwner(owner: {
|
||||
hostId?: ExecutionHostId
|
||||
runtimeOwnerEnvironmentId?: string
|
||||
}): WorktreeOperationRoute | null {
|
||||
const runtimeOwnerEnvironmentId = owner.runtimeOwnerEnvironmentId?.trim()
|
||||
if (!owner.hostId && !runtimeOwnerEnvironmentId) {
|
||||
function ownerRecordsOnHost(
|
||||
state: WorktreeOperationRouteState,
|
||||
worktreeId: string,
|
||||
executionHostId: ExecutionHostId
|
||||
): WorktreeOperationOwnerRecord[] {
|
||||
const owners: WorktreeOperationOwnerRecord[] = []
|
||||
const catalogs = [
|
||||
...Object.values(state.worktreesByRepo ?? {}),
|
||||
...Object.values(state.detectedWorktreesByRepo ?? {}).map((result) => result.worktrees)
|
||||
]
|
||||
for (const worktrees of catalogs) {
|
||||
for (const worktree of worktrees) {
|
||||
if (
|
||||
worktree.id === worktreeId &&
|
||||
parseExecutionHostId(worktree.hostId)?.id === executionHostId
|
||||
) {
|
||||
owners.push(worktree)
|
||||
}
|
||||
}
|
||||
}
|
||||
return owners
|
||||
}
|
||||
|
||||
/**
|
||||
* The active workspace's host selection is authoritative identity, but it carries no transport:
|
||||
* an `ssh:` host reached through a paired HUB names the target, not the HUB that proxies it. Keep
|
||||
* the selected host and recover the runtime owner from the matching owner rows (#11346).
|
||||
*/
|
||||
export function resolveActiveWorkspaceRoute(
|
||||
state: WorktreeOperationRouteState,
|
||||
worktreeId: string
|
||||
): WorktreeOperationRoute | null {
|
||||
const activeHost =
|
||||
state.activeWorktreeId === worktreeId
|
||||
? parseExecutionHostId(state.activeWorkspaceExecutionHostId)
|
||||
: null
|
||||
if (!activeHost) {
|
||||
return null
|
||||
}
|
||||
const parsedHost = parseExecutionHostId(owner.hostId)
|
||||
if (activeHost.kind === 'runtime') {
|
||||
return { executionHostId: activeHost.id, runtimeEnvironmentId: activeHost.environmentId }
|
||||
}
|
||||
// Why: only an `ssh:` selection can hide a paired HUB owner, so local stays an O(1) hot path.
|
||||
if (activeHost.kind !== 'ssh') {
|
||||
return { executionHostId: activeHost.id, runtimeEnvironmentId: null }
|
||||
}
|
||||
const environmentIds = new Set<string>()
|
||||
for (const owner of ownerRecordsOnHost(state, worktreeId, activeHost.id)) {
|
||||
const resolution = resolveExactWorktreeRoute(state, owner)
|
||||
if (resolution.kind === 'resolved' && resolution.route.runtimeEnvironmentId) {
|
||||
environmentIds.add(resolution.route.runtimeEnvironmentId)
|
||||
}
|
||||
}
|
||||
const environmentId = environmentIds.values().next().value
|
||||
return {
|
||||
executionHostId: owner.hostId ?? null,
|
||||
runtimeEnvironmentId:
|
||||
runtimeOwnerEnvironmentId ||
|
||||
(parsedHost?.kind === 'runtime' ? parsedHost.environmentId : null)
|
||||
executionHostId: activeHost.id,
|
||||
// Why: rival HUBs projecting the same host cannot be disambiguated by the host selection alone.
|
||||
runtimeEnvironmentId: environmentIds.size === 1 && environmentId ? environmentId : null
|
||||
}
|
||||
}
|
||||
|
||||
function addRoute(
|
||||
routes: Map<string, WorktreeOperationRoute>,
|
||||
route: WorktreeOperationRoute | null
|
||||
): void {
|
||||
if (!route) {
|
||||
return
|
||||
}
|
||||
routes.set(JSON.stringify(route), route)
|
||||
}
|
||||
|
||||
function resolveRepoRouteForSshOwner(
|
||||
repos: WorktreeOperationRouteState['repos'],
|
||||
owner: WorktreeOperationOwnerRecord
|
||||
): WorktreeOperationRouteResolution {
|
||||
if (!repos || !owner.hostId) {
|
||||
return { kind: 'missing' }
|
||||
}
|
||||
const routes = new Map<string, WorktreeOperationRoute>()
|
||||
for (const repo of repos) {
|
||||
if (repo.id !== owner.repoId) {
|
||||
continue
|
||||
}
|
||||
const connectionHostId = repo.connectionId ? toSshExecutionHostId(repo.connectionId) : null
|
||||
if (getRepoExecutionHostId(repo) !== owner.hostId && connectionHostId !== owner.hostId) {
|
||||
continue
|
||||
}
|
||||
addRoute(routes, routeForOwner({ hostId: getRepoExecutionHostId(repo) }))
|
||||
}
|
||||
const route = routes.values().next().value
|
||||
if (routes.size === 1 && route) {
|
||||
return { kind: 'resolved', route }
|
||||
}
|
||||
return routes.size > 1 ? { kind: 'ambiguous' } : { kind: 'missing' }
|
||||
}
|
||||
|
||||
function resolveExactWorktreeRoute(
|
||||
state: WorktreeOperationRouteState,
|
||||
owner: WorktreeOperationOwnerRecord
|
||||
): WorktreeOperationRouteResolution {
|
||||
const route = routeForOwner(owner)
|
||||
if (!route) {
|
||||
return { kind: 'missing' }
|
||||
}
|
||||
if (route.runtimeEnvironmentId || parseExecutionHostId(route.executionHostId)?.kind !== 'ssh') {
|
||||
return { kind: 'resolved', route }
|
||||
}
|
||||
// Recover an optional HUB transport only from the repo setup matching the worktree's SSH host.
|
||||
const repoRoute = resolveRepoRouteForSshOwner(state.repos, owner)
|
||||
if (repoRoute.kind === 'ambiguous') {
|
||||
return repoRoute
|
||||
}
|
||||
if (repoRoute.kind === 'resolved' && repoRoute.route.runtimeEnvironmentId) {
|
||||
return {
|
||||
kind: 'resolved',
|
||||
route: { ...route, runtimeEnvironmentId: repoRoute.route.runtimeEnvironmentId }
|
||||
}
|
||||
}
|
||||
return { kind: 'resolved', route }
|
||||
}
|
||||
|
||||
export function resolveWorktreeOperationRoute(
|
||||
state: WorktreeOperationRouteState,
|
||||
worktreeId: string
|
||||
@@ -134,18 +118,9 @@ export function resolveWorktreeOperationRouteResult(
|
||||
state: WorktreeOperationRouteState,
|
||||
worktreeId: string
|
||||
): WorktreeOperationRouteResolution {
|
||||
const activeHost =
|
||||
state.activeWorktreeId === worktreeId
|
||||
? parseExecutionHostId(state.activeWorkspaceExecutionHostId)
|
||||
: null
|
||||
if (activeHost) {
|
||||
return {
|
||||
kind: 'resolved',
|
||||
route: {
|
||||
executionHostId: activeHost.id,
|
||||
runtimeEnvironmentId: activeHost.kind === 'runtime' ? activeHost.environmentId : null
|
||||
}
|
||||
}
|
||||
const activeRoute = resolveActiveWorkspaceRoute(state, worktreeId)
|
||||
if (activeRoute) {
|
||||
return { kind: 'resolved', route: activeRoute }
|
||||
}
|
||||
// Why: folder workspaces are not Git worktrees — they never appear in the worktree/repo
|
||||
// catalogs scanned below, so without this branch a plain local folder workspace reads as an
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
import {
|
||||
getRepoExecutionHostId,
|
||||
parseExecutionHostId,
|
||||
toSshExecutionHostId
|
||||
} from '../../../shared/execution-host'
|
||||
import type { ExecutionHostId } from '../../../shared/execution-host'
|
||||
import type {
|
||||
WorktreeOperationOwnerRecord,
|
||||
WorktreeOperationRoute,
|
||||
WorktreeOperationRouteResolution,
|
||||
WorktreeOperationRouteState
|
||||
} from './worktree-operation-route'
|
||||
|
||||
export function routeForOwner(owner: {
|
||||
hostId?: ExecutionHostId
|
||||
runtimeOwnerEnvironmentId?: string
|
||||
}): WorktreeOperationRoute | null {
|
||||
const runtimeOwnerEnvironmentId = owner.runtimeOwnerEnvironmentId?.trim()
|
||||
if (!owner.hostId && !runtimeOwnerEnvironmentId) {
|
||||
return null
|
||||
}
|
||||
const parsedHost = parseExecutionHostId(owner.hostId)
|
||||
return {
|
||||
executionHostId: owner.hostId ?? null,
|
||||
runtimeEnvironmentId:
|
||||
runtimeOwnerEnvironmentId ||
|
||||
(parsedHost?.kind === 'runtime' ? parsedHost.environmentId : null)
|
||||
}
|
||||
}
|
||||
|
||||
export function addRoute(
|
||||
routes: Map<string, WorktreeOperationRoute>,
|
||||
route: WorktreeOperationRoute | null
|
||||
): void {
|
||||
if (!route) {
|
||||
return
|
||||
}
|
||||
routes.set(JSON.stringify(route), route)
|
||||
}
|
||||
|
||||
function resolveRepoRouteForSshOwner(
|
||||
repos: WorktreeOperationRouteState['repos'],
|
||||
owner: WorktreeOperationOwnerRecord
|
||||
): WorktreeOperationRouteResolution {
|
||||
if (!repos || !owner.hostId) {
|
||||
return { kind: 'missing' }
|
||||
}
|
||||
const routes = new Map<string, WorktreeOperationRoute>()
|
||||
for (const repo of repos) {
|
||||
if (repo.id !== owner.repoId) {
|
||||
continue
|
||||
}
|
||||
const connectionHostId = repo.connectionId ? toSshExecutionHostId(repo.connectionId) : null
|
||||
if (getRepoExecutionHostId(repo) !== owner.hostId && connectionHostId !== owner.hostId) {
|
||||
continue
|
||||
}
|
||||
addRoute(routes, routeForOwner({ hostId: getRepoExecutionHostId(repo) }))
|
||||
}
|
||||
const route = routes.values().next().value
|
||||
if (routes.size === 1 && route) {
|
||||
return { kind: 'resolved', route }
|
||||
}
|
||||
return routes.size > 1 ? { kind: 'ambiguous' } : { kind: 'missing' }
|
||||
}
|
||||
|
||||
export function resolveExactWorktreeRoute(
|
||||
state: WorktreeOperationRouteState,
|
||||
owner: WorktreeOperationOwnerRecord
|
||||
): WorktreeOperationRouteResolution {
|
||||
const route = routeForOwner(owner)
|
||||
if (!route) {
|
||||
return { kind: 'missing' }
|
||||
}
|
||||
if (route.runtimeEnvironmentId || parseExecutionHostId(route.executionHostId)?.kind !== 'ssh') {
|
||||
return { kind: 'resolved', route }
|
||||
}
|
||||
// Recover an optional HUB transport only from the repo setup matching the worktree's SSH host.
|
||||
const repoRoute = resolveRepoRouteForSshOwner(state.repos, owner)
|
||||
if (repoRoute.kind === 'ambiguous') {
|
||||
return repoRoute
|
||||
}
|
||||
if (repoRoute.kind === 'resolved' && repoRoute.route.runtimeEnvironmentId) {
|
||||
return {
|
||||
kind: 'resolved',
|
||||
route: { ...route, runtimeEnvironmentId: repoRoute.route.runtimeEnvironmentId }
|
||||
}
|
||||
}
|
||||
return { kind: 'resolved', route }
|
||||
}
|
||||
@@ -579,3 +579,34 @@ describe('getRuntimeSessionMirrorEnvironmentIds', () => {
|
||||
expect(getRuntimeSessionMirrorEnvironmentIds(localOnlyState)).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
describe('active workspace host selection', () => {
|
||||
const PAIRED_HUB_WORKTREE_ID = 'hub-repo::wt-paired'
|
||||
const pairedHubState: WorktreeRuntimeOwnerState = {
|
||||
activeWorktreeId: PAIRED_HUB_WORKTREE_ID,
|
||||
activeWorkspaceExecutionHostId: 'ssh:hub-private-target',
|
||||
worktreesByRepo: {
|
||||
'hub-repo': [
|
||||
{
|
||||
id: PAIRED_HUB_WORKTREE_ID,
|
||||
repoId: 'hub-repo',
|
||||
hostId: 'ssh:hub-private-target',
|
||||
runtimeOwnerEnvironmentId: 'hub-a'
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
it('keeps the HUB transport for the active paired SSH worktree', () => {
|
||||
expect(getRuntimeEnvironmentIdForWorktree(pairedHubState, PAIRED_HUB_WORKTREE_ID)).toBe('hub-a')
|
||||
expect(getExplicitRuntimeEnvironmentIdForWorktree(pairedHubState, PAIRED_HUB_WORKTREE_ID)).toBe(
|
||||
'hub-a'
|
||||
)
|
||||
})
|
||||
|
||||
it('keeps the selected host authoritative for the active worktree', () => {
|
||||
expect(getExecutionHostIdForWorktree(pairedHubState, PAIRED_HUB_WORKTREE_ID)).toBe(
|
||||
'ssh:hub-private-target'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
getRuntimeEnvironmentIdForFolderWorkspace
|
||||
} from './folder-workspace-runtime-owner'
|
||||
import {
|
||||
resolveActiveWorkspaceRoute,
|
||||
resolveExplicitWorktreeOperationRouteResult,
|
||||
resolveWorktreeOperationRouteResult
|
||||
} from './worktree-operation-route'
|
||||
@@ -62,9 +63,9 @@ export function getRuntimeEnvironmentIdForWorktree(
|
||||
if (worktreeId === FLOATING_TERMINAL_WORKTREE_ID) {
|
||||
return null
|
||||
}
|
||||
const activeHost = parseExecutionHostId(getActiveWorkspaceExecutionHostId(state, worktreeId))
|
||||
if (activeHost) {
|
||||
return activeHost.kind === 'runtime' ? activeHost.environmentId : null
|
||||
const activeRoute = resolveActiveWorkspaceRoute(state, worktreeId)
|
||||
if (activeRoute) {
|
||||
return activeRoute.runtimeEnvironmentId
|
||||
}
|
||||
const workspaceScope = parseWorkspaceKey(worktreeId)
|
||||
if (workspaceScope?.type === 'folder') {
|
||||
@@ -113,9 +114,9 @@ export function getExplicitRuntimeEnvironmentIdForWorktree(
|
||||
if (!worktreeId) {
|
||||
return null
|
||||
}
|
||||
const activeHost = parseExecutionHostId(getActiveWorkspaceExecutionHostId(state, worktreeId))
|
||||
if (activeHost) {
|
||||
return activeHost.kind === 'runtime' ? activeHost.environmentId : null
|
||||
const activeRoute = resolveActiveWorkspaceRoute(state, worktreeId)
|
||||
if (activeRoute) {
|
||||
return activeRoute.runtimeEnvironmentId
|
||||
}
|
||||
const workspaceScope = parseWorkspaceKey(worktreeId)
|
||||
if (workspaceScope?.type === 'folder') {
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { beforeEach, describe, expect, it } from 'vitest'
|
||||
import type { Repo, TerminalTab, Worktree } from '../../../shared/types'
|
||||
import { FLOATING_TERMINAL_WORKTREE_ID } from '../../../shared/constants'
|
||||
import { toRuntimeExecutionHostId } from '../../../shared/execution-host'
|
||||
import { toRuntimeExecutionHostId, toSshExecutionHostId } from '../../../shared/execution-host'
|
||||
import { isGitRepoKind } from '../../../shared/repo-kind'
|
||||
import type { AppState } from './types'
|
||||
import {
|
||||
getAllWorktreesFromState,
|
||||
@@ -288,6 +289,100 @@ describe('store selectors', () => {
|
||||
).toBeNull()
|
||||
})
|
||||
|
||||
it('keeps the repo when a paired-hub worktree reports a different execution host', () => {
|
||||
// Why: withRepoHostOwnership deliberately keeps an SSH worktree's own host while its repo stays hub-owned.
|
||||
const repo = makeRepo({
|
||||
id: 'hub-repo',
|
||||
path: '/hub/repo',
|
||||
displayName: 'hub',
|
||||
executionHostId: toRuntimeExecutionHostId('hub-a')
|
||||
})
|
||||
|
||||
expect(
|
||||
selectRepoByIdForActiveWorkspace(
|
||||
{
|
||||
repos: [repo],
|
||||
activeRepoId: 'hub-repo',
|
||||
activeWorkspaceExecutionHostId: toSshExecutionHostId('hub-private-target')
|
||||
},
|
||||
'hub-repo'
|
||||
)
|
||||
).toBe(repo)
|
||||
// Why: useGitStatusPolling gates every lane on this exact expression, so a null repo silently stops polling.
|
||||
const activeRepo = selectRepoByIdForActiveWorkspace(
|
||||
{
|
||||
repos: [repo],
|
||||
activeRepoId: 'hub-repo',
|
||||
activeWorkspaceExecutionHostId: toSshExecutionHostId('hub-private-target')
|
||||
},
|
||||
'hub-repo'
|
||||
)
|
||||
expect(activeRepo ? isGitRepoKind(activeRepo) : false).toBe(true)
|
||||
})
|
||||
|
||||
it('still prefers the host-matching row when one repo ID spans hosts', () => {
|
||||
const hub = makeRepo({
|
||||
id: 'shared-repo',
|
||||
path: '/hub/repo',
|
||||
displayName: 'hub',
|
||||
executionHostId: toRuntimeExecutionHostId('hub-a')
|
||||
})
|
||||
const ssh = makeRepo({
|
||||
id: 'shared-repo',
|
||||
path: '/ssh/repo',
|
||||
displayName: 'ssh',
|
||||
executionHostId: toSshExecutionHostId('hub-private-target')
|
||||
})
|
||||
|
||||
expect(
|
||||
selectRepoByIdForActiveWorkspace(
|
||||
{
|
||||
repos: [hub, ssh],
|
||||
activeRepoId: 'shared-repo',
|
||||
activeWorkspaceExecutionHostId: toSshExecutionHostId('hub-private-target')
|
||||
},
|
||||
'shared-repo'
|
||||
)
|
||||
).toBe(ssh)
|
||||
})
|
||||
|
||||
it('keeps every non-paired-hub host mismatch failing closed', () => {
|
||||
// Why: the paired-hub exemption is ssh-selection-over-runtime-repo only; anything else is a wrong host.
|
||||
const runtime = makeRepo({
|
||||
id: 'same-repo',
|
||||
path: '/runtime/repo',
|
||||
displayName: 'runtime',
|
||||
executionHostId: toRuntimeExecutionHostId('env-1')
|
||||
})
|
||||
const ssh = makeRepo({
|
||||
id: 'ssh-repo',
|
||||
path: '/ssh/repo',
|
||||
displayName: 'ssh',
|
||||
executionHostId: toSshExecutionHostId('target-1')
|
||||
})
|
||||
|
||||
expect(
|
||||
selectRepoByIdForActiveWorkspace(
|
||||
{
|
||||
repos: [runtime],
|
||||
activeRepoId: 'same-repo',
|
||||
activeWorkspaceExecutionHostId: toRuntimeExecutionHostId('env-2')
|
||||
},
|
||||
'same-repo'
|
||||
)
|
||||
).toBeNull()
|
||||
expect(
|
||||
selectRepoByIdForActiveWorkspace(
|
||||
{
|
||||
repos: [ssh],
|
||||
activeRepoId: 'ssh-repo',
|
||||
activeWorkspaceExecutionHostId: toSshExecutionHostId('target-2')
|
||||
},
|
||||
'ssh-repo'
|
||||
)
|
||||
).toBeNull()
|
||||
})
|
||||
|
||||
it('prefers hydrated project host setup state when present', () => {
|
||||
const repos = [
|
||||
makeRepo({
|
||||
|
||||
@@ -3,7 +3,7 @@ import { useShallow } from 'zustand/react/shallow'
|
||||
import type { Repo, Worktree, TerminalTab } from '../../../shared/types'
|
||||
import type { AppState } from './types'
|
||||
import { FLOATING_TERMINAL_WORKTREE_ID } from '../../../shared/constants'
|
||||
import { getRepoExecutionHostId } from '../../../shared/execution-host'
|
||||
import { getRepoExecutionHostId, parseExecutionHostId } from '../../../shared/execution-host'
|
||||
import { getProjectHostSetupProjectionFromState } from './project-host-setup-selector'
|
||||
import {
|
||||
getIndexedAllWorktrees as getCachedAllWorktrees,
|
||||
@@ -181,16 +181,26 @@ export function selectRepoByIdForActiveWorkspace(
|
||||
if (!repoId) {
|
||||
return null
|
||||
}
|
||||
const repo = getCachedRepoMap(state.repos).get(repoId) ?? null
|
||||
if (repoId === state.activeRepoId && state.activeWorkspaceExecutionHostId) {
|
||||
return (
|
||||
state.repos.find(
|
||||
(repo) =>
|
||||
repo.id === repoId &&
|
||||
getRepoExecutionHostId(repo) === state.activeWorkspaceExecutionHostId
|
||||
) ?? null
|
||||
const hostMatch = state.repos.find(
|
||||
(candidate) =>
|
||||
candidate.id === repoId &&
|
||||
getRepoExecutionHostId(candidate) === state.activeWorkspaceExecutionHostId
|
||||
)
|
||||
if (hostMatch) {
|
||||
return hostMatch
|
||||
}
|
||||
// Why: withRepoHostOwnership keeps a paired-hub worktree on its own SSH host while the repo
|
||||
// stays hub-owned, so that one mismatch still names the right repo; every other stays closed.
|
||||
const isPairedHubRepo =
|
||||
parseExecutionHostId(state.activeWorkspaceExecutionHostId)?.kind === 'ssh' &&
|
||||
parseExecutionHostId(repo ? getRepoExecutionHostId(repo) : undefined)?.kind === 'runtime'
|
||||
if (!isPairedHubRepo) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
return getCachedRepoMap(state.repos).get(repoId) ?? null
|
||||
return repo
|
||||
}
|
||||
|
||||
export const useRepoById = (repoId: string | null) =>
|
||||
|
||||
Reference in New Issue
Block a user