mirror of
https://github.com/stablyai/orca.git
synced 2026-10-21 00:03:46 +00:00
fix(skills): preserve SSH pane transport identity
This commit is contained in:
@@ -0,0 +1,135 @@
|
||||
import type { AppState } from '../../store/types'
|
||||
import {
|
||||
parseExecutionHostId,
|
||||
toSshExecutionHostId,
|
||||
type ExecutionHostId
|
||||
} from '../../../../shared/execution-host'
|
||||
import { folderWorkspaceKey } from '../../../../shared/workspace-scope'
|
||||
|
||||
type FolderSshRouteState = Pick<
|
||||
AppState,
|
||||
'activeWorkspaceExecutionHostId' | 'activeWorktreeId' | 'folderWorkspaces' | 'projectGroups'
|
||||
>
|
||||
|
||||
export type NativeChatFolderSshRouteResolution =
|
||||
| {
|
||||
kind: 'resolved'
|
||||
hostId: ExecutionHostId
|
||||
targetId: string
|
||||
environmentId: string | null
|
||||
}
|
||||
| { kind: 'ambiguous' }
|
||||
| { kind: 'missing'; ownerHostId?: ExecutionHostId | null }
|
||||
|
||||
type FolderSshRoute = Extract<NativeChatFolderSshRouteResolution, { kind: 'resolved' }>
|
||||
|
||||
export function resolveNativeChatFolderSshRoute(
|
||||
state: FolderSshRouteState,
|
||||
folderWorkspaceId: string
|
||||
): NativeChatFolderSshRouteResolution {
|
||||
const routes = new Map<string, FolderSshRoute>()
|
||||
const nonSshOwnerHostIds: (ExecutionHostId | null)[] = []
|
||||
const activeHost =
|
||||
state.activeWorktreeId === folderWorkspaceKey(folderWorkspaceId)
|
||||
? parseExecutionHostId(state.activeWorkspaceExecutionHostId)
|
||||
: null
|
||||
const workspaces = state.folderWorkspaces.filter(
|
||||
(workspace) => workspace.id === folderWorkspaceId
|
||||
)
|
||||
for (const workspace of workspaces) {
|
||||
const workspaceHost = parseExecutionHostId(workspace.executionHostId)
|
||||
const groups = state.projectGroups.filter((group) => {
|
||||
if (group.id !== workspace.projectGroupId) {
|
||||
return false
|
||||
}
|
||||
const groupHost = parseExecutionHostId(group.executionHostId)
|
||||
return !workspaceHost || !groupHost || workspaceHost.id === groupHost.id
|
||||
})
|
||||
const owners = groups.length > 0 ? groups : [null]
|
||||
for (const group of owners) {
|
||||
const route = routeForFolderOwners(workspace, group)
|
||||
if (route.kind === 'ambiguous') {
|
||||
return route
|
||||
}
|
||||
if (route.kind === 'resolved') {
|
||||
routes.set(JSON.stringify([route.hostId, route.environmentId]), route)
|
||||
} else {
|
||||
nonSshOwnerHostIds.push(route.ownerHostId ?? null)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (routes.size === 0) {
|
||||
return activeHost?.kind === 'ssh' && nonSshOwnerHostIds.length > 0
|
||||
? { kind: 'ambiguous' }
|
||||
: { kind: 'missing' }
|
||||
}
|
||||
const candidates = [...routes.values()].filter((route) => {
|
||||
if (!activeHost) {
|
||||
return true
|
||||
}
|
||||
return activeHost.kind === 'ssh'
|
||||
? activeHost.id === route.hostId
|
||||
: activeHost.kind === 'runtime'
|
||||
? activeHost.environmentId === route.environmentId
|
||||
: false
|
||||
})
|
||||
const hasRelevantNonSshOwner = nonSshOwnerHostIds.some(
|
||||
(ownerHostId) => !activeHost || !ownerHostId || ownerHostId === activeHost.id
|
||||
)
|
||||
return candidates.length === 1 && !hasRelevantNonSshOwner ? candidates[0] : { kind: 'ambiguous' }
|
||||
}
|
||||
|
||||
function routeForFolderOwners(
|
||||
workspace: FolderSshRouteState['folderWorkspaces'][number],
|
||||
group: FolderSshRouteState['projectGroups'][number] | null
|
||||
): NativeChatFolderSshRouteResolution {
|
||||
const workspaceHost = parseExecutionHostId(workspace.executionHostId)
|
||||
const groupHost = parseExecutionHostId(group?.executionHostId)
|
||||
if (
|
||||
(workspace.executionHostId !== null &&
|
||||
workspace.executionHostId !== undefined &&
|
||||
!workspaceHost) ||
|
||||
(group?.executionHostId !== null && group?.executionHostId !== undefined && !groupHost) ||
|
||||
(workspace.connectionId !== null &&
|
||||
workspace.connectionId !== undefined &&
|
||||
!workspace.connectionId.trim()) ||
|
||||
(group?.connectionId !== null &&
|
||||
group?.connectionId !== undefined &&
|
||||
!group.connectionId.trim())
|
||||
) {
|
||||
return { kind: 'ambiguous' }
|
||||
}
|
||||
if (workspaceHost && groupHost && workspaceHost.id !== groupHost.id) {
|
||||
return { kind: 'missing', ownerHostId: workspaceHost.id }
|
||||
}
|
||||
const logicalHost = workspaceHost ?? groupHost
|
||||
const connectionIds = new Set(
|
||||
[workspace.connectionId?.trim(), group?.connectionId?.trim()].filter(
|
||||
(connectionId): connectionId is string => Boolean(connectionId)
|
||||
)
|
||||
)
|
||||
if (connectionIds.size > 1 || (logicalHost?.kind === 'local' && connectionIds.size > 0)) {
|
||||
return { kind: 'ambiguous' }
|
||||
}
|
||||
const connectionId = connectionIds.values().next().value
|
||||
let hostId = connectionId ? toSshExecutionHostId(connectionId) : null
|
||||
if (logicalHost?.kind === 'ssh') {
|
||||
if (hostId && hostId !== logicalHost.id) {
|
||||
return { kind: 'ambiguous' }
|
||||
}
|
||||
hostId = logicalHost.id
|
||||
}
|
||||
if (!hostId) {
|
||||
return { kind: 'missing', ownerHostId: logicalHost?.id ?? null }
|
||||
}
|
||||
const host = parseExecutionHostId(hostId)
|
||||
if (host?.kind !== 'ssh') {
|
||||
return { kind: 'missing', ownerHostId: logicalHost?.id ?? null }
|
||||
}
|
||||
return {
|
||||
kind: 'resolved',
|
||||
hostId: host.id,
|
||||
targetId: host.targetId,
|
||||
environmentId: logicalHost?.kind === 'runtime' ? logicalHost.environmentId : null
|
||||
}
|
||||
}
|
||||
@@ -1,20 +1,15 @@
|
||||
import type { AppState } from '../../store/types'
|
||||
import type { PaneSkillDiscoveryTarget, SkillDiscoveryTarget } from '../../../../shared/skills'
|
||||
import {
|
||||
isRuntimeOwnedSshTargetId,
|
||||
parseExecutionHostId,
|
||||
toSshExecutionHostId,
|
||||
type ExecutionHostId
|
||||
} from '../../../../shared/execution-host'
|
||||
import { getRepoIdFromWorktreeId } from '../../../../shared/worktree-id'
|
||||
import { isRuntimeOwnedSshTargetId, parseExecutionHostId } from '../../../../shared/execution-host'
|
||||
import type { RuntimeClientTarget } from '@/runtime/runtime-rpc-client'
|
||||
import { resolveExactWorktreeRoute } from '@/lib/worktree-owner-route'
|
||||
import {
|
||||
getExplicitRuntimeEnvironmentIdForWorktree,
|
||||
getExecutionHostIdForWorktree
|
||||
} from '@/lib/worktree-runtime-owner'
|
||||
import { getLocalProjectExecutionRuntimeContext } from '@/lib/local-preflight-context'
|
||||
import { parseWorkspaceKey } from '../../../../shared/workspace-scope'
|
||||
import { resolveNativeChatFolderSshRoute } from './native-chat-folder-ssh-route'
|
||||
import { resolveNativeChatWorktreeSshTransport } from './native-chat-worktree-ssh-transport'
|
||||
|
||||
export type NativeChatSkillStateInputs = Pick<
|
||||
AppState,
|
||||
@@ -78,65 +73,6 @@ export function selectNativeChatSkillStateInputs(state: AppState): NativeChatSki
|
||||
}
|
||||
}
|
||||
|
||||
type SshTransportResolution =
|
||||
| { kind: 'resolved'; environmentId: string | null }
|
||||
| { kind: 'ambiguous' }
|
||||
| { kind: 'missing' }
|
||||
|
||||
function resolveCatalogSshTransport(
|
||||
state: NativeChatSkillStateInputs,
|
||||
worktreeId: string,
|
||||
hostId: ExecutionHostId
|
||||
): SshTransportResolution {
|
||||
const scope = parseWorkspaceKey(worktreeId)
|
||||
if (scope?.type === 'folder') {
|
||||
return { kind: 'missing' }
|
||||
}
|
||||
const rawWorktreeId = scope?.type === 'worktree' ? scope.worktreeId : worktreeId
|
||||
const repoIds = new Set([getRepoIdFromWorktreeId(rawWorktreeId)])
|
||||
const environmentIds = new Set<string | null>()
|
||||
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 !== rawWorktreeId || parseExecutionHostId(worktree.hostId)?.id !== hostId) {
|
||||
continue
|
||||
}
|
||||
repoIds.add(worktree.repoId)
|
||||
const resolution = resolveExactWorktreeRoute(state, worktree)
|
||||
if (resolution.kind === 'ambiguous') {
|
||||
return resolution
|
||||
}
|
||||
if (resolution.kind === 'resolved' && resolution.route.executionHostId === hostId) {
|
||||
environmentIds.add(resolution.route.runtimeEnvironmentId)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (environmentIds.size === 0) {
|
||||
for (const repo of state.repos) {
|
||||
if (!repoIds.has(repo.id)) {
|
||||
continue
|
||||
}
|
||||
const executionHost = parseExecutionHostId(repo.executionHostId)
|
||||
const connectionId = repo.connectionId?.trim()
|
||||
const connectionHostId = connectionId ? toSshExecutionHostId(connectionId) : null
|
||||
if (connectionHostId !== hostId && executionHost?.id !== hostId) {
|
||||
continue
|
||||
}
|
||||
environmentIds.add(executionHost?.kind === 'runtime' ? executionHost.environmentId : null)
|
||||
}
|
||||
}
|
||||
if (environmentIds.size > 1) {
|
||||
return { kind: 'ambiguous' }
|
||||
}
|
||||
const environmentId = environmentIds.values().next().value
|
||||
return environmentIds.size === 1
|
||||
? { kind: 'resolved', environmentId: environmentId ?? null }
|
||||
: { kind: 'missing' }
|
||||
}
|
||||
|
||||
export function resolveNativeChatSkillDiscoveryCwd(
|
||||
state: NativeChatSkillWorktreeState,
|
||||
terminalTabId: string
|
||||
@@ -170,12 +106,29 @@ export function resolveNativeChatSkillDiscoveryContext(
|
||||
}
|
||||
const hostId = getExecutionHostIdForWorktree(state, worktreeId)
|
||||
const parsedHost = parseExecutionHostId(hostId)
|
||||
if (parsedHost?.kind === 'ssh') {
|
||||
const workspaceScope = parseWorkspaceKey(worktreeId)
|
||||
const folderSshRoute =
|
||||
workspaceScope?.type === 'folder'
|
||||
? resolveNativeChatFolderSshRoute(state, workspaceScope.folderWorkspaceId)
|
||||
: { kind: 'missing' as const }
|
||||
if (folderSshRoute.kind === 'ambiguous') {
|
||||
return null
|
||||
}
|
||||
const sshHost =
|
||||
folderSshRoute.kind === 'resolved'
|
||||
? parseExecutionHostId(folderSshRoute.hostId)
|
||||
: parsedHost?.kind === 'ssh'
|
||||
? parsedHost
|
||||
: null
|
||||
if (sshHost?.kind === 'ssh') {
|
||||
const explicitRuntimeEnvironmentId = getExplicitRuntimeEnvironmentIdForWorktree(
|
||||
state,
|
||||
worktreeId
|
||||
)
|
||||
const catalogTransport = resolveCatalogSshTransport(state, worktreeId, parsedHost.id)
|
||||
const catalogTransport =
|
||||
folderSshRoute.kind === 'resolved'
|
||||
? { kind: 'resolved' as const, environmentId: folderSshRoute.environmentId }
|
||||
: resolveNativeChatWorktreeSshTransport(state, worktreeId, sshHost.id)
|
||||
if (
|
||||
catalogTransport.kind === 'ambiguous' ||
|
||||
(explicitRuntimeEnvironmentId &&
|
||||
@@ -189,14 +142,14 @@ export function resolveNativeChatSkillDiscoveryContext(
|
||||
(catalogTransport.kind === 'resolved' ? catalogTransport.environmentId : null)
|
||||
// Why: unresolved paired-runtime transport can collide with a local target
|
||||
// id; failing closed prevents the identity-only RPC reaching the wrong host.
|
||||
if (!runtimeEnvironmentId && isRuntimeOwnedSshTargetId(parsedHost.targetId)) {
|
||||
if (!runtimeEnvironmentId && isRuntimeOwnedSshTargetId(sshHost.targetId)) {
|
||||
return null
|
||||
}
|
||||
const connectionState = runtimeEnvironmentId
|
||||
? state.sshStateByEnvironment
|
||||
.get(runtimeEnvironmentId)
|
||||
?.connectionStates.get(parsedHost.targetId)
|
||||
: state.sshConnectionStates.get(parsedHost.targetId)
|
||||
?.connectionStates.get(sshHost.targetId)
|
||||
: state.sshConnectionStates.get(sshHost.targetId)
|
||||
// Why: an unhydrated environment bucket is unknown, not disconnected — the
|
||||
// owning runtime answers authoritatively. A missing local entry is offline.
|
||||
const status = connectionState?.status ?? (runtimeEnvironmentId ? null : 'disconnected')
|
||||
@@ -204,7 +157,7 @@ export function resolveNativeChatSkillDiscoveryContext(
|
||||
key: JSON.stringify([
|
||||
'ssh',
|
||||
runtimeEnvironmentId ?? null,
|
||||
hostId,
|
||||
sshHost.id,
|
||||
connectionState?.connectionGeneration ?? 0,
|
||||
worktreeId,
|
||||
terminalTabId
|
||||
@@ -218,7 +171,6 @@ export function resolveNativeChatSkillDiscoveryContext(
|
||||
}
|
||||
}
|
||||
|
||||
const workspaceScope = parseWorkspaceKey(worktreeId)
|
||||
const cwd =
|
||||
resolveNativeChatSkillDiscoveryCwd(state, terminalTabId) ??
|
||||
(workspaceScope?.type === 'folder'
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
import type { AppState } from '../../store/types'
|
||||
import { normalizeRuntimePathForComparison } from '../../../../shared/cross-platform-path'
|
||||
import {
|
||||
parseExecutionHostId,
|
||||
toSshExecutionHostId,
|
||||
type ExecutionHostId
|
||||
} from '../../../../shared/execution-host'
|
||||
import {
|
||||
getRepoIdFromWorktreeId,
|
||||
splitWorktreeIdForFilesystem
|
||||
} from '../../../../shared/worktree-id'
|
||||
import { parseWorkspaceKey } from '../../../../shared/workspace-scope'
|
||||
import { resolveExactWorktreeRoute } from '@/lib/worktree-owner-route'
|
||||
|
||||
type WorktreeSshTransportState = Pick<
|
||||
AppState,
|
||||
'detectedWorktreesByRepo' | 'repos' | 'worktreesByRepo'
|
||||
>
|
||||
|
||||
export type NativeChatWorktreeSshTransportResolution =
|
||||
| { kind: 'resolved'; environmentId: string | null }
|
||||
| { kind: 'ambiguous' }
|
||||
| { kind: 'missing' }
|
||||
|
||||
export function resolveNativeChatWorktreeSshTransport(
|
||||
state: WorktreeSshTransportState,
|
||||
worktreeId: string,
|
||||
hostId: ExecutionHostId
|
||||
): NativeChatWorktreeSshTransportResolution {
|
||||
const scope = parseWorkspaceKey(worktreeId)
|
||||
const rawWorktreeId = scope?.type === 'worktree' ? scope.worktreeId : worktreeId
|
||||
const repoIds = new Set([getRepoIdFromWorktreeId(rawWorktreeId)])
|
||||
const environmentIds = new Set<string | null>()
|
||||
const catalogs = [
|
||||
...Object.values(state.worktreesByRepo ?? {}),
|
||||
...Object.values(state.detectedWorktreesByRepo ?? {}).map((result) => result.worktrees)
|
||||
]
|
||||
let hasDirectOwner = false
|
||||
let hasRuntimeOwner = false
|
||||
for (const worktrees of catalogs) {
|
||||
for (const worktree of worktrees) {
|
||||
if (
|
||||
!worktreeIdsEqual(worktree.id, rawWorktreeId) ||
|
||||
parseExecutionHostId(worktree.hostId)?.id !== hostId
|
||||
) {
|
||||
continue
|
||||
}
|
||||
repoIds.add(worktree.repoId)
|
||||
if (worktree.runtimeOwnerEnvironmentId?.trim()) {
|
||||
hasRuntimeOwner = true
|
||||
} else {
|
||||
hasDirectOwner = true
|
||||
}
|
||||
const resolution = resolveExactWorktreeRoute(state, worktree)
|
||||
if (resolution.kind === 'ambiguous') {
|
||||
return resolution
|
||||
}
|
||||
if (resolution.kind === 'resolved' && resolution.route.executionHostId === hostId) {
|
||||
environmentIds.add(resolution.route.runtimeEnvironmentId)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (hasDirectOwner && hasRuntimeOwner) {
|
||||
return { kind: 'ambiguous' }
|
||||
}
|
||||
if (environmentIds.size === 0) {
|
||||
for (const repo of state.repos) {
|
||||
if (!repoIds.has(repo.id)) {
|
||||
continue
|
||||
}
|
||||
const executionHost = parseExecutionHostId(repo.executionHostId)
|
||||
const connectionId = repo.connectionId?.trim()
|
||||
const connectionHostId = connectionId ? toSshExecutionHostId(connectionId) : null
|
||||
if (connectionHostId !== hostId && executionHost?.id !== hostId) {
|
||||
continue
|
||||
}
|
||||
environmentIds.add(executionHost?.kind === 'runtime' ? executionHost.environmentId : null)
|
||||
}
|
||||
}
|
||||
if (environmentIds.size > 1) {
|
||||
return { kind: 'ambiguous' }
|
||||
}
|
||||
const environmentId = environmentIds.values().next().value
|
||||
return environmentIds.size === 1
|
||||
? { kind: 'resolved', environmentId: environmentId ?? null }
|
||||
: { kind: 'missing' }
|
||||
}
|
||||
|
||||
function worktreeIdsEqual(left: string, right: string): boolean {
|
||||
if (left === right) {
|
||||
return true
|
||||
}
|
||||
const parsedLeft = splitWorktreeIdForFilesystem(left)
|
||||
const parsedRight = splitWorktreeIdForFilesystem(right)
|
||||
return Boolean(
|
||||
parsedLeft &&
|
||||
parsedRight &&
|
||||
parsedLeft.repoId === parsedRight.repoId &&
|
||||
normalizeRuntimePathForComparison(parsedLeft.worktreePath) ===
|
||||
normalizeRuntimePathForComparison(parsedRight.worktreePath)
|
||||
)
|
||||
}
|
||||
@@ -240,10 +240,60 @@ describe('useNativeChatSkills', () => {
|
||||
)
|
||||
})
|
||||
|
||||
it('routes a projected paired SSH folder through pane discovery', async () => {
|
||||
const worktreeId = 'folder:folder-1'
|
||||
mocks.state = {
|
||||
...stateForHost('runtime:hub-a'),
|
||||
activeWorktreeId: worktreeId,
|
||||
folderWorkspaces: [
|
||||
{
|
||||
id: 'folder-1',
|
||||
projectGroupId: 'group-1',
|
||||
folderPath: '/remote/folder',
|
||||
executionHostId: 'runtime:hub-a'
|
||||
}
|
||||
],
|
||||
projectGroups: [
|
||||
{
|
||||
id: 'group-1',
|
||||
connectionId: 'private-target',
|
||||
executionHostId: 'runtime:hub-a'
|
||||
}
|
||||
],
|
||||
sshStateByEnvironment: new Map([
|
||||
[
|
||||
'hub-a',
|
||||
{ connectionStates: new Map([['private-target', sshConnectionState('private-target')]]) }
|
||||
]
|
||||
]),
|
||||
tabsByWorktree: { [worktreeId]: [{ id: 'tab-1' }] },
|
||||
worktreesByRepo: {}
|
||||
}
|
||||
mocks.callRuntimeRpc.mockResolvedValue({ status: 'ok', result: DISCOVERY_RESULT })
|
||||
|
||||
render(<Probe enabled />)
|
||||
await waitFor(() => expect(mocks.snapshots.at(-1)?.status).toBe('ready'))
|
||||
|
||||
expect(mocks.callRuntimeRpc).toHaveBeenCalledWith(
|
||||
{ kind: 'environment', environmentId: 'hub-a' },
|
||||
'skills.discoverForPane',
|
||||
{ worktreeId, terminalTabId: 'tab-1' },
|
||||
{ timeoutMs: 10_000 }
|
||||
)
|
||||
})
|
||||
|
||||
it('issues no RPC for an ambiguous direct and paired SSH pane', async () => {
|
||||
mocks.state = {
|
||||
...stateForHost('ssh:shared-target'),
|
||||
activeWorkspaceExecutionHostId: 'ssh:shared-target',
|
||||
repos: [
|
||||
{
|
||||
id: 'repo-1',
|
||||
path: '/repo',
|
||||
connectionId: 'shared-target',
|
||||
executionHostId: 'runtime:hub-a'
|
||||
}
|
||||
],
|
||||
sshConnectionStates: new Map([['shared-target', sshConnectionState('shared-target')]]),
|
||||
worktreesByRepo: {
|
||||
'repo-1': [
|
||||
|
||||
@@ -513,6 +513,163 @@ describe('resolveNativeChatSkillDiscoveryContext for SSH panes', () => {
|
||||
expect(context).toBeNull()
|
||||
})
|
||||
|
||||
it('fails closed before a direct repo row hydrates beside a paired repo fallback', () => {
|
||||
const worktreeId = 'repo-1::/remote/repo'
|
||||
const context = resolveNativeChatSkillDiscoveryContext(
|
||||
selectedSshInputs({
|
||||
activeWorkspaceExecutionHostId: 'ssh:shared-target',
|
||||
activeWorktreeId: worktreeId,
|
||||
repos: [
|
||||
{
|
||||
id: 'repo-1',
|
||||
path: '/remote/repo',
|
||||
connectionId: 'shared-target',
|
||||
executionHostId: 'runtime:hub-a'
|
||||
}
|
||||
],
|
||||
tabsByWorktree: { [worktreeId]: [{ id: 'tab-1' }] },
|
||||
worktreesByRepo: {
|
||||
'repo-1': [
|
||||
{
|
||||
id: worktreeId,
|
||||
repoId: 'repo-1',
|
||||
path: '/remote/repo',
|
||||
hostId: 'ssh:shared-target'
|
||||
},
|
||||
{
|
||||
id: worktreeId,
|
||||
repoId: 'repo-1',
|
||||
path: '/remote/repo',
|
||||
hostId: 'ssh:shared-target',
|
||||
runtimeOwnerEnvironmentId: 'hub-a'
|
||||
}
|
||||
]
|
||||
}
|
||||
}),
|
||||
'tab-1'
|
||||
)
|
||||
|
||||
expect(context).toBeNull()
|
||||
})
|
||||
|
||||
it('resolves paired transport across equivalent Windows worktree ids', () => {
|
||||
const paneWorktreeId = 'repo-1::C:\\remote\\repo'
|
||||
const context = resolveNativeChatSkillDiscoveryContext(
|
||||
selectedSshInputs({
|
||||
activeWorkspaceExecutionHostId: 'ssh:private-target',
|
||||
activeWorktreeId: paneWorktreeId,
|
||||
repos: [],
|
||||
sshConnectionStates: new Map([
|
||||
['private-target', connectionState({ targetId: 'private-target' })]
|
||||
]),
|
||||
tabsByWorktree: { [paneWorktreeId]: [{ id: 'tab-1' }] },
|
||||
worktreesByRepo: {
|
||||
'repo-1': [
|
||||
{
|
||||
id: 'repo-1::c:/remote/repo',
|
||||
repoId: 'repo-1',
|
||||
path: 'c:/remote/repo',
|
||||
hostId: 'ssh:private-target',
|
||||
runtimeOwnerEnvironmentId: 'hub-a'
|
||||
}
|
||||
]
|
||||
}
|
||||
}),
|
||||
'tab-1'
|
||||
)
|
||||
|
||||
expect(context?.executionHostKind).toBe('ssh')
|
||||
if (context?.executionHostKind !== 'ssh') {
|
||||
throw new Error('expected ssh context')
|
||||
}
|
||||
expect(context.runtimeTarget).toEqual({ kind: 'environment', environmentId: 'hub-a' })
|
||||
})
|
||||
|
||||
it('uses pane discovery for an SSH folder projected through a runtime', () => {
|
||||
const worktreeId = 'folder:folder-1'
|
||||
const context = resolveNativeChatSkillDiscoveryContext(
|
||||
selectedSshInputs({
|
||||
activeWorkspaceExecutionHostId: 'runtime:hub-a',
|
||||
activeWorktreeId: worktreeId,
|
||||
folderWorkspaces: [
|
||||
{
|
||||
id: 'folder-1',
|
||||
projectGroupId: 'group-1',
|
||||
folderPath: '/remote/folder',
|
||||
executionHostId: 'runtime:hub-a'
|
||||
}
|
||||
],
|
||||
projectGroups: [
|
||||
{
|
||||
id: 'group-1',
|
||||
connectionId: 'private-target',
|
||||
executionHostId: 'runtime:hub-a'
|
||||
}
|
||||
],
|
||||
repos: [],
|
||||
sshStateByEnvironment: new Map([
|
||||
[
|
||||
'hub-a',
|
||||
{
|
||||
connectionStates: new Map([
|
||||
['private-target', connectionState({ targetId: 'private-target' })]
|
||||
])
|
||||
}
|
||||
]
|
||||
]),
|
||||
tabsByWorktree: { [worktreeId]: [{ id: 'tab-1' }] },
|
||||
worktreesByRepo: {}
|
||||
}),
|
||||
'tab-1'
|
||||
)
|
||||
|
||||
expect(context?.executionHostKind).toBe('ssh')
|
||||
if (context?.executionHostKind !== 'ssh') {
|
||||
throw new Error('expected ssh context')
|
||||
}
|
||||
expect(context.runtimeTarget).toEqual({ kind: 'environment', environmentId: 'hub-a' })
|
||||
expect(context.paneTarget).toEqual({ worktreeId, terminalTabId: 'tab-1' })
|
||||
})
|
||||
|
||||
it('isolates a paired SSH folder from a duplicate local folder id', () => {
|
||||
const worktreeId = 'folder:folder-1'
|
||||
const context = resolveNativeChatSkillDiscoveryContext(
|
||||
selectedSshInputs({
|
||||
activeWorkspaceExecutionHostId: 'runtime:hub-a',
|
||||
activeWorktreeId: worktreeId,
|
||||
folderWorkspaces: [
|
||||
{
|
||||
id: 'folder-1',
|
||||
projectGroupId: 'local-group',
|
||||
folderPath: '/local/folder',
|
||||
executionHostId: 'local'
|
||||
},
|
||||
{
|
||||
id: 'folder-1',
|
||||
projectGroupId: 'remote-group',
|
||||
folderPath: '/remote/folder',
|
||||
executionHostId: 'runtime:hub-a'
|
||||
}
|
||||
],
|
||||
projectGroups: [
|
||||
{ id: 'local-group', executionHostId: 'local' },
|
||||
{
|
||||
id: 'remote-group',
|
||||
connectionId: 'private-target',
|
||||
executionHostId: 'runtime:hub-a'
|
||||
}
|
||||
],
|
||||
repos: [],
|
||||
tabsByWorktree: { [worktreeId]: [{ id: 'tab-1' }] },
|
||||
worktreesByRepo: {}
|
||||
}),
|
||||
'tab-1'
|
||||
)
|
||||
|
||||
expect(context?.executionHostKind).toBe('ssh')
|
||||
expect(context?.runtimeTarget).toEqual({ kind: 'environment', environmentId: 'hub-a' })
|
||||
})
|
||||
|
||||
it('keeps the same remote path on two SSH hosts cache-isolated', () => {
|
||||
const onTargetTwo = resolveNativeChatSkillDiscoveryContext(
|
||||
sshInputs({
|
||||
|
||||
Reference in New Issue
Block a user