mirror of
https://github.com/stablyai/orca.git
synced 2026-09-29 16:02:50 +00:00
`repoIsRemote` read `repo.connectionId` directly. That is one of four spellings of host ownership, so the predicate was wrong in both directions: a row carrying only `executionHostId: 'ssh:<target>'` read as local and got the Linux-only `orca-ide` rename it cannot resolve through the relay shim, while a row that declares itself `local` with a stale `connectionId` read as remote and lost the rename it needs on a Linux desktop. The predicate now resolves the host first and asks "does an SSH target hold this row's files" via `getRepoSshConnectionId`. That keeps a `runtime:` host's nested SSH target remote (that machine reaches the files through its own relay shim) while a runtime with no nested target - a full Orca install - stays local, as do WSL and local. Its call sites did not all want that question: - The four launch-scope sites in main already hold the resolved PTY route on `TerminalWorkspaceLaunchScope.connectionId`. `scope.repo` is documented display metadata and can be a row from a different host than the worktree names, so they now read the route they will actually spawn on. A launch shape that disagrees with its own route is the bug, not a second predicate. - `launchAgentInNewTab` picked its repo row with a host-blind `store.repos.find`, so a worktree that names its own host could be shaped by another host's row. It now resolves through `getConnectionIdFromState`, the same rule the file already used for transcript readability. - `resolveAgentBackgroundLaunchHost` derived the route, the trust write and the launch shape from three reads of the raw field; one resolution now feeds all three. Also converts the raw `repo.connectionId` agent-detection probe eight lines above `buildWorktreeStartupForDraft`'s launch shape, which #17919 deferred precisely because converting it alone would have left that file internally inconsistent. Tests cover two distinct SSH hosts (a single-host fixture passes even when the answer comes off the wrong row, which is how the `ssh:m4air` -> openclaw leak survived review) and a `runtime:` host carrying a nested SSH target.
144 lines
4.7 KiB
TypeScript
144 lines
4.7 KiB
TypeScript
import { describe, expect, it, vi } from 'vitest'
|
|
import type { Repo } from '../../shared/repo-types'
|
|
|
|
const mocks = vi.hoisted(() => ({
|
|
markCodexProjectTrusted: vi.fn(),
|
|
markCopilotFolderTrusted: vi.fn(),
|
|
markCursorWorkspaceTrusted: vi.fn(),
|
|
detectRemoteAgents: vi.fn(),
|
|
detectInstalledAgentsWithShellPathHydration: vi.fn()
|
|
}))
|
|
|
|
vi.mock('../agent-trust-presets', () => ({
|
|
markCodexProjectTrusted: mocks.markCodexProjectTrusted,
|
|
markCopilotFolderTrusted: mocks.markCopilotFolderTrusted,
|
|
markCursorWorkspaceTrusted: mocks.markCursorWorkspaceTrusted
|
|
}))
|
|
|
|
vi.mock('../preflight/agent-detection', () => ({
|
|
detectRemoteAgents: mocks.detectRemoteAgents,
|
|
detectInstalledAgentsWithShellPathHydration: mocks.detectInstalledAgentsWithShellPathHydration
|
|
}))
|
|
|
|
import {
|
|
buildWorktreeStartupForAgent,
|
|
buildWorktreeStartupForDraft,
|
|
markLocalWorktreeTrusted
|
|
} from './runtime-worktree-agent-startup'
|
|
|
|
function makeRepo(fields: Partial<Repo>): Repo {
|
|
return {
|
|
id: 'repo-1',
|
|
name: 'repo',
|
|
path: '/srv/repo',
|
|
connectionId: null,
|
|
executionHostId: null,
|
|
...fields
|
|
} as Repo
|
|
}
|
|
|
|
const settings = {
|
|
agentCmdOverrides: {},
|
|
agentDefaultArgs: {},
|
|
agentDefaultEnv: {},
|
|
disabledTuiAgents: [],
|
|
defaultTuiAgent: undefined,
|
|
terminalWindowsShell: null
|
|
} as never
|
|
|
|
/** The launched CLI name is the whole decision: `orca` is the relay shim, `orca-ide` is local. */
|
|
function launchCliNameFor(repo: Repo): string {
|
|
return buildWorktreeStartupForAgent({
|
|
repo,
|
|
settings,
|
|
agent: 'claude-agent-teams',
|
|
getLaunchPlatform: () => 'linux',
|
|
toSessionOptions: () => undefined
|
|
}).startup.command.split(' ')[0]!
|
|
}
|
|
|
|
describe('buildWorktreeStartupForAgent host resolution', () => {
|
|
// Why two hosts: one SSH fixture passes even when the launch shape is resolved off another
|
|
// host's row, which is the shape of the `ssh:m4air` -> openclaw leak.
|
|
it('drops the Linux-only rename for both spellings of SSH ownership on two hosts', () => {
|
|
expect(launchCliNameFor(makeRepo({ connectionId: 'm4air' }))).toBe('orca')
|
|
expect(launchCliNameFor(makeRepo({ executionHostId: 'ssh:openclaw' }))).toBe('orca')
|
|
})
|
|
|
|
it('keeps the Linux rename for a local row carrying a stale connection', () => {
|
|
expect(launchCliNameFor(makeRepo({ connectionId: 'm4air', executionHostId: 'local' }))).toBe(
|
|
'orca-ide'
|
|
)
|
|
})
|
|
|
|
it('drops the rename for a runtime host reaching a nested SSH target', () => {
|
|
expect(
|
|
launchCliNameFor(makeRepo({ connectionId: 'nested', executionHostId: 'runtime:vm-1' }))
|
|
).toBe('orca')
|
|
})
|
|
|
|
it('keeps the rename for a runtime host with no nested SSH target', () => {
|
|
expect(launchCliNameFor(makeRepo({ executionHostId: 'runtime:vm-1' }))).toBe('orca-ide')
|
|
})
|
|
})
|
|
|
|
describe('buildWorktreeStartupForDraft agent detection', () => {
|
|
it('probes the SSH host named only by executionHostId instead of this client', async () => {
|
|
mocks.detectRemoteAgents.mockResolvedValueOnce(['claude'])
|
|
mocks.detectInstalledAgentsWithShellPathHydration.mockResolvedValue([])
|
|
|
|
const result = await buildWorktreeStartupForDraft({
|
|
repo: makeRepo({ executionHostId: 'ssh:openclaw' }),
|
|
settings,
|
|
draft: 'ship it',
|
|
getLaunchPlatform: () => 'linux'
|
|
})
|
|
|
|
expect(mocks.detectRemoteAgents).toHaveBeenCalledWith({ connectionId: 'openclaw' })
|
|
expect(mocks.detectInstalledAgentsWithShellPathHydration).not.toHaveBeenCalled()
|
|
expect(result?.agent).toBe('claude')
|
|
})
|
|
|
|
it('probes this client for a local row carrying a stale connection', async () => {
|
|
mocks.detectRemoteAgents.mockClear()
|
|
mocks.detectInstalledAgentsWithShellPathHydration.mockResolvedValueOnce(['claude'])
|
|
|
|
const result = await buildWorktreeStartupForDraft({
|
|
repo: makeRepo({ connectionId: 'm4air', executionHostId: 'local' }),
|
|
settings,
|
|
draft: 'ship it',
|
|
getLaunchPlatform: () => 'linux'
|
|
})
|
|
|
|
expect(mocks.detectRemoteAgents).not.toHaveBeenCalled()
|
|
expect(result?.agent).toBe('claude')
|
|
})
|
|
})
|
|
|
|
describe('markLocalWorktreeTrusted', () => {
|
|
it('waits for the Codex trust write before resolving', async () => {
|
|
let finish!: () => void
|
|
mocks.markCodexProjectTrusted.mockReturnValue(
|
|
new Promise<void>((resolve) => {
|
|
finish = resolve
|
|
})
|
|
)
|
|
let settled = false
|
|
const marking = markLocalWorktreeTrusted('codex', '/workspace/app').then(() => {
|
|
settled = true
|
|
})
|
|
|
|
await Promise.resolve()
|
|
expect(settled).toBe(false)
|
|
finish()
|
|
await marking
|
|
expect(mocks.markCodexProjectTrusted).toHaveBeenCalledWith('/workspace/app')
|
|
})
|
|
|
|
it('contains a rejected Codex trust write', async () => {
|
|
mocks.markCodexProjectTrusted.mockRejectedValueOnce(new Error('write failed'))
|
|
|
|
await expect(markLocalWorktreeTrusted('codex', '/workspace/app')).resolves.toBeUndefined()
|
|
})
|
|
})
|