refactor(runtime): runtime git and file callers drop SSH arms (#27501)

Co-authored-by: m4air <m4air@Mac.localdomain>
This commit is contained in:
OrcaWin
2026-10-11 08:23:12 -07:00
committed by GitHub
co-authored by m4air
parent 6ffbdcd875
commit 215ee3e625
74 changed files with 705 additions and 4668 deletions
+2 -29
View File
@@ -1,22 +1,11 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
const {
gitExecFileAsyncMock,
getSshGitProviderMock,
getSshGitProviderGenerationMock,
readLocalGitConfigSignatureMock
} = vi.hoisted(() => ({
const { gitExecFileAsyncMock, readLocalGitConfigSignatureMock } = vi.hoisted(() => ({
gitExecFileAsyncMock: vi.fn(),
getSshGitProviderMock: vi.fn(),
getSshGitProviderGenerationMock: vi.fn(() => 0),
readLocalGitConfigSignatureMock: vi.fn<() => Promise<string | undefined>>(async () => 'sig-1')
}))
vi.mock('./runner', () => ({ gitExecFileAsync: gitExecFileAsyncMock }))
vi.mock('../providers/ssh-git-dispatch', () => ({
getSshGitProvider: getSshGitProviderMock,
getSshGitProviderGeneration: getSshGitProviderGenerationMock
}))
vi.mock('../github/local-git-config-signature', () => ({
readLocalGitConfigSignature: readLocalGitConfigSignatureMock
}))
@@ -38,9 +27,6 @@ describe('cached git remote name listing', () => {
beforeEach(() => {
_resetRemoteNameListingCache()
gitExecFileAsyncMock.mockReset()
getSshGitProviderMock.mockReset()
getSshGitProviderGenerationMock.mockReset()
getSshGitProviderGenerationMock.mockReturnValue(0)
readLocalGitConfigSignatureMock.mockReset()
readLocalGitConfigSignatureMock.mockImplementation(async () => 'sig-1')
})
@@ -180,20 +166,7 @@ describe('cached git remote name listing', () => {
})
})
it('lists remotes through the SSH git provider', async () => {
const exec = vi.fn(async () => ({ stdout: 'origin\n', stderr: '' }))
getSshGitProviderMock.mockReturnValue({ exec })
await expect(shouldProbeGitRemote('/remote/repo', 'upstream', 'ssh-1')).resolves.toBe(false)
expect(gitExecFileAsyncMock).not.toHaveBeenCalled()
expect(exec).toHaveBeenCalledWith(['remote'], '/remote/repo', {
signal: expect.any(AbortSignal)
})
})
it('fails open when the SSH git provider is missing instead of listing locally', async () => {
getSshGitProviderMock.mockReturnValue(undefined)
it('fails open for an SSH repo instead of listing locally', async () => {
await expect(shouldProbeGitRemote('/remote/repo', 'upstream', 'ssh-1')).resolves.toBe(true)
expect(gitExecFileAsyncMock).not.toHaveBeenCalled()
})
+12 -29
View File
@@ -1,5 +1,4 @@
import { readLocalGitConfigSignature } from '../github/local-git-config-signature'
import { getSshGitProvider, getSshGitProviderGeneration } from '../providers/ssh-git-dispatch'
import { runCoalescedProbe, type CoalescedProbes } from './coalesced-probe'
import type { GitAdmissionTier } from './command-runner/git-exec-options'
import { REMOTE_URL_PROBE_TIMEOUT_MS } from './remote-url-probe'
@@ -38,13 +37,9 @@ function parseRemoteNames(stdout: string): string[] {
function remoteNameListingCacheKey(
repoPath: string,
connectionId?: string | null,
localGitOptions: RemoteNameListingGitOptions = {}
): string {
const runtimeKey = connectionId
? `ssh:${connectionId}:${getSshGitProviderGeneration(connectionId)}`
: `local:${localGitOptions.wslDistro ?? 'host'}`
return `${runtimeKey}\0${repoPath}`
return `local:${localGitOptions.wslDistro ?? 'host'}\0${repoPath}`
}
function pruneRemoteNameListingCache(now: number): void {
@@ -64,12 +59,11 @@ function pruneRemoteNameListingCache(now: number): void {
function listingGitConfigContext(
repoPath: string,
connectionId?: string | null,
localGitOptions: RemoteNameListingGitOptions = {}
): { repoPath: string; connectionId: string | null; wslDistro?: string } {
): { repoPath: string; connectionId: null; wslDistro?: string } {
return {
repoPath,
connectionId: connectionId ?? null,
connectionId: null,
...(localGitOptions.wslDistro ? { wslDistro: localGitOptions.wslDistro } : {})
}
}
@@ -77,21 +71,25 @@ function listingGitConfigContext(
/**
* `git remote` names for one repo/runtime. Failed listings are not cached: a
* missed `upstream` would otherwise send issue/PR resolvers to origin on a
* contributor clone (#7331).
* contributor clone (#7331). An `ssh:` repo is not served here, so it lists as
* failed (`null`), never as this machine's remotes.
*/
export async function listCachedRemoteNames(
repoPath: string,
connectionId?: string | null,
localGitOptions: RemoteNameListingGitOptions = {}
): Promise<string[] | null> {
const cacheKey = remoteNameListingCacheKey(repoPath, connectionId, localGitOptions)
if (connectionId) {
return null
}
const cacheKey = remoteNameListingCacheKey(repoPath, localGitOptions)
const now = Date.now()
pruneRemoteNameListingCache(now)
const cached = remoteNameListingCache.get(cacheKey)
if (cached && cached.expiresAt > now) {
if (cached.configSignature !== undefined) {
const currentSignature = await readLocalGitConfigSignature(
listingGitConfigContext(repoPath, connectionId, localGitOptions)
listingGitConfigContext(repoPath, localGitOptions)
)
if (currentSignature === cached.configSignature) {
return cached.remotes
@@ -103,9 +101,9 @@ export async function listCachedRemoteNames(
}
return runCoalescedProbe(remoteNameListingInFlight, cacheKey, async (ownsKey) => {
const configContext = listingGitConfigContext(repoPath, connectionId, localGitOptions)
const configContext = listingGitConfigContext(repoPath, localGitOptions)
const configSignatureBefore = await readLocalGitConfigSignature(configContext)
const remotes = await listUncachedRemoteNames(repoPath, connectionId, localGitOptions)
const remotes = await listUncachedRemoteNames(repoPath, localGitOptions)
if (remotes === null) {
return null
}
@@ -132,23 +130,8 @@ export async function listCachedRemoteNames(
async function listUncachedRemoteNames(
repoPath: string,
connectionId?: string | null,
localGitOptions: RemoteNameListingGitOptions = {}
): Promise<string[] | null> {
if (connectionId) {
const provider = getSshGitProvider(connectionId)
if (!provider) {
return null
}
try {
const { stdout } = await provider.exec(['remote'], repoPath, {
signal: AbortSignal.timeout(REMOTE_URL_PROBE_TIMEOUT_MS)
})
return parseRemoteNames(stdout)
} catch {
return null
}
}
try {
const { stdout } = await gitExecFileAsync(['remote'], {
cwd: repoPath,
@@ -142,15 +142,16 @@ function makeRepo(fields: Pick<Repo, 'connectionId' | 'executionHostId'>): Repo
)
}
const localCalls = [
mocks.hasHooks,
mocks.localRead,
mocks.localWrite,
mocks.localFileRead,
mocks.localStat,
mocks.effectiveHooks
]
function expectRoute(target: string | null): void {
const localCalls = [
mocks.hasHooks,
mocks.localRead,
mocks.localWrite,
mocks.localFileRead,
mocks.localStat,
mocks.effectiveHooks
]
if (target) {
expect(mocks.provider).toHaveBeenCalledWith(target)
for (const [connectionId] of mocks.provider.mock.calls) {
@@ -209,13 +210,15 @@ describe.each(owners)('runtime hooks: $label', ({ fields, target }) => {
} else {
await issue.write(repo.id, 'save')
}
expectRoute(target)
if (method === 'write' && target) {
expect(mocks.ignored).toHaveBeenCalledWith(repo.path, target)
expect(mocks.remoteWrite).toHaveBeenCalledWith(
'/remote/fixture/.orca/issue-command',
'save\n'
)
if (target) {
// The runtime serves no SSH filesystem: an SSH owner is neither read remotely nor locally.
expect(mocks.provider).not.toHaveBeenCalled()
expect(mocks.remoteWrite).not.toHaveBeenCalled()
for (const method of localCalls) {
expect(method).not.toHaveBeenCalled()
}
} else {
expectRoute(target)
}
}
)
@@ -43,17 +43,8 @@ function localWorkspaceScan(): WorkspacePortScanResult {
}
}
function deps(
request?: (method: string) => Promise<unknown>,
connected = true
): WorkspacePortExecutionHostDeps & { request: ReturnType<typeof vi.fn> } {
const requestMock = vi.fn(request ?? (async () => ({ ports: [], platform: 'linux' })))
return {
request: requestMock,
getLocalProbes: async () => [LOCAL_PROBE],
getSshMultiplexer: (targetId) =>
connected && targetId === 'box' ? { request: requestMock } : undefined
}
function deps(): WorkspacePortExecutionHostDeps {
return { getLocalProbes: async () => [LOCAL_PROBE] }
}
describe('scanWorkspacePortsOnExecutionHost', () => {
@@ -69,64 +60,16 @@ describe('scanWorkspacePortsOnExecutionHost', () => {
const result = await scanWorkspacePortsOnExecutionHost('local', d)
expect(scanWorkspacePortsMock).toHaveBeenCalledWith([LOCAL_PROBE], undefined, undefined)
expect(d.request).not.toHaveBeenCalled()
expect(result).toMatchObject({ executionHostId: 'local', platform: 'linux' })
expect(result.ports).toHaveLength(1)
})
it("scans the workspace's SSH host through its relay, never this host", async () => {
const d = deps(async () => ({
platform: 'linux',
ports: [{ port: PORT, host: '0.0.0.0', pid: PID, processName: 'node' }]
}))
const result = await scanWorkspacePortsOnExecutionHost('ssh:box', d)
it('reports an SSH host as unavailable, not as an empty host, and never scans this host', async () => {
const result = await scanWorkspacePortsOnExecutionHost('ssh:box', deps())
expect(scanWorkspacePortsMock).not.toHaveBeenCalled()
expect(d.request).toHaveBeenCalledWith('ports.detect', undefined, expect.any(Object))
expect(result).toMatchObject({
executionHostId: 'ssh:box',
platform: 'linux',
ports: [
{
kind: 'external',
bindHost: '0.0.0.0',
connectHost: 'localhost',
port: PORT,
pid: PID,
processName: 'node'
}
]
})
expect(result.unavailableReason).toBeUndefined()
})
it('reports a disconnected SSH host as unavailable, not as an empty host', async () => {
const result = await scanWorkspacePortsOnExecutionHost('ssh:box', deps(undefined, false))
expect(result).toMatchObject({ executionHostId: 'ssh:box', ports: [] })
expect(result.unavailableReason).toMatch(/not connected/)
})
it('reports a failed or unsupported relay scan as unavailable', async () => {
const failed = await scanWorkspacePortsOnExecutionHost(
'ssh:box',
deps(async () => {
throw new Error('Request "ports.detect" timed out after 10000ms')
})
)
const unsupported = await scanWorkspacePortsOnExecutionHost(
'ssh:box',
deps(async () => ({ ports: [], platform: 'darwin' }))
)
const malformed = await scanWorkspacePortsOnExecutionHost(
'ssh:box',
deps(async () => ({ ports: [{ port: 'x' }], platform: 'linux' }))
)
expect(failed.unavailableReason).toMatch(/timed out/)
expect(unsupported.unavailableReason).toMatch(/not supported/)
expect(malformed.unavailableReason).toMatch(/invalid/)
expect(result.unavailableReason).toMatch(/Reconnect/)
})
it('refuses a workspace hosted by another server instead of scanning here', async () => {
@@ -134,7 +77,6 @@ describe('scanWorkspacePortsOnExecutionHost', () => {
const result = await scanWorkspacePortsOnExecutionHost('runtime:env-2', d)
expect(scanWorkspacePortsMock).not.toHaveBeenCalled()
expect(d.request).not.toHaveBeenCalled()
expect(result.unavailableReason).toBeTruthy()
})
})
@@ -192,6 +134,5 @@ describe('killWorkspacePortOnExecutionHost', () => {
reason: 'Stopping processes on an SSH host is not supported yet.'
})
expect(kill).not.toHaveBeenCalled()
expect(d.request).not.toHaveBeenCalled()
})
})
@@ -3,34 +3,18 @@ import {
parseExecutionHostId,
type ExecutionHostId
} from '../../shared/execution-host'
import type { DetectedPort } from '../../shared/ssh-types'
import type {
WorkspacePort,
WorkspacePortHostKillRequest,
WorkspacePortHostScanResult,
WorkspacePortKillResult,
WorkspacePortProbe
} from '../../shared/workspace-ports'
import type { SshMultiplexerRequestOptions } from '../ssh/ssh-channel-multiplexer'
import { connectHostForBindHost } from './local-workspace-port-address'
import { SSH_HOST_NOT_SERVED_MESSAGE } from '../providers/ssh-host-not-served-error'
import { killWorkspacePort, scanWorkspacePortProbes } from './workspace-port-ownership'
// Why: the relay walks /proc/*/fd on demand; a slow host must answer "unavailable", not hang the RPC.
const SSH_PORT_SCAN_TIMEOUT_MS = 10_000
type SshPortScanMultiplexer = {
request: (
method: string,
params?: Record<string, unknown>,
options?: SshMultiplexerRequestOptions
) => Promise<unknown>
}
export type WorkspacePortExecutionHostDeps = {
/** Workspaces this host runs itself; the only processes Stop may signal here. */
getLocalProbes: () => Promise<WorkspacePortProbe[]>
/** Undefined means "not connected", never "the host's processes exited". */
getSshMultiplexer: (targetId: string) => SshPortScanMultiplexer | undefined
}
function unavailable(executionHostId: string, reason: string): WorkspacePortHostScanResult {
@@ -43,73 +27,6 @@ function unavailable(executionHostId: string, reason: string): WorkspacePortHost
}
}
function errorMessage(error: unknown): string {
return error instanceof Error ? error.message : String(error)
}
function isDetectedPort(value: unknown): value is DetectedPort {
if (!value || typeof value !== 'object') {
return false
}
const port: { port?: unknown; host?: unknown; pid?: unknown; processName?: unknown } = value
return (
typeof port.port === 'number' &&
Number.isSafeInteger(port.port) &&
typeof port.host === 'string' &&
(port.pid === undefined || typeof port.pid === 'number') &&
(port.processName === undefined || typeof port.processName === 'string')
)
}
// Why `external`: the relay reports listeners without workspace attribution, so no row it returns
// can be treated as workspace-owned (which is what makes a row stoppable).
function toUnattributedPort(port: DetectedPort): WorkspacePort {
return {
id: `${port.host}:${port.port}:${port.pid ?? 'unknown'}`,
kind: 'external',
bindHost: port.host,
connectHost: connectHostForBindHost(port.host),
port: port.port,
...(port.pid !== undefined ? { pid: port.pid } : {}),
...(port.processName !== undefined ? { processName: port.processName } : {}),
protocol: 'unknown'
}
}
async function scanSshExecutionHost(
executionHostId: string,
targetId: string,
deps: WorkspacePortExecutionHostDeps
): Promise<WorkspacePortHostScanResult> {
let response: unknown
try {
const mux = deps.getSshMultiplexer(targetId)
if (!mux) {
return unavailable(executionHostId, 'The SSH host is not connected.')
}
response = await mux.request('ports.detect', undefined, {
timeoutMs: SSH_PORT_SCAN_TIMEOUT_MS
})
} catch (error) {
return unavailable(executionHostId, `Could not scan the SSH host: ${errorMessage(error)}`)
}
const result: { ports?: unknown; platform?: unknown } =
response && typeof response === 'object' ? response : {}
if (!Array.isArray(result.ports) || !result.ports.every(isDetectedPort)) {
return unavailable(executionHostId, 'The SSH host returned an invalid port scan.')
}
// Why: the relay answers an empty list on platforms it cannot scan; that is "could not look".
if (result.platform !== 'linux' && result.platform !== 'win32') {
return unavailable(executionHostId, 'Port detection is not supported on this SSH host.')
}
return {
executionHostId,
platform: result.platform,
scannedAt: Date.now(),
ports: result.ports.map(toUnattributedPort)
}
}
/** Scans the execution host a workspace resolved to on this endpoint, and names it on the result. */
export async function scanWorkspacePortsOnExecutionHost(
executionHostId: ExecutionHostId,
@@ -119,8 +36,12 @@ export async function scanWorkspacePortsOnExecutionHost(
if (host?.kind === 'local') {
return { ...(await scanWorkspacePortProbes(await deps.getLocalProbes())), executionHostId }
}
// Why: this process serves no SSH host, so its listeners are unavailable, never "none".
if (host?.kind === 'ssh') {
return scanSshExecutionHost(executionHostId, host.targetId, deps)
return unavailable(
executionHostId,
`Could not scan the SSH host: ${SSH_HOST_NOT_SERVED_MESSAGE}`
)
}
return unavailable(executionHostId, 'This workspace does not run on this server.')
}
@@ -0,0 +1,90 @@
import { describe, expect, it } from 'vitest'
import type { GitWorktreeInfo } from '../shared/worktree/types'
import { verifyProvisionedRootCheckout } from './provisioned-root-checkout'
const projectRoot = '/workspace/orca'
function gitWorktree(path: string, overrides: Partial<GitWorktreeInfo> = {}): GitWorktreeInfo {
return {
path,
head: 'abc123',
branch: 'refs/heads/fix-sandbox',
isBare: false,
isMainWorktree: true,
...overrides
}
}
function verify(
worktree: GitWorktreeInfo,
request: Partial<Parameters<typeof verifyProvisionedRootCheckout>[0]['request']> = {},
sparseCheckoutEnabled = false
): GitWorktreeInfo {
return verifyProvisionedRootCheckout({
worktrees: [worktree],
sparseCheckoutEnabled,
projectRoot,
request: { name: 'fix-sandbox', ...request }
})
}
describe('verifyProvisionedRootCheckout', () => {
it('accepts the primary checkout on the requested branch', () => {
expect(verify(gitWorktree(projectRoot)).path).toBe(projectRoot)
})
it('rejects a checkout on a branch Orca did not request', () => {
expect(() => verify(gitWorktree(projectRoot, { branch: 'refs/heads/wrong' }))).toThrow(
'requested branch'
)
})
it('rejects a checkout that did not start from the requested ref', () => {
expect(() =>
verify(gitWorktree(projectRoot, { head: 'wrong-head' }), {
baseBranch: 'v1.2.3',
expectedRefHead: 'expected-head'
})
).toThrow('requested ref')
})
it('accepts a requested branch created from a tag or commit ref', () => {
expect(
verify(gitWorktree(projectRoot), { baseBranch: 'v1.2.3', expectedRefHead: 'abc123' }).branch
).toBe('refs/heads/fix-sandbox')
})
it('rejects a requested ref without its source-host commit identity', () => {
expect(() => verify(gitWorktree(projectRoot), { baseBranch: 'missing-ref' })).toThrow(
'ref identity is missing'
)
})
it('uses an explicit branch override as the requested local branch', () => {
expect(
verify(gitWorktree(projectRoot, { branch: 'refs/heads/feature/review' }), {
branchNameOverride: 'feature/review'
}).branch
).toBe('refs/heads/feature/review')
})
it('rejects a linked worktree and a sparse checkout', () => {
expect(() => verify(gitWorktree(projectRoot, { isMainWorktree: false }))).toThrow(
'must be the repository primary checkout'
)
expect(() => verify(gitWorktree(projectRoot), {}, true)).toThrow(
'cannot adopt a sparse checkout'
)
})
it('compares Windows checkout roots using runtime path semantics', () => {
expect(
verifyProvisionedRootCheckout({
worktrees: [gitWorktree('c:/workspace/orca/')],
sparseCheckoutEnabled: false,
projectRoot: 'C:\\Workspace\\Orca',
request: { name: 'fix-sandbox' }
}).path
).toBe('c:/workspace/orca/')
})
})
+4 -283
View File
@@ -6,16 +6,10 @@ import type { Store } from './persistence'
import type { Repo } from '../shared/repo-types'
import type { AdoptProvisionedRootArgs } from '../shared/worktree/create-types'
import type { WorktreeMeta } from '../shared/worktree/meta-types'
import type { GitWorktreeInfo } from '../shared/worktree/types'
import {
listEphemeralVmRuntimes,
upsertEphemeralVmRuntime
} from '../shared/ephemeral-vm-runtime-store'
import { registerSshGitProvider, unregisterSshGitProvider } from './providers/ssh-git-dispatch'
import {
resetSshProviderAuthorities,
rotateSshProviderAuthority
} from './ssh/ssh-provider-authority'
import { adoptProvisionedRootSshCheckout } from './provisioned-root-ssh-adoption'
const connectionId = 'runtime-ssh-test'
@@ -26,88 +20,14 @@ describe('adoptProvisionedRootSshCheckout', () => {
beforeEach(() => {
userDataPath = mkdtempSync(join(tmpdir(), 'orca-provisioned-root-'))
resetSshProviderAuthorities()
})
afterEach(() => {
unregisterSshGitProvider(connectionId)
resetSshProviderAuthorities()
rmSync(userDataPath, { recursive: true, force: true })
})
it('adopts the exact primary checkout, persists host metadata, and attaches the runtime', async () => {
it('refuses a valid request because this process serves no SSH git, without writing state', async () => {
seedRuntime(userDataPath, projectRoot)
registerSshGitProvider(connectionId, {
listWorktrees: vi.fn().mockResolvedValue([gitWorktree(projectRoot)]),
exec: sparseCheckoutProbe(false)
} as never)
const { store, setWorktreeMeta } = makeStore()
const result = await adoptProvisionedRootSshCheckout({
userDataPath,
request: request(projectRoot),
repo: repo(projectRoot),
store,
isRepoCurrent: () => true
})
expect(result.worktree).toMatchObject({
id: `repo-1::${projectRoot}`,
path: projectRoot,
isMainWorktree: true,
hostId: `ssh:${connectionId}`,
ephemeralVmCheckoutMode: 'provisioned-root',
linkedGitLabIssue: 17
})
expect(setWorktreeMeta).toHaveBeenCalledWith(
`repo-1::${projectRoot}`,
expect.objectContaining({
hostId: `ssh:${connectionId}`,
ephemeralVmCheckoutMode: 'provisioned-root',
linkedGitLabIssue: 17
})
)
expect(listEphemeralVmRuntimes(userDataPath)[0]).toMatchObject({
workspaceId: `repo-1::${projectRoot}`,
status: 'running'
})
})
it('pins an explicit label even when it equals the adopted branch', async () => {
seedRuntime(userDataPath, projectRoot)
registerSshGitProvider(connectionId, {
listWorktrees: vi.fn().mockResolvedValue([gitWorktree(projectRoot)]),
exec: sparseCheckoutProbe(false)
} as never)
const { store, setWorktreeMeta } = makeStore()
const result = await adoptProvisionedRootSshCheckout({
userDataPath,
request: {
...request(projectRoot),
displayName: 'fix-sandbox',
displayNameKind: 'user'
},
repo: repo(projectRoot),
store,
isRepoCurrent: () => true
})
expect(result.worktree.displayName).toBe('fix-sandbox')
expect(setWorktreeMeta).toHaveBeenCalledWith(
`repo-1::${projectRoot}`,
expect.objectContaining({ displayName: 'fix-sandbox', displayNameIsPinned: true })
)
})
it('rejects a recipe checkout on a branch Orca did not request', async () => {
seedRuntime(userDataPath, projectRoot)
registerSshGitProvider(connectionId, {
listWorktrees: vi
.fn()
.mockResolvedValue([gitWorktree(projectRoot, { branch: 'refs/heads/wrong-branch' })]),
exec: sparseCheckoutProbe(false)
} as never)
const { store, setWorktreeMeta } = makeStore()
await expect(
@@ -118,121 +38,15 @@ describe('adoptProvisionedRootSshCheckout', () => {
store,
isRepoCurrent: () => true
})
).rejects.toThrow('requested branch')
).rejects.toThrow('no longer available')
expect(setWorktreeMeta).not.toHaveBeenCalled()
expect(listEphemeralVmRuntimes(userDataPath)[0]?.workspaceId).toBeUndefined()
})
it('rejects a recipe checkout that did not start from the requested ref', async () => {
it('rejects sparse checkout requests', async () => {
seedRuntime(userDataPath, projectRoot)
const exec = sparseCheckoutProbe(false)
registerSshGitProvider(connectionId, {
listWorktrees: vi.fn().mockResolvedValue([gitWorktree(projectRoot, { head: 'wrong-head' })]),
exec
} as never)
const { store, setWorktreeMeta } = makeStore()
await expect(
adoptProvisionedRootSshCheckout({
userDataPath,
request: {
...request(projectRoot),
baseBranch: 'v1.2.3',
expectedRefHead: 'expected-head'
},
repo: repo(projectRoot),
store,
isRepoCurrent: () => true
})
).rejects.toThrow('requested ref')
expect(exec).toHaveBeenCalledOnce()
expect(setWorktreeMeta).not.toHaveBeenCalled()
})
it('accepts an attached requested branch created from a tag or commit ref', async () => {
seedRuntime(userDataPath, projectRoot)
const exec = sparseCheckoutProbe(false)
registerSshGitProvider(connectionId, {
listWorktrees: vi.fn().mockResolvedValue([gitWorktree(projectRoot)]),
exec
} as never)
const { store } = makeStore()
await expect(
adoptProvisionedRootSshCheckout({
userDataPath,
request: {
...request(projectRoot),
baseBranch: 'v1.2.3',
expectedRefHead: 'abc123'
},
repo: repo(projectRoot),
store,
isRepoCurrent: () => true
})
).resolves.toMatchObject({ worktree: { branch: 'refs/heads/fix-sandbox' } })
})
it('rejects a requested ref without its source-host commit identity', async () => {
seedRuntime(userDataPath, projectRoot)
registerSshGitProvider(connectionId, {
listWorktrees: vi.fn().mockResolvedValue([gitWorktree(projectRoot)]),
exec: sparseCheckoutProbe(false)
} as never)
const { store, setWorktreeMeta } = makeStore()
await expect(
adoptProvisionedRootSshCheckout({
userDataPath,
request: { ...request(projectRoot), baseBranch: 'missing-ref' },
repo: repo(projectRoot),
store,
isRepoCurrent: () => true
})
).rejects.toThrow('ref identity is missing')
expect(setWorktreeMeta).not.toHaveBeenCalled()
})
it('uses an explicit branch override as the requested local branch', async () => {
seedRuntime(userDataPath, projectRoot)
registerSshGitProvider(connectionId, {
listWorktrees: vi
.fn()
.mockResolvedValue([gitWorktree(projectRoot, { branch: 'refs/heads/feature/review' })]),
exec: sparseCheckoutProbe(false)
} as never)
const { store } = makeStore()
await expect(
adoptProvisionedRootSshCheckout({
userDataPath,
request: { ...request(projectRoot), branchNameOverride: 'feature/review' },
repo: repo(projectRoot),
store,
isRepoCurrent: () => true
})
).resolves.toMatchObject({ worktree: { branch: 'refs/heads/feature/review' } })
})
it('rejects a linked worktree and sparse checkout', async () => {
seedRuntime(userDataPath, projectRoot)
const listWorktrees = vi
.fn()
.mockResolvedValue([gitWorktree(projectRoot, { isMainWorktree: false })])
registerSshGitProvider(connectionId, {
listWorktrees,
exec: sparseCheckoutProbe(false)
} as never)
const { store, setWorktreeMeta } = makeStore()
await expect(
adoptProvisionedRootSshCheckout({
userDataPath,
request: request(projectRoot),
repo: repo(projectRoot),
store,
isRepoCurrent: () => true
})
).rejects.toThrow('must be the repository primary checkout')
await expect(
adoptProvisionedRootSshCheckout({
userDataPath,
@@ -242,15 +56,10 @@ describe('adoptProvisionedRootSshCheckout', () => {
isRepoCurrent: () => true
})
).rejects.toThrow('do not support sparse checkout')
expect(setWorktreeMeta).not.toHaveBeenCalled()
})
it('rejects path and runtime target mismatches', async () => {
seedRuntime(userDataPath, projectRoot)
registerSshGitProvider(connectionId, {
listWorktrees: vi.fn().mockResolvedValue([gitWorktree(projectRoot)]),
exec: sparseCheckoutProbe(false)
} as never)
const { store } = makeStore()
await expect(
@@ -272,79 +81,6 @@ describe('adoptProvisionedRootSshCheckout', () => {
})
).rejects.toThrow('host does not match')
})
it('rejects provider rotation during verification', async () => {
seedRuntime(userDataPath, projectRoot)
let resolveList: (value: ReturnType<typeof gitWorktree>[]) => void = () => undefined
const listWorktrees = vi.fn(
() =>
new Promise<ReturnType<typeof gitWorktree>[]>((resolve) => {
resolveList = resolve
})
)
registerSshGitProvider(connectionId, {
listWorktrees,
exec: sparseCheckoutProbe(false)
} as never)
const { store } = makeStore()
const adoption = adoptProvisionedRootSshCheckout({
userDataPath,
request: request(projectRoot),
repo: repo(projectRoot),
store,
isRepoCurrent: () => true
})
await vi.waitFor(() => expect(listWorktrees).toHaveBeenCalledOnce())
rotateSshProviderAuthority(connectionId)
resolveList([gitWorktree(projectRoot)])
await expect(adoption).rejects.toThrow('changed during checkout verification')
})
it('compares Windows checkout roots using runtime path semantics', async () => {
const windowsRoot = 'C:\\Workspace\\Orca'
seedRuntime(userDataPath, windowsRoot)
registerSshGitProvider(connectionId, {
listWorktrees: vi.fn().mockResolvedValue([gitWorktree('c:/workspace/orca/')]),
exec: sparseCheckoutProbe(false)
} as never)
const { store } = makeStore()
const result = await adoptProvisionedRootSshCheckout({
userDataPath,
request: request('C:/WORKSPACE/ORCA'),
repo: repo('c:\\workspace\\orca'),
store,
isRepoCurrent: () => true
})
expect(result.worktree.path).toBe('c:/workspace/orca/')
})
it('rejects sparse checkout enabled in the remote Git config', async () => {
seedRuntime(userDataPath, projectRoot)
const exec = sparseCheckoutProbe(true)
registerSshGitProvider(connectionId, {
listWorktrees: vi.fn().mockResolvedValue([gitWorktree(projectRoot)]),
exec
} as never)
const { store, setWorktreeMeta } = makeStore()
await expect(
adoptProvisionedRootSshCheckout({
userDataPath,
request: request(projectRoot),
repo: repo(projectRoot),
store,
isRepoCurrent: () => true
})
).rejects.toThrow('cannot adopt a sparse checkout')
expect(exec).toHaveBeenCalledWith(
['config', '--bool', '--get', '--default=false', 'core.sparseCheckout'],
projectRoot
)
expect(setWorktreeMeta).not.toHaveBeenCalled()
})
})
function seedRuntime(userDataPath: string, root: string): void {
@@ -398,21 +134,6 @@ function request(expectedPath: string): AdoptProvisionedRootArgs {
}
}
function gitWorktree(path: string, overrides: Partial<GitWorktreeInfo> = {}): GitWorktreeInfo {
return {
path,
head: 'abc123',
branch: 'refs/heads/fix-sandbox',
isBare: false,
isMainWorktree: true,
...overrides
}
}
function sparseCheckoutProbe(enabled: boolean): ReturnType<typeof vi.fn> {
return vi.fn().mockResolvedValue({ stdout: `${enabled}\n`, stderr: '' })
}
function makeStore(): {
store: Store
setWorktreeMeta: ReturnType<typeof vi.fn>
+4 -66
View File
@@ -12,18 +12,7 @@ import {
} from '../shared/ephemeral-vm-recipes'
import { listEphemeralVmRuntimes } from '../shared/ephemeral-vm-runtime-store'
import type { EphemeralVmRuntimeRecord } from '../shared/ephemeral-vm-runtimes'
import { getSshGitProvider } from './providers/ssh-git-dispatch'
import {
getSshProviderAuthority,
isCurrentSshProviderAuthority
} from './ssh/ssh-provider-authority'
import { attachEphemeralVmRuntimeToWorkspace } from './ephemeral-vm-runtime-attachment'
import { mergeWorktree } from './ipc/worktree-logic'
import {
buildProvisionedRootMeta,
provisionedRootPathsEqual,
verifyProvisionedRootCheckout
} from './provisioned-root-checkout'
import { provisionedRootPathsEqual } from './provisioned-root-checkout'
type AdoptionArgs = AdoptProvisionedRootArgs & {
automationProvenance?: AutomationWorkspaceProvenance
@@ -36,7 +25,7 @@ export async function adoptProvisionedRootSshCheckout(args: {
store: Store
isRepoCurrent: () => boolean
}): Promise<CreateWorktreeResult> {
const { request, repo, store } = args
const { request, repo } = args
if (request.sparseCheckout) {
throw new Error('Provisioned-root recipes do not support sparse checkout.')
}
@@ -61,59 +50,8 @@ export async function adoptProvisionedRootSshCheckout(args: {
throw new Error('The recipe projectRoot does not match the imported Git checkout root.')
}
const provider = getSshGitProvider(connectionId)
if (!provider) {
throw new Error('The recipe-created SSH connection is no longer available.')
}
const authority = { ...getSshProviderAuthority(connectionId) }
const [worktrees, sparseCheckoutEnabled] = await Promise.all([
provider.listWorktrees(repo.path),
isSparseCheckoutEnabled(provider, projectRoot)
])
if (
getSshGitProvider(connectionId) !== provider ||
!isCurrentSshProviderAuthority(authority) ||
!args.isRepoCurrent()
) {
throw new Error('The recipe-created SSH connection changed during checkout verification.')
}
requireOwnedProvisionedRootRuntime(args.userDataPath, request.runtimeId, connectionId)
const gitWorktree = verifyProvisionedRootCheckout({
worktrees,
sparseCheckoutEnabled,
projectRoot,
request
})
const worktreeId = `${repo.id}::${gitWorktree.path}`
attachEphemeralVmRuntimeToWorkspace({
userDataPath: args.userDataPath,
runtimeId: request.runtimeId,
workspaceId: worktreeId
})
const now = Date.now()
const meta = store.setWorktreeMeta(
worktreeId,
buildProvisionedRootMeta(store, repo, request, {
branchName: gitWorktree.branch.replace(/^refs\/heads\//, ''),
now,
hostId: args.request.executionHostId,
source: 'ssh'
})
)
return { worktree: mergeWorktree(repo.id, gitWorktree, meta) }
}
async function isSparseCheckoutEnabled(
provider: NonNullable<ReturnType<typeof getSshGitProvider>>,
projectRoot: string
): Promise<boolean> {
const { stdout } = await provider.exec(
['config', '--bool', '--get', '--default=false', 'core.sparseCheckout'],
projectRoot
)
return stdout.trim() === 'true'
// Why: this process serves no SSH git, so a provisioned SSH checkout is never adopted here.
throw new Error('The recipe-created SSH connection is no longer available.')
}
function requireOwnedProvisionedRootRuntime(
@@ -0,0 +1,68 @@
import { describe, expect, it, vi } from 'vitest'
import { openMock, resolveAuthorizedPathMock } from './orca-runtime-files-mock-registry'
import {
createRuntimeFileCommands,
useRuntimeFileCommandsLifecycle
} from './orca-runtime-files-test-harness'
import { SshHostNotServedError } from '../providers/ssh-host-not-served-error'
vi.mock('fs', async () => (await import('./orca-runtime-files-mock-registry')).fsModuleMock())
vi.mock('fs/promises', async () =>
(await import('./orca-runtime-files-mock-registry')).fsPromisesModuleMock()
)
vi.mock(
'./file-watcher-host',
async () => (await import('./orca-runtime-files-mock-registry')).fileWatcherHostMock
)
vi.mock('../ipc/filesystem-auth', async () =>
(await import('./orca-runtime-files-mock-registry')).filesystemAuthModuleMock()
)
vi.mock('../git/runner', async () =>
(await import('./orca-runtime-files-mock-registry')).gitRunnerModuleMock()
)
vi.mock(
'../ipc/local-worktree-runtime-options',
async () => (await import('./orca-runtime-files-mock-registry')).localWorktreeRuntimeOptionsMock
)
describe('RuntimeFileCommands.readFileExplorerChunk', () => {
useRuntimeFileCommandsLifecycle()
it('serves a local worktree from the local file handle', async () => {
const contents = Buffer.from(Array.from({ length: 2048 }, (_, index) => (index * 31) % 251))
const { commands } = createRuntimeFileCommands()
resolveAuthorizedPathMock.mockResolvedValue('/repo/local.bin')
const close = vi.fn(async () => undefined)
openMock.mockResolvedValue({
stat: vi.fn(async () => ({ isFile: () => true, size: contents.byteLength })),
read: vi.fn(
async (buffer: Buffer, bufferOffset: number, length: number, position: number) => {
const bytesRead = contents.copy(buffer, bufferOffset, position, position + length)
return { bytesRead }
}
),
close
})
const chunk = await commands.readFileExplorerChunk('id:wt-1', 'local.bin', 0, 1024)
expect(Buffer.from(chunk.contentBase64, 'base64')).toEqual(contents.subarray(0, 1024))
expect(chunk.eof).toBe(false)
expect(close).toHaveBeenCalled()
})
it('refuses an SSH worktree without opening a local file', async () => {
const { commands } = createRuntimeFileCommands({
path: '/remote/repo',
resolveRuntimeFileTarget: vi.fn(async () => ({
worktree: { id: 'wt-1', repoId: 'repo-1', path: '/remote/repo' },
executionHostId: 'ssh:ssh-1'
}))
})
await expect(commands.readFileExplorerChunk('id:wt-1', 'big.bin', 0, 1024)).rejects.toThrow(
SshHostNotServedError
)
expect(openMock).not.toHaveBeenCalled()
})
})
@@ -1,4 +1,5 @@
import { describe, expect, it, vi } from 'vitest'
import { SshHostNotServedError } from '../providers/ssh-host-not-served-error'
import type * as FileSystem from 'node:fs/promises'
import {
enoent,
@@ -10,7 +11,6 @@ import {
createRuntimeFileCommands,
useRuntimeFileCommandsLifecycle
} from './orca-runtime-files-test-harness'
import { getSshFilesystemProvider } from '../providers/ssh-filesystem-dispatch'
vi.mock('fs', async () => (await import('./orca-runtime-files-mock-registry')).fsModuleMock())
vi.mock('fs/promises', async () =>
@@ -30,10 +30,6 @@ vi.mock(
'../ipc/local-worktree-runtime-options',
async () => (await import('./orca-runtime-files-mock-registry')).localWorktreeRuntimeOptionsMock
)
vi.mock(
'../providers/ssh-filesystem-dispatch',
async () => (await import('./orca-runtime-files-mock-registry')).sshFilesystemDispatchMock
)
function dirEntry(args: { name: string; directory?: boolean; symlink?: boolean }) {
return {
@@ -181,42 +177,22 @@ describe('RuntimeFileCommands', () => {
)
})
it('names this desktop as the owner of SSH file and diff opens', async () => {
it('refuses SSH file opens without creating an editor tab', async () => {
const openFile = vi.fn()
const openDiff = vi.fn()
const resolveRuntimeFileTarget = vi.fn(async () => ({
worktree: { id: 'wt-1', repoId: 'repo-1', path: '/remote/repo' },
executionHostId: 'ssh:ssh-1'
}))
const { commands } = createRuntimeFileCommands({
openFile,
openDiff,
path: '/remote/repo',
resolveRuntimeFileTarget
})
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the open path only calls `stat`.
vi.mocked(getSshFilesystemProvider).mockReturnValue({
stat: vi.fn().mockResolvedValue({ type: 'file', size: 1, mtime: 0 })
} as never)
await commands.openMobileFile('id:wt-1', 'docs/readme.md')
await commands.openMobileDiff('id:wt-1', 'docs/readme.md', false)
expect(openFile).toHaveBeenCalledWith(
'wt-1',
'/remote/repo/docs/readme.md',
'docs/readme.md',
null,
undefined
)
expect(openDiff).toHaveBeenCalledWith(
'wt-1',
'/remote/repo/docs/readme.md',
'docs/readme.md',
false,
null,
undefined
await expect(commands.openMobileFile('id:wt-1', 'docs/readme.md')).rejects.toThrow(
SshHostNotServedError
)
expect(openFile).not.toHaveBeenCalled()
})
it('names a paired server as the owner of a diff its catalog row is stamped with', async () => {
@@ -288,31 +264,6 @@ describe('RuntimeFileCommands', () => {
expect(openFile).not.toHaveBeenCalled()
})
it('rejects missing remote files without creating an editor tab', async () => {
const openFile = vi.fn()
const resolveRuntimeFileTarget = vi.fn(async () => ({
worktree: {
id: 'wt-1',
repoId: 'repo-1',
path: '/remote/repo'
},
executionHostId: 'ssh:ssh-1'
}))
const { commands } = createRuntimeFileCommands({
openFile,
path: '/remote/repo',
resolveRuntimeFileTarget
})
vi.mocked(getSshFilesystemProvider).mockReturnValue({
stat: vi.fn().mockRejectedValue(new Error('ENOENT: no such file or directory'))
} as never)
await expect(commands.openMobileFile('id:wt-1', 'docs/missing.md')).rejects.toThrow(
"ENOENT: no such file or directory, open '/remote/repo/docs/missing.md'"
)
expect(openFile).not.toHaveBeenCalled()
})
it('rejects a local directory without creating an editor tab', async () => {
const openFile = vi.fn()
const { commands } = createRuntimeFileCommands({ openFile })
@@ -325,32 +276,6 @@ describe('RuntimeFileCommands', () => {
expect(openFile).not.toHaveBeenCalled()
})
it('rejects a remote directory without creating an editor tab', async () => {
const openFile = vi.fn()
const resolveRuntimeFileTarget = vi.fn(async () => ({
worktree: {
id: 'wt-1',
repoId: 'repo-1',
path: '/remote/repo'
},
executionHostId: 'ssh:ssh-1'
}))
const { commands } = createRuntimeFileCommands({
openFile,
path: '/remote/repo',
resolveRuntimeFileTarget
})
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the open path only calls `stat`.
vi.mocked(getSshFilesystemProvider).mockReturnValue({
stat: vi.fn().mockResolvedValue({ type: 'directory', size: 0, mtime: 0 })
} as never)
await expect(commands.openMobileFile('id:wt-1', 'src')).rejects.toThrow(
"EISDIR: illegal operation on a directory, open '/remote/repo/src'"
)
expect(openFile).not.toHaveBeenCalled()
})
it('does not follow symlinks when reading runtime-local file explorer dirs', async () => {
const { commands } = createRuntimeFileCommands()
resolveAuthorizedPathMock.mockResolvedValue('/repo')
@@ -7,10 +7,8 @@ import {
createRuntimeFileCommands,
useRuntimeFileCommandsLifecycle
} from './orca-runtime-files-test-harness'
import { getSshFilesystemProvider } from '../providers/ssh-filesystem-dispatch'
import { RUNTIME_PREVIEWABLE_BINARY_MAX_BYTES } from './orca-runtime-files'
import { REMOTE_RPC_MAX_CONTENT_BYTES } from '../../shared/remote-rpc-content-budget'
import { FileReadCapExceededError, StreamProtocolError } from '../ssh/ssh-filesystem-stream-reader'
vi.mock('fs', async () => (await import('./orca-runtime-files-mock-registry')).fsModuleMock())
vi.mock('fs/promises', async () =>
@@ -30,10 +28,6 @@ vi.mock(
'../ipc/local-worktree-runtime-options',
async () => (await import('./orca-runtime-files-mock-registry')).localWorktreeRuntimeOptionsMock
)
vi.mock(
'../providers/ssh-filesystem-dispatch',
async () => (await import('./orca-runtime-files-mock-registry')).sshFilesystemDispatchMock
)
describe('RuntimeFileCommands', () => {
useRuntimeFileCommandsLifecycle()
@@ -106,152 +100,5 @@ describe('RuntimeFileCommands', () => {
mimeType: 'image/png'
})
})
it('rejects an SSH text preview past the decoded text limit the local branch enforces', async () => {
const { commands, store } = createRuntimeFileCommands({ path: '/repo' })
store.getRepo.mockReturnValue({ connectionId: 'ssh-1' })
// NUL-free control bytes: sniffed as text, yet each escapes to six JSON bytes.
const content = '\u0001'.repeat(1024 * 1024)
vi.mocked(getSshFilesystemProvider).mockReturnValue({
stat: vi.fn().mockResolvedValue({ type: 'file', size: content.length }),
readFile: vi.fn().mockResolvedValue({ content, isBinary: false })
} as never)
await expect(commands.readFileExplorerPreview('id:wt-1', 'log.txt')).rejects.toThrow(
'file_too_large'
)
})
it('still returns an SSH binary preview inside the base64 cap', async () => {
const { commands, store } = createRuntimeFileCommands({ path: '/repo' })
store.getRepo.mockReturnValue({ connectionId: 'ssh-1' })
const preview = { content: 'a'.repeat(1024 * 1024), isBinary: true, isImage: true }
vi.mocked(getSshFilesystemProvider).mockReturnValue({
stat: vi
.fn()
.mockResolvedValue({ type: 'file', size: RUNTIME_PREVIEWABLE_BINARY_MAX_BYTES }),
readFile: vi.fn().mockResolvedValue(preview)
} as never)
await expect(commands.readFileExplorerPreview('id:wt-1', 'logo.png')).resolves.toEqual(
preview
)
})
it('rejects an SSH binary result that grew past its request-scoped budget', async () => {
const { commands, store } = createRuntimeFileCommands({ path: '/repo' })
store.getRepo.mockReturnValue({ connectionId: 'ssh-1' })
const readFile = vi.fn().mockResolvedValue({
content: 'a'.repeat(13),
isBinary: true,
isImage: true
})
vi.mocked(getSshFilesystemProvider).mockReturnValue({
stat: vi.fn().mockResolvedValue({ type: 'file', size: 0 }),
readFile
} as never)
await expect(commands.readFileExplorerPreview('id:wt-1', 'logo.png', 12)).rejects.toThrow(
'file_too_large'
)
expect(readFile).toHaveBeenCalledWith('/repo/logo.png', {
maxBinaryBytes: 0,
maxTextBytes: 512 * 1024
})
})
it('rejects escape-dense SSH text beyond the request-scoped result budget', async () => {
const { commands, store } = createRuntimeFileCommands({ path: '/repo' })
store.getRepo.mockReturnValue({ connectionId: 'ssh-1' })
vi.mocked(getSshFilesystemProvider).mockReturnValue({
stat: vi.fn().mockResolvedValue({ type: 'file', size: 64 }),
readFile: vi.fn().mockResolvedValue({ content: '\u0001'.repeat(64), isBinary: false })
} as never)
await expect(commands.readFileExplorerPreview('id:wt-1', 'log.txt', 128)).rejects.toThrow(
'file_too_large'
)
})
// Why: without translation the reader's raw "exceeds client cap" string reaches the client as a
// generic runtime_error, which neither the desktop nor the mobile preview arm recognizes.
it('translates an over-cap stream read into file_too_large', async () => {
const { commands, store } = createRuntimeFileCommands({ path: '/repo' })
store.getRepo.mockReturnValue({ connectionId: 'ssh-1' })
vi.mocked(getSshFilesystemProvider).mockReturnValue({
stat: vi.fn().mockResolvedValue({ type: 'file', size: 1024 }),
readFile: vi
.fn()
.mockRejectedValue(
new FileReadCapExceededError('Reported totalSize 900000 exceeds client cap 524288')
)
} as never)
await expect(commands.readFileExplorerPreview('id:wt-1', 'log.txt')).rejects.toThrow(
'file_too_large'
)
})
it('leaves a genuine stream protocol failure unmasked', async () => {
const { commands, store } = createRuntimeFileCommands({ path: '/repo' })
store.getRepo.mockReturnValue({ connectionId: 'ssh-1' })
vi.mocked(getSshFilesystemProvider).mockReturnValue({
stat: vi.fn().mockResolvedValue({ type: 'file', size: 1024 }),
readFile: vi.fn().mockRejectedValue(new StreamProtocolError('Malformed chunk for stream 4'))
} as never)
await expect(commands.readFileExplorerPreview('id:wt-1', 'log.txt')).rejects.toThrow(
'Malformed chunk'
)
})
it('rejects oversized SSH preview metadata with small content', async () => {
const { commands, store } = createRuntimeFileCommands({ path: '/repo' })
store.getRepo.mockReturnValue({ connectionId: 'ssh-1' })
vi.mocked(getSshFilesystemProvider).mockReturnValue({
stat: vi.fn().mockResolvedValue({ type: 'file', size: 1 }),
readFile: vi.fn().mockResolvedValue({
content: 'a',
isBinary: true,
mimeType: 'x'.repeat(128)
})
} as never)
await expect(commands.readFileExplorerPreview('id:wt-1', 'logo.png', 128)).rejects.toThrow(
'file_too_large'
)
})
it('sends preview authority to the SSH execution host in one read', async () => {
const { commands, store } = createRuntimeFileCommands({ path: '/repo' })
store.getRepo.mockReturnValue({ connectionId: 'ssh-1' })
const readDocPreviewFile = vi.fn().mockResolvedValue({ content: 'ok', isBinary: false })
vi.mocked(getSshFilesystemProvider).mockReturnValue({ readDocPreviewFile } as never)
await expect(
commands.readDocPreviewFile('id:wt-1', 'docs/index.html', 'docs/index.html', 'docs', [
'assets'
])
).resolves.toEqual({ content: 'ok', isBinary: false })
expect(readDocPreviewFile).toHaveBeenCalledWith({
boundaryPath: '/repo',
entryPath: '/repo/docs/index.html',
implicitRootPath: '/repo/docs',
authorizedRootPaths: ['/repo/assets'],
targetPath: '/repo/docs/index.html',
maxTextBytes: 512 * 1024,
maxBinaryBytes: 10 * 1024 * 1024
})
})
it('fails closed when the SSH execution host lacks scoped preview reads', async () => {
const { commands, store } = createRuntimeFileCommands({ path: '/repo' })
store.getRepo.mockReturnValue({ connectionId: 'ssh-1' })
vi.mocked(getSshFilesystemProvider).mockReturnValue({} as never)
await expect(
commands.readDocPreviewFile('id:wt-1', 'docs/index.html', 'docs/index.html', 'docs', [])
).rejects.toThrow('newer SSH relay')
})
})
})
@@ -9,7 +9,7 @@ import {
createRuntimeFileCommands,
useRuntimeFileCommandsLifecycle
} from './orca-runtime-files-test-harness'
import { getSshFilesystemProvider } from '../providers/ssh-filesystem-dispatch'
import { SshHostNotServedError } from '../providers/ssh-host-not-served-error'
import { setSshConnectionGeneration } from '../ssh/ssh-connection-generation'
vi.mock('fs', async () => (await import('./orca-runtime-files-mock-registry')).fsModuleMock())
@@ -30,10 +30,6 @@ vi.mock(
'../ipc/local-worktree-runtime-options',
async () => (await import('./orca-runtime-files-mock-registry')).localWorktreeRuntimeOptionsMock
)
vi.mock(
'../providers/ssh-filesystem-dispatch',
async () => (await import('./orca-runtime-files-mock-registry')).sshFilesystemDispatchMock
)
function mockStats(dev: number, ino: number) {
return { dev, ino, isDirectory: () => false }
@@ -76,7 +72,6 @@ describe('RuntimeFileCommands', () => {
'newer Orca client'
)
expect(getSshFilesystemProvider).not.toHaveBeenCalled()
expect(renameMock).not.toHaveBeenCalled()
})
@@ -88,7 +83,6 @@ describe('RuntimeFileCommands', () => {
commands.renameFileExplorerPath('id:wt-1', 'old.ts', 'new.ts', 0, 'ssh-1')
).rejects.toThrow('newer Orca client')
expect(getSshFilesystemProvider).not.toHaveBeenCalled()
expect(renameMock).not.toHaveBeenCalled()
})
@@ -100,7 +94,6 @@ describe('RuntimeFileCommands', () => {
commands.renameFileExplorerPath('id:wt-1', 'old.ts', 'new.ts', undefined, undefined, 'local')
).rejects.toThrow('Workspace host changed')
expect(getSshFilesystemProvider).not.toHaveBeenCalled()
expect(renameMock).not.toHaveBeenCalled()
})
@@ -111,7 +104,6 @@ describe('RuntimeFileCommands', () => {
commands.renameFileExplorerPath('id:wt-1', 'old.ts', 'new.ts', 0, 'ssh-1', 'ssh:ssh-1')
).rejects.toThrow('Workspace host changed')
expect(getSshFilesystemProvider).not.toHaveBeenCalled()
expect(renameMock).not.toHaveBeenCalled()
})
@@ -190,22 +182,17 @@ describe('RuntimeFileCommands', () => {
expect(renameMock).not.toHaveBeenCalled()
})
it('routes runtime remote rename through the SSH no-clobber provider method', async () => {
const renameNoClobber = vi.fn().mockResolvedValue(undefined)
vi.mocked(getSshFilesystemProvider).mockReturnValue({ renameNoClobber } as never)
it('refuses a matching SSH rename without renaming on this machine', async () => {
const { commands, store } = createRuntimeFileCommands()
store.getRepo.mockReturnValue({ connectionId: 'ssh-1' })
await commands.renameFileExplorerPath('id:wt-1', 'old.ts', 'new.ts', 0, 'ssh-1', 'ssh:ssh-1')
expect(renameNoClobber).toHaveBeenCalledWith('/repo/old.ts', '/repo/new.ts')
expect(store.getRepo).toHaveBeenCalledTimes(1)
await expect(
commands.renameFileExplorerPath('id:wt-1', 'old.ts', 'new.ts', 0, 'ssh-1', 'ssh:ssh-1')
).rejects.toThrow(SshHostNotServedError)
expect(renameMock).not.toHaveBeenCalled()
})
it('rejects a mutation captured for an obsolete SSH connection generation', async () => {
const renameNoClobber = vi.fn().mockResolvedValue(undefined)
vi.mocked(getSshFilesystemProvider).mockReturnValue({ renameNoClobber } as never)
const { commands, store } = createRuntimeFileCommands()
store.getRepo.mockReturnValue({ connectionId: 'ssh-1' })
setSshConnectionGeneration('ssh-1', 8)
@@ -213,12 +200,10 @@ describe('RuntimeFileCommands', () => {
await expect(
commands.renameFileExplorerPath('id:wt-1', 'old.ts', 'new.ts', 7, 'ssh-1', 'ssh:ssh-1')
).rejects.toThrow('SSH connection changed')
expect(renameNoClobber).not.toHaveBeenCalled()
expect(renameMock).not.toHaveBeenCalled()
})
it('rejects nested SSH mutations from clients without generation support', async () => {
const renameNoClobber = vi.fn().mockResolvedValue(undefined)
vi.mocked(getSshFilesystemProvider).mockReturnValue({ renameNoClobber } as never)
const { commands, store } = createRuntimeFileCommands()
store.getRepo.mockReturnValue({ connectionId: 'ssh-1' })
@@ -232,20 +217,17 @@ describe('RuntimeFileCommands', () => {
'ssh:ssh-1'
)
).rejects.toThrow('SSH connection changed')
expect(renameNoClobber).not.toHaveBeenCalled()
expect(renameMock).not.toHaveBeenCalled()
})
it('rejects an equal-generation mutation captured for another SSH target', async () => {
const renameNoClobber = vi.fn().mockResolvedValue(undefined)
vi.mocked(getSshFilesystemProvider).mockReturnValue({ renameNoClobber } as never)
const { commands, store } = createRuntimeFileCommands()
store.getRepo.mockReturnValue({ connectionId: 'ssh-b' })
await expect(
commands.renameFileExplorerPath('id:wt-1', 'old.ts', 'new.ts', 0, 'ssh-a', 'ssh:ssh-a')
).rejects.toThrow('Workspace host changed')
expect(getSshFilesystemProvider).not.toHaveBeenCalled()
expect(renameNoClobber).not.toHaveBeenCalled()
expect(renameMock).not.toHaveBeenCalled()
})
it('rejects a stale SSH expectation after the worktree becomes HUB-local', async () => {
@@ -256,16 +238,4 @@ describe('RuntimeFileCommands', () => {
).rejects.toThrow('Workspace host changed')
expect(renameMock).not.toHaveBeenCalled()
})
it('propagates runtime remote no-clobber rename failures', async () => {
const renameNoClobber = vi.fn().mockRejectedValue(new Error('destination exists'))
vi.mocked(getSshFilesystemProvider).mockReturnValue({ renameNoClobber } as never)
const { commands, store } = createRuntimeFileCommands()
store.getRepo.mockReturnValue({ connectionId: 'ssh-1' })
await expect(
commands.renameFileExplorerPath('id:wt-1', 'old.ts', 'new.ts', 0, 'ssh-1', 'ssh:ssh-1')
).rejects.toThrow('destination exists')
expect(renameMock).not.toHaveBeenCalled()
})
})
@@ -3,7 +3,6 @@ import { EventEmitter } from 'node:events'
import {
bundledRipgrepCommandMock,
getLocalGitOptionsForRegisteredWorktreeMock,
getSshFilesystemProviderMock,
resolveAuthorizedPathMock,
wslAwareSpawnMock
} from './orca-runtime-files-mock-registry'
@@ -34,10 +33,6 @@ vi.mock(
vi.mock('../ripgrep/bundled-ripgrep-path', async () =>
(await import('./orca-runtime-files-mock-registry')).bundledRipgrepPathModuleMock()
)
vi.mock(
'../providers/ssh-filesystem-dispatch',
async () => (await import('./orca-runtime-files-mock-registry')).sshFilesystemDispatchMock
)
type MockRuntimeSearchChild = EventEmitter & {
stdout: EventEmitter & { setEncoding: ReturnType<typeof vi.fn> }
@@ -63,18 +58,6 @@ async function flushRuntimeSearchMicrotasks(): Promise<void> {
describe('RuntimeFileCommands', () => {
useRuntimeFileCommandsLifecycle()
it('forwards cancellation across a nested SSH route', async () => {
const controller = new AbortController()
const search = vi.fn().mockResolvedValue({ files: [], totalMatches: 0, truncated: false })
getSshFilesystemProviderMock.mockReturnValue({ search })
const { commands } = createRuntimeFileCommands({ hostId: 'ssh:ssh-1' })
await commands.searchRuntimeFiles('id:wt-1', { query: 'needle' }, { signal: controller.signal })
expect(search).toHaveBeenCalledWith(
{ rootPath: '/repo', query: 'needle' },
{ signal: controller.signal }
)
})
it('keeps concurrent runtime clients on the same root independent during cancellation', async () => {
const { commands } = createRuntimeFileCommands()
const firstChild = createRuntimeSearchChild()
@@ -147,26 +130,6 @@ describe('RuntimeFileCommands', () => {
)
})
it('keeps byte-budgeted legacy listings count-bounded across an SSH hop', async () => {
const listFiles = vi.fn().mockResolvedValue(['src/index.ts'])
getSshFilesystemProviderMock.mockReturnValue({ listFiles })
const { commands } = createRuntimeFileCommands({
resolveRuntimeFileTarget: vi.fn(async () => ({
worktree: { id: 'wt-1', repoId: 'repo-1', path: '/repo' },
executionHostId: 'ssh:ssh-1'
}))
})
await expect(commands.listRuntimeFiles('id:wt-1', { maxContentBytes: 1024 })).resolves.toEqual([
'src/index.ts'
])
expect(listFiles).toHaveBeenCalledWith('/repo', {
excludePaths: undefined,
maxResults: 20_001,
signal: undefined
})
})
it('settles and detaches runtime rg searches when timeout kill is ignored', async () => {
const resolveRuntimeFileTarget = vi.fn(async () => ({
worktree: {
@@ -327,30 +290,4 @@ describe('RuntimeFileCommands', () => {
})
)
})
it('keeps legacy SSH Quick Open replies within the frame-sized result bound', async () => {
const resolveRuntimeFileTarget = vi.fn(async () => ({
worktree: { id: 'wt-1', repoId: 'repo-1', path: '/repo' },
executionHostId: 'ssh:ssh-1'
}))
const { commands } = createRuntimeFileCommands({ resolveRuntimeFileTarget })
const listFiles = vi.fn(async () => ['src/target.ts'])
getSshFilesystemProviderMock.mockReturnValue({
supportsQuickOpenSearch: vi.fn(async () => false),
listFiles
})
await expect(commands.searchQuickOpenFilePaths('id:wt-1', 'target', 32)).resolves.toMatchObject(
{
files: [{ relativePath: 'src/target.ts' }],
totalCount: 1,
truncated: false
}
)
expect(listFiles).toHaveBeenCalledWith('/repo', {
excludePaths: undefined,
maxResults: 32,
signal: undefined
})
})
})
@@ -1,330 +0,0 @@
import { createHash } from 'node:crypto'
import { describe, expect, it, vi } from 'vitest'
import { enoent, openMock, resolveAuthorizedPathMock } from './orca-runtime-files-mock-registry'
import {
createRuntimeFileCommands,
useRuntimeFileCommandsLifecycle
} from './orca-runtime-files-test-harness'
import { getSshFilesystemProvider } from '../providers/ssh-filesystem-dispatch'
import { FileRangeReadUnsupportedError } from '../providers/types'
import { MAX_FILE_RANGE_READ_BYTES } from '../../shared/file-range-read'
import { BROWSER_CLIENT_FILE_CHANNEL_CHUNK_MAX_BYTES } from '../../shared/browser-client-file-channel-protocol'
import type { RuntimeFileCommands } from './orca-runtime-files'
vi.mock('fs', async () => (await import('./orca-runtime-files-mock-registry')).fsModuleMock())
vi.mock('fs/promises', async () =>
(await import('./orca-runtime-files-mock-registry')).fsPromisesModuleMock()
)
vi.mock(
'./file-watcher-host',
async () => (await import('./orca-runtime-files-mock-registry')).fileWatcherHostMock
)
vi.mock('../ipc/filesystem-auth', async () =>
(await import('./orca-runtime-files-mock-registry')).filesystemAuthModuleMock()
)
vi.mock('../git/runner', async () =>
(await import('./orca-runtime-files-mock-registry')).gitRunnerModuleMock()
)
vi.mock(
'../ipc/local-worktree-runtime-options',
async () => (await import('./orca-runtime-files-mock-registry')).localWorktreeRuntimeOptionsMock
)
vi.mock(
'../providers/ssh-filesystem-dispatch',
async () => (await import('./orca-runtime-files-mock-registry')).sshFilesystemDispatchMock
)
/**
* LCG bytes, whose period is far past any fixture here. An index-derived pattern repeats on a
* power-of-two stride that divides the chunk and window sizes, which makes a re-read of the wrong
* window compare equal to the right one.
*/
function remoteFileBytes(size: number): Buffer {
const bytes = Buffer.alloc(size)
let state = 0x2f6e2b1
for (let index = 0; index < size; index += 1) {
state = (state * 1664525 + 1013904223) >>> 0
bytes[index] = state >>> 24
}
return bytes
}
function sshCommands() {
return createRuntimeFileCommands({
path: '/remote/repo',
resolveRuntimeFileTarget: vi.fn(async () => ({
worktree: { id: 'wt-1', repoId: 'repo-1', path: '/remote/repo' },
executionHostId: 'ssh:ssh-1'
}))
}).commands
}
function installSshFile(
contents: Buffer,
overrides?: { readFileRange?: unknown; stat?: unknown }
): { readFileRange: ReturnType<typeof vi.fn>; stat: ReturnType<typeof vi.fn> } {
const readFileRange = vi.fn(async (_filePath: string, position: number, length: number) => {
const bytes = contents.subarray(position, position + length)
return { bytes, bytesRead: bytes.byteLength }
})
const stat = vi.fn(async () => ({ size: contents.byteLength, type: 'file' as const, mtime: 0 }))
const provider = {
stat: overrides?.stat ?? stat,
...(overrides && 'readFileRange' in overrides
? { readFileRange: overrides.readFileRange }
: { readFileRange })
}
vi.mocked(getSshFilesystemProvider).mockReturnValue(provider as never)
return { readFileRange, stat }
}
/** Mirrors the `browser.upload` staging loop in browser-client-upload-transfer. */
async function drainUploadChunks(
commands: RuntimeFileCommands,
relativePath: string
): Promise<Buffer> {
const parts: Buffer[] = []
let offset = 0
for (let iterations = 0; ; iterations += 1) {
expect(iterations).toBeLessThan(64)
const chunk = await commands.readFileExplorerChunk(
'id:wt-1',
relativePath,
offset,
BROWSER_CLIENT_FILE_CHANNEL_CHUNK_MAX_BYTES
)
const payload = Buffer.from(chunk.contentBase64, 'base64')
// The real loop rejects a chunk whose count disagrees with its payload before staging it.
expect(payload.byteLength).toBe(chunk.bytesRead)
parts.push(payload)
offset += chunk.bytesRead
if (chunk.eof) {
return Buffer.concat(parts)
}
expect(chunk.bytesRead).toBeGreaterThan(0)
}
}
/** Digest compare: a failed `toEqual` on a few-hundred-KB Buffer spends minutes rendering a diff. */
function expectSameBytes(actual: Buffer, expected: Buffer): void {
const digest = (bytes: Buffer) => ({
byteLength: bytes.byteLength,
sha256: createHash('sha256').update(bytes).digest('hex')
})
expect(digest(actual)).toEqual(digest(expected))
}
/** The clamp and the cap together promise every request lands inside the statted file. */
function expectRequestsWithinFile(readFileRange: ReturnType<typeof vi.fn>, fileSize: number): void {
expect(readFileRange.mock.calls.length).toBeGreaterThan(0)
for (const [, position, length] of readFileRange.mock.calls) {
expect(length).toBeLessThanOrEqual(MAX_FILE_RANGE_READ_BYTES)
expect(position + length).toBeLessThanOrEqual(fileSize)
}
}
// A size that is not a multiple of either the chunk or the window, so a page served from the wrong
// offset cannot land on an identical-looking boundary.
const REMOTE_FILE_BYTES = BROWSER_CLIENT_FILE_CHANNEL_CHUNK_MAX_BYTES * 3 + 4321
// files.readChunk window the renderer's remote download loop uses (REMOTE_DOWNLOAD_CHUNK_BYTES).
const REMOTE_DOWNLOAD_WINDOW_BYTES = 384 * 1024
describe('RuntimeFileCommands.readFileExplorerChunk over SSH', () => {
useRuntimeFileCommandsLifecycle()
it('reassembles a multi-chunk SSH-workspace file byte-identically', async () => {
const contents = remoteFileBytes(REMOTE_FILE_BYTES)
const { readFileRange } = installSshFile(contents)
expectSameBytes(await drainUploadChunks(sshCommands(), 'docs/report.pdf'), contents)
expect(readFileRange).toHaveBeenCalledWith(
'/remote/repo/docs/report.pdf',
0,
expect.any(Number)
)
expectRequestsWithinFile(readFileRange, contents.byteLength)
})
it('ends on the chunk boundary when the file size is an exact chunk multiple', async () => {
const contents = remoteFileBytes(BROWSER_CLIENT_FILE_CHANNEL_CHUNK_MAX_BYTES * 2)
installSshFile(contents)
const commands = sshCommands()
const first = await commands.readFileExplorerChunk(
'id:wt-1',
'archive.zip',
0,
BROWSER_CLIENT_FILE_CHANNEL_CHUNK_MAX_BYTES
)
const second = await commands.readFileExplorerChunk(
'id:wt-1',
'archive.zip',
BROWSER_CLIENT_FILE_CHANNEL_CHUNK_MAX_BYTES,
BROWSER_CLIENT_FILE_CHANNEL_CHUNK_MAX_BYTES
)
expect(first.eof).toBe(false)
expect(second.bytesRead).toBe(BROWSER_CLIENT_FILE_CHANNEL_CHUNK_MAX_BYTES)
expect(second.eof).toBe(true)
expectSameBytes(
Buffer.concat([
Buffer.from(first.contentBase64, 'base64'),
Buffer.from(second.contentBase64, 'base64')
]),
contents
)
})
it('pages a window wider than the host range cap instead of asking for an over-cap read', async () => {
const contents = remoteFileBytes(REMOTE_FILE_BYTES)
const { readFileRange } = installSshFile(contents)
const chunk = await sshCommands().readFileExplorerChunk(
'id:wt-1',
'big.bin',
0,
REMOTE_FILE_BYTES
)
expectSameBytes(Buffer.from(chunk.contentBase64, 'base64'), contents)
expect(chunk.bytesRead).toBe(REMOTE_FILE_BYTES)
expect(chunk.eof).toBe(true)
expect(readFileRange.mock.calls.map((call) => [call[1], call[2]])).toEqual([
[0, MAX_FILE_RANGE_READ_BYTES],
[MAX_FILE_RANGE_READ_BYTES, REMOTE_FILE_BYTES - MAX_FILE_RANGE_READ_BYTES]
])
})
// The upload chunk (128 KiB) never pages; the renderer's 384 KiB download window is the caller
// that does, and it reaches SSH worktrees only because this seam stopped throwing.
it('pages the renderer download window into one capped read and one remainder', async () => {
const contents = remoteFileBytes(REMOTE_FILE_BYTES)
const { readFileRange } = installSshFile(contents)
const chunk = await sshCommands().readFileExplorerChunk(
'id:wt-1',
'big.bin',
0,
REMOTE_DOWNLOAD_WINDOW_BYTES
)
expectSameBytes(
Buffer.from(chunk.contentBase64, 'base64'),
contents.subarray(0, REMOTE_DOWNLOAD_WINDOW_BYTES)
)
expect(chunk.eof).toBe(false)
expect(readFileRange.mock.calls.map((call) => [call[1], call[2]])).toEqual([
[0, MAX_FILE_RANGE_READ_BYTES],
[MAX_FILE_RANGE_READ_BYTES, REMOTE_DOWNLOAD_WINDOW_BYTES - MAX_FILE_RANGE_READ_BYTES]
])
})
it('clamps a window that overshoots EOF instead of requesting past the file', async () => {
const contents = remoteFileBytes(MAX_FILE_RANGE_READ_BYTES + 4321)
const { readFileRange } = installSshFile(contents)
const chunk = await sshCommands().readFileExplorerChunk(
'id:wt-1',
'big.bin',
0,
REMOTE_DOWNLOAD_WINDOW_BYTES
)
expectSameBytes(Buffer.from(chunk.contentBase64, 'base64'), contents)
expect(chunk.eof).toBe(true)
expectRequestsWithinFile(readFileRange, contents.byteLength)
})
it('never reads past the requested window', async () => {
const contents = remoteFileBytes(4096)
installSshFile(contents)
const chunk = await sshCommands().readFileExplorerChunk('id:wt-1', 'small.bin', 1000, 500)
expect(chunk.bytesRead).toBe(500)
expect(Buffer.from(chunk.contentBase64, 'base64')).toEqual(contents.subarray(1000, 1500))
expect(chunk.eof).toBe(false)
})
// Not a stall guarantee: the caller's next chunk re-stats the now-smaller file and ends the
// transfer short, exactly as the local branch does. Only the single chunk's flag is pinned here.
it('stops on a window the host could not fill and reports it against the stale size', async () => {
const contents = remoteFileBytes(4096)
installSshFile(contents, {
readFileRange: vi.fn(async () => ({ bytes: contents.subarray(0, 10), bytesRead: 10 }))
})
const chunk = await sshCommands().readFileExplorerChunk('id:wt-1', 'short.bin', 0, 4096)
expect(chunk.bytesRead).toBe(10)
expect(chunk.eof).toBe(false)
})
it('fails loudly when the SSH host cannot serve ranged reads', async () => {
installSshFile(remoteFileBytes(4096), {
readFileRange: vi.fn(async () => {
throw new FileRangeReadUnsupportedError()
})
})
await expect(
sshCommands().readFileExplorerChunk('id:wt-1', 'docs/report.pdf', 0, 1024)
).rejects.toThrow('ranged file reads')
})
it('fails loudly when the SSH provider has no ranged-read method at all', async () => {
installSshFile(remoteFileBytes(4096), { readFileRange: undefined })
await expect(
sshCommands().readFileExplorerChunk('id:wt-1', 'docs/report.pdf', 0, 1024)
).rejects.toThrow('ranged file reads')
})
it('fails loudly when the remote file is missing', async () => {
const { readFileRange } = installSshFile(remoteFileBytes(4096), {
stat: vi.fn(async () => {
throw enoent()
})
})
await expect(
sshCommands().readFileExplorerChunk('id:wt-1', 'docs/missing.pdf', 0, 1024)
).rejects.toThrow('ENOENT')
expect(readFileRange).not.toHaveBeenCalled()
})
it('refuses a remote directory before reading', async () => {
const { readFileRange } = installSshFile(remoteFileBytes(0), {
stat: vi.fn(async () => ({ size: 0, type: 'directory' as const, mtime: 0 }))
})
await expect(sshCommands().readFileExplorerChunk('id:wt-1', 'docs', 0, 1024)).rejects.toThrow(
'Cannot download a directory'
)
expect(readFileRange).not.toHaveBeenCalled()
})
it('still serves a non-SSH worktree from the local file handle', async () => {
const contents = remoteFileBytes(2048)
const { commands } = createRuntimeFileCommands()
resolveAuthorizedPathMock.mockResolvedValue('/repo/local.bin')
const close = vi.fn(async () => undefined)
openMock.mockResolvedValue({
stat: vi.fn(async () => ({ isFile: () => true, size: contents.byteLength })),
read: vi.fn(
async (buffer: Buffer, bufferOffset: number, length: number, position: number) => {
const bytesRead = contents.copy(buffer, bufferOffset, position, position + length)
return { bytesRead }
}
),
close
})
const chunk = await commands.readFileExplorerChunk('id:wt-1', 'local.bin', 0, 1024)
expect(Buffer.from(chunk.contentBase64, 'base64')).toEqual(contents.subarray(0, 1024))
expect(chunk.eof).toBe(false)
expect(close).toHaveBeenCalled()
expect(getSshFilesystemProvider).not.toHaveBeenCalled()
})
})
@@ -3,7 +3,6 @@ import { link, mkdtemp, readFile, realpath, symlink, writeFile } from 'node:fs/p
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { enoent, resolveAuthorizedPathMock, statMock } from './orca-runtime-files-mock-registry'
import { getSshFilesystemProvider } from '../providers/ssh-filesystem-dispatch'
import {
createRuntimeFileCommands,
useRuntimeFileCommandsLifecycle
@@ -32,10 +31,6 @@ vi.mock(
'../ipc/local-worktree-runtime-options',
async () => (await import('./orca-runtime-files-mock-registry')).localWorktreeRuntimeOptionsMock
)
vi.mock(
'../providers/ssh-filesystem-dispatch',
async () => (await import('./orca-runtime-files-mock-registry')).sshFilesystemDispatchMock
)
describe('RuntimeFileCommands', () => {
useRuntimeFileCommandsLifecycle()
@@ -376,77 +371,9 @@ describe('RuntimeFileCommands', () => {
expect(statMock).not.toHaveBeenCalled()
})
it('uses the canonical remote temp artifact path for the exact grant', async () => {
const { commands, store } = createRuntimeFileCommands({ path: '/repo' })
store.getRepo.mockReturnValue({ connectionId: 'ssh-1' })
const stat = vi.fn().mockResolvedValue({ type: 'file', size: 2, mtime: 3 })
const realpath = vi.fn(async (p: string) =>
p === '/tmp/link-result.json' ? '/tmp/result.json' : p
)
vi.mocked(getSshFilesystemProvider).mockReturnValue({ stat, realpath } as never)
const result = await resolveTerminalArtifactPath(commands, '/tmp/link-result.json')
expect(stat).toHaveBeenCalledWith('/tmp/result.json')
expect(result).toMatchObject({
relativePath: null,
absolutePath: '/tmp/result.json',
exists: true,
openTarget: {
kind: 'absolute-file',
provider: 'ssh',
absolutePath: '/tmp/result.json'
}
})
})
it('does not grant hard-linked remote temp artifacts', async () => {
const { commands, store } = createRuntimeFileCommands({ path: '/repo' })
store.getRepo.mockReturnValue({ connectionId: 'ssh-1' })
const stat = vi.fn().mockResolvedValue({ type: 'file', size: 2, mtime: 3, nlink: 2 })
const realpath = vi.fn(async (p: string) => p)
vi.mocked(getSshFilesystemProvider).mockReturnValue({ stat, realpath } as never)
const result = await resolveTerminalArtifactPath(commands, '/tmp/result.json')
expect(result).toMatchObject({
relativePath: null,
absolutePath: '/tmp/result.json',
exists: false
})
expect(result.openTarget).toBeUndefined()
})
it('allows canonical remote macOS private temp artifacts', async () => {
const { commands, store } = createRuntimeFileCommands({ path: '/repo' })
store.getRepo.mockReturnValue({ connectionId: 'ssh-1' })
const stat = vi.fn().mockResolvedValue({ type: 'file', size: 2, mtime: 3 })
const realpath = vi.fn(async (p: string) => p)
vi.mocked(getSshFilesystemProvider).mockReturnValue({ stat, realpath } as never)
const result = await resolveTerminalArtifactPath(commands, '/private/tmp/result.json')
expect(stat).toHaveBeenCalledWith('/private/tmp/result.json')
expect(result).toMatchObject({
relativePath: null,
absolutePath: '/private/tmp/result.json',
exists: true,
openTarget: {
kind: 'absolute-file',
provider: 'ssh',
absolutePath: '/private/tmp/result.json'
}
})
})
it('does not grant remote temp artifacts that resolve outside allowed temp roots', async () => {
const { commands, store } = createRuntimeFileCommands({ path: '/repo' })
store.getRepo.mockReturnValue({ connectionId: 'ssh-1' })
const stat = vi.fn()
const realpath = vi.fn(async (p: string) =>
p === '/tmp/link-result.json' ? '/home/me/.ssh/config' : p
)
vi.mocked(getSshFilesystemProvider).mockReturnValue({ stat, realpath } as never)
const result = await commands.resolveTerminalPath(
'id:wt-1',
@@ -462,7 +389,7 @@ describe('RuntimeFileCommands', () => {
exists: false,
isDirectory: false
})
expect(stat).not.toHaveBeenCalled()
expect(statMock).not.toHaveBeenCalled()
})
})
})
@@ -11,7 +11,6 @@ import {
resolveTerminalArtifactPath,
useTerminalArtifactTempFiles
} from './orca-runtime-files-terminal-artifact-fixtures'
import { getSshFilesystemProvider } from '../providers/ssh-filesystem-dispatch'
import { SshHostNotServedError } from '../providers/ssh-host-not-served-error'
import { RUNTIME_PREVIEWABLE_BINARY_MAX_BYTES } from './orca-runtime-files'
import type { RuntimeFileCommands } from './orca-runtime-files'
@@ -34,10 +33,6 @@ vi.mock(
'../ipc/local-worktree-runtime-options',
async () => (await import('./orca-runtime-files-mock-registry')).localWorktreeRuntimeOptionsMock
)
vi.mock(
'../providers/ssh-filesystem-dispatch',
async () => (await import('./orca-runtime-files-mock-registry')).sshFilesystemDispatchMock
)
describe('RuntimeFileCommands', () => {
useRuntimeFileCommandsLifecycle()
@@ -45,42 +40,6 @@ describe('RuntimeFileCommands', () => {
describe('resolveTerminalPath', () => {
const { tempFile } = useTerminalArtifactTempFiles()
function createRemoteTerminalArtifactGrantFixture(
artifactPath = '/tmp/result.json',
nativeChat = false
) {
const { commands, store } = createRuntimeFileCommands({
path: '/repo',
...(nativeChat ? { hasRecentNativeChatOutputPath: vi.fn(() => true) } : {})
})
store.getRepo.mockReturnValue({ connectionId: 'ssh-1' })
let realArtifactPath = artifactPath
let artifactStat = { type: 'file', size: 11, mtime: 3 }
const stat = vi.fn(async () => artifactStat)
const readTerminalArtifact = vi
.fn()
.mockResolvedValue({ content: '{"ok":true}', isBinary: false })
const writeTerminalArtifact = vi.fn().mockResolvedValue({ type: 'file', size: 12, mtime: 4 })
const realpath = vi.fn(async (p: string) => (p === artifactPath ? realArtifactPath : p))
vi.mocked(getSshFilesystemProvider).mockReturnValue({
stat,
readTerminalArtifact,
realpath,
writeTerminalArtifact
} as never)
return {
commands,
readTerminalArtifact,
writeTerminalArtifact,
moveArtifactTarget: (nextPath: string) => {
realArtifactPath = nextPath
},
replaceArtifact: () => {
artifactStat = { type: 'file', size: 12, mtime: 4 }
}
}
}
function resolveRemoteNativeChatArtifact(commands: RuntimeFileCommands, artifactPath: string) {
return commands.resolveTerminalPath('id:wt-1', artifactPath, null, 'client-a', null, true, {
tabId: 'tab-1',
@@ -405,129 +364,16 @@ describe('RuntimeFileCommands', () => {
await expect(readFile(artifactPath, 'utf8')).resolves.toBe('%PDF text-looking bytes')
})
it('rejects remote binary terminal artifact writes before changing the file', async () => {
const { commands, store } = createRuntimeFileCommands({ path: '/repo' })
store.getRepo.mockReturnValue({ connectionId: 'ssh-1' })
const stat = vi
.fn()
.mockResolvedValue({ type: 'file', size: 4, mtimeMs: 3, isDirectory: () => false })
const writeTerminalArtifact = vi.fn().mockRejectedValue(new Error('binary_file'))
const realpath = vi.fn(async (p: string) => p)
const writeFile = vi.fn()
vi.mocked(getSshFilesystemProvider).mockReturnValue({
stat,
realpath,
writeFile,
writeTerminalArtifact
} as never)
const result = await resolveTerminalArtifactPath(commands, '/tmp/result.txt')
const grantId = result.openTarget?.kind === 'absolute-file' ? result.openTarget.grantId : ''
await expect(
commands.writeTerminalArtifactFile(
'id:wt-1',
grantId,
'/tmp/result.txt',
'not binary anymore',
'client-a'
)
).rejects.toThrow('binary_file')
expect(writeFile).not.toHaveBeenCalled()
expect(writeTerminalArtifact).toHaveBeenCalled()
})
it('refuses a native-chat artifact on an SSH target as not served', async () => {
const { commands, readTerminalArtifact } = createRemoteTerminalArtifactGrantFixture(
'/home/me/report.json',
true
)
const { commands, store } = createRuntimeFileCommands({
path: '/repo',
hasRecentNativeChatOutputPath: vi.fn(() => true)
})
store.getRepo.mockReturnValue({ connectionId: 'ssh-1' })
await expect(
resolveRemoteNativeChatArtifact(commands, '/home/me/report.json')
).rejects.toThrow(SshHostNotServedError)
expect(readTerminalArtifact).not.toHaveBeenCalled()
})
it('rejects remote terminal artifact reads when a grant no longer resolves to the granted path', async () => {
const { commands, readTerminalArtifact, moveArtifactTarget } =
createRemoteTerminalArtifactGrantFixture()
const result = await resolveTerminalArtifactPath(commands, '/tmp/result.json')
const target = absoluteFileTarget(result)
moveArtifactTarget('/home/me/.ssh/config')
await expect(
commands.readTerminalArtifactFile(
'id:wt-1',
target.grantId,
target.absolutePath,
'client-a'
)
).rejects.toThrow('terminal_file_grant_stale')
expect(readTerminalArtifact).not.toHaveBeenCalled()
})
it('rejects remote terminal artifact previews when a grant no longer resolves to the granted path', async () => {
const { commands, readTerminalArtifact, moveArtifactTarget } =
createRemoteTerminalArtifactGrantFixture('/tmp/result.png')
const result = await resolveTerminalArtifactPath(commands, '/tmp/result.png')
const target = absoluteFileTarget(result)
moveArtifactTarget('/tmp/other.png')
await expect(
commands.readTerminalArtifactPreview(
'id:wt-1',
target.grantId,
target.absolutePath,
'client-a'
)
).rejects.toThrow('terminal_file_grant_stale')
expect(readTerminalArtifact).not.toHaveBeenCalled()
})
it('rejects additive remote terminal preview fields beyond the request budget', async () => {
const { commands, readTerminalArtifact } =
createRemoteTerminalArtifactGrantFixture('/tmp/result.png')
const result = await resolveTerminalArtifactPath(commands, '/tmp/result.png')
const target = absoluteFileTarget(result)
readTerminalArtifact.mockResolvedValue({
content: 'a',
isBinary: true,
futureMetadata: 'x'.repeat(128)
})
await expect(
commands.readTerminalArtifactPreview(
'id:wt-1',
target.grantId,
target.absolutePath,
'client-a',
128
)
).rejects.toThrow('file_too_large')
})
it('rejects remote terminal artifact writes when a grant no longer resolves to the granted path', async () => {
const { commands, readTerminalArtifact, writeTerminalArtifact, moveArtifactTarget } =
createRemoteTerminalArtifactGrantFixture()
const result = await resolveTerminalArtifactPath(commands, '/tmp/result.json')
const target = absoluteFileTarget(result)
moveArtifactTarget('/home/me/.ssh/config')
await expect(
commands.writeTerminalArtifactFile(
'id:wt-1',
target.grantId,
target.absolutePath,
'{"ok":false}',
'client-a'
)
).rejects.toThrow('terminal_file_grant_stale')
expect(readTerminalArtifact).not.toHaveBeenCalled()
expect(writeTerminalArtifact).not.toHaveBeenCalled()
})
})
})
@@ -1,7 +1,7 @@
import { SshHostNotServedError } from '../providers/ssh-host-not-served-error'
import { describe, expect, it, vi } from 'vitest'
import { realpath } from 'node:fs/promises'
import { openMock, resolveAuthorizedPathMock, statMock } from './orca-runtime-files-mock-registry'
import { getSshFilesystemProvider } from '../providers/ssh-filesystem-dispatch'
import {
createRuntimeFileCommands,
useRuntimeFileCommandsLifecycle
@@ -30,10 +30,6 @@ vi.mock(
'../ipc/local-worktree-runtime-options',
async () => (await import('./orca-runtime-files-mock-registry')).localWorktreeRuntimeOptionsMock
)
vi.mock(
'../providers/ssh-filesystem-dispatch',
async () => (await import('./orca-runtime-files-mock-registry')).sshFilesystemDispatchMock
)
describe('RuntimeFileCommands', () => {
useRuntimeFileCommandsLifecycle()
@@ -182,7 +178,7 @@ describe('RuntimeFileCommands', () => {
})
})
it('opens host-qualified remote POSIX terminal links when the source terminal verified the host', async () => {
it('refuses host-qualified terminal links on an SSH workspace instead of reading this machine', async () => {
const resolveTerminalFileUriHostname = vi.fn(() => 'remote-host')
const hasRecentTerminalOutputPath = vi.fn(() => true)
const { commands, store } = createRuntimeFileCommands({
@@ -191,69 +187,25 @@ describe('RuntimeFileCommands', () => {
hasRecentTerminalOutputPath
})
store.getRepo.mockReturnValue({ connectionId: 'ssh-1' })
const stat = vi.fn().mockResolvedValue({ type: 'file', size: 2, mtime: 3 })
const realpath = vi.fn(async (p: string) => p)
vi.mocked(getSshFilesystemProvider).mockReturnValue({ stat, realpath } as never)
const result = await resolveTerminalArtifactPath(commands, '//remote-host/tmp/result.json')
expect(resolveTerminalFileUriHostname).toHaveBeenCalledWith('term-1')
expect(hasRecentTerminalOutputPath).toHaveBeenCalledWith(
'term-1',
'//remote-host/tmp/result.json',
'/tmp/result.json'
)
expect(stat).toHaveBeenCalledWith('/tmp/result.json')
expect(result).toMatchObject({
relativePath: null,
absolutePath: '/tmp/result.json',
exists: true,
openTarget: {
kind: 'absolute-file',
provider: 'ssh',
absolutePath: '/tmp/result.json'
}
})
})
it('opens host-qualified Windows SSH worktree file URLs with a drive path', async () => {
const resolveTerminalFileUriHostname = vi.fn(() => 'remote-host')
const { commands, store } = createRuntimeFileCommands({
path: 'C:/Users/me/repo',
resolveTerminalFileUriHostname
})
store.getRepo.mockReturnValue({ connectionId: 'ssh-1' })
const stat = vi.fn().mockResolvedValue({ type: 'file', size: 2, mtime: 3 })
const realpath = vi.fn(async (p: string) => p)
vi.mocked(getSshFilesystemProvider).mockReturnValue({ stat, realpath } as never)
const result = await commands.resolveTerminalPath(
'id:wt-1',
'//remote-host/C:/Users/me/repo/src/app.ts',
null,
'client-a',
'term-1'
)
expect(result).toMatchObject({
relativePath: 'src/app.ts',
absolutePath: 'C:/Users/me/repo/src/app.ts',
exists: true,
openTarget: {
kind: 'worktree-file',
provider: 'ssh',
relativePath: 'src/app.ts',
absolutePath: 'C:/Users/me/repo/src/app.ts'
}
})
await expect(
resolveTerminalArtifactPath(commands, '//remote-host/tmp/result.json')
).rejects.toThrow(SshHostNotServedError)
await expect(
commands.resolveTerminalPath(
'id:wt-1',
'//remote-host/C:/Users/me/repo/src/app.ts',
null,
'client-a',
'term-1'
)
).rejects.toThrow(SshHostNotServedError)
expect(hasRecentTerminalOutputPath).not.toHaveBeenCalled()
})
it('rejects host-qualified remote POSIX terminal links without a verified host match', async () => {
const { commands, store } = createRuntimeFileCommands({ path: '/home/me/repo' })
store.getRepo.mockReturnValue({ connectionId: 'ssh-1' })
const stat = vi.fn().mockResolvedValue({ type: 'file', size: 2, mtime: 3 })
const realpath = vi.fn(async (p: string) => p)
vi.mocked(getSshFilesystemProvider).mockReturnValue({ stat, realpath } as never)
const result = await commands.resolveTerminalPath(
'id:wt-1',
@@ -262,7 +214,7 @@ describe('RuntimeFileCommands', () => {
'client-a'
)
expect(stat).not.toHaveBeenCalled()
expect(statMock).not.toHaveBeenCalled()
expect(result).toEqual({
worktree: 'wt-1',
relativePath: null,
@@ -1,3 +1,4 @@
import { SshHostNotServedError } from '../providers/ssh-host-not-served-error'
import { describe, expect, it, vi } from 'vitest'
import { resolveAuthorizedPathMock, statMock } from './orca-runtime-files-mock-registry'
import {
@@ -8,7 +9,6 @@ import {
resolveTerminalArtifactPath,
statAsFile
} from './orca-runtime-files-terminal-artifact-fixtures'
import { getSshFilesystemProvider } from '../providers/ssh-filesystem-dispatch'
vi.mock('fs', async () => (await import('./orca-runtime-files-mock-registry')).fsModuleMock())
vi.mock('fs/promises', async () =>
@@ -28,10 +28,6 @@ vi.mock(
'../ipc/local-worktree-runtime-options',
async () => (await import('./orca-runtime-files-mock-registry')).localWorktreeRuntimeOptionsMock
)
vi.mock(
'../providers/ssh-filesystem-dispatch',
async () => (await import('./orca-runtime-files-mock-registry')).sshFilesystemDispatchMock
)
describe('RuntimeFileCommands', () => {
useRuntimeFileCommandsLifecycle()
@@ -173,99 +169,14 @@ describe('RuntimeFileCommands', () => {
expect(hasRecentTerminalOutputPath).not.toHaveBeenCalled()
})
it('stats a sibling SSH workspace through its owning provider', async () => {
const sibling = {
id: 'wt-2',
repoId: 'repo-2',
path: '/sibling',
hostId: 'ssh:ssh-1',
git: {
path: '/sibling',
head: '',
branch: '',
isBare: false,
isMainWorktree: true
}
}
const resolveKnownWorkspaceFileTarget = vi.fn(async () => ({
worktree: sibling,
executionHostId: 'ssh:ssh-1',
relativePath: 'docs/readme.md'
}))
const { commands, store } = createRuntimeFileCommands({
path: '/repo',
resolveKnownWorkspaceFileTarget
})
it('refuses an SSH workspace path instead of stating this machine', async () => {
const { commands, store } = createRuntimeFileCommands({ path: '/repo' })
store.getRepo.mockReturnValue({ connectionId: 'ssh-1' })
const remoteStat = vi.fn().mockResolvedValue({ type: 'file', size: 12, mtime: 3 })
vi.mocked(getSshFilesystemProvider).mockReturnValue({ stat: remoteStat } as never)
const result = await commands.resolveTerminalPath(
'id:wt-1',
'/sibling/docs/readme.md',
null,
undefined,
null,
true
await expect(commands.resolveTerminalPath('id:wt-1', 'src/app.ts')).rejects.toThrow(
SshHostNotServedError
)
expect(resolveKnownWorkspaceFileTarget).toHaveBeenCalledWith(
'/sibling/docs/readme.md',
'ssh:ssh-1'
)
expect(remoteStat).toHaveBeenCalledWith('/sibling/docs/readme.md')
expect(statMock).not.toHaveBeenCalled()
expect(result).toMatchObject({
worktree: 'wt-2',
relativePath: 'docs/readme.md',
exists: true,
openTarget: { provider: 'ssh' }
})
})
it('stats an exact SSH sibling root as a directory through its owning provider', async () => {
const sibling = {
id: 'wt-2',
repoId: 'repo-2',
path: '/sibling',
hostId: 'ssh:ssh-1',
git: {
path: '/sibling',
head: '',
branch: '',
isBare: false,
isMainWorktree: true
}
}
const resolveKnownWorkspaceFileTarget = vi.fn(async () => ({
worktree: sibling,
executionHostId: 'ssh:ssh-1',
relativePath: ''
}))
const hasRecentTerminalOutputPath = vi.fn(() => true)
const { commands, store } = createRuntimeFileCommands({
path: '/repo',
resolveKnownWorkspaceFileTarget,
hasRecentTerminalOutputPath
})
store.getRepo.mockReturnValue({ connectionId: 'ssh-1' })
const remoteStat = vi.fn().mockResolvedValue({ type: 'directory', size: 0, mtime: 3 })
vi.mocked(getSshFilesystemProvider).mockReturnValue({ stat: remoteStat } as never)
const result = await resolveTerminalArtifactPath(commands, '/sibling', null, 'client-a', true)
expect(resolveKnownWorkspaceFileTarget).toHaveBeenCalledWith('/sibling', 'ssh:ssh-1')
expect(remoteStat).toHaveBeenCalledWith('/sibling')
expect(statMock).not.toHaveBeenCalled()
expect(result).toEqual({
worktree: 'wt-2',
relativePath: '',
absolutePath: '/sibling',
exists: true,
isDirectory: true,
openTarget: undefined
})
expect(hasRecentTerminalOutputPath).not.toHaveBeenCalled()
})
// The host was `runtime:env-a` until this process stopped dispatching runtime hosts at all
@@ -445,13 +356,11 @@ describe('RuntimeFileCommands', () => {
it('does not expand ~/ on a remote worktree (home is unknown)', async () => {
const { commands, store } = createRuntimeFileCommands({ path: '/repo' })
store.getRepo.mockReturnValue({ connectionId: 'ssh-1' })
const stat = vi.fn()
vi.mocked(getSshFilesystemProvider).mockReturnValue({ stat } as never)
const result = await commands.resolveTerminalPath('id:wt-1', '~/notes.md')
expect(result).toMatchObject({ relativePath: null, exists: false })
expect(stat).not.toHaveBeenCalled()
expect(statMock).not.toHaveBeenCalled()
})
it('still refuses a native-chat ~/ path on a remote worktree', async () => {
@@ -475,27 +384,5 @@ describe('RuntimeFileCommands', () => {
expect(result).toMatchObject({ relativePath: null, exists: false })
expect(hasRecentNativeChatOutputPath).not.toHaveBeenCalled()
})
it('reports a missing remote file as not existing', async () => {
const { commands, store } = createRuntimeFileCommands({ path: '/repo' })
store.getRepo.mockReturnValue({ connectionId: 'ssh-1' })
const stat = vi.fn().mockRejectedValue(new Error('ENOENT: no such file'))
vi.mocked(getSshFilesystemProvider).mockReturnValue({ stat } as never)
const result = await commands.resolveTerminalPath('id:wt-1', 'src/missing.ts')
expect(result).toMatchObject({ relativePath: 'src/missing.ts', exists: false })
})
it('rethrows a remote transport error instead of reporting not-found', async () => {
const { commands, store } = createRuntimeFileCommands({ path: '/repo' })
store.getRepo.mockReturnValue({ connectionId: 'ssh-1' })
const stat = vi.fn().mockRejectedValue(new Error('Remote connection dropped'))
vi.mocked(getSshFilesystemProvider).mockReturnValue({ stat } as never)
await expect(commands.resolveTerminalPath('id:wt-1', 'src/x.ts')).rejects.toThrow(
'Remote connection dropped'
)
})
})
})
@@ -1,8 +1,7 @@
import { describe, expect, it, vi } from 'vitest'
import { SshHostNotServedError } from '../providers/ssh-host-not-served-error'
import { EventEmitter } from 'node:events'
import type { FsChangeEvent } from '../../shared/filesystem-entry-types'
import {
getSshFilesystemProviderMock,
resolveAuthorizedPathMock,
statMock,
watchInWatcherProcessMock,
@@ -12,7 +11,6 @@ import {
createRuntimeFileCommands,
useRuntimeFileCommandsLifecycle
} from './orca-runtime-files-test-harness'
import { getSshFilesystemProvider } from '../providers/ssh-filesystem-dispatch'
import { awaitRuntimeFileWatcherUnsubscribes } from './orca-runtime-files'
vi.mock('fs', async () => (await import('./orca-runtime-files-mock-registry')).fsModuleMock())
@@ -33,10 +31,6 @@ vi.mock(
'../ipc/local-worktree-runtime-options',
async () => (await import('./orca-runtime-files-mock-registry')).localWorktreeRuntimeOptionsMock
)
vi.mock(
'../providers/ssh-filesystem-dispatch',
async () => (await import('./orca-runtime-files-mock-registry')).sshFilesystemDispatchMock
)
describe('RuntimeFileCommands', () => {
useRuntimeFileCommandsLifecycle()
@@ -102,80 +96,14 @@ describe('RuntimeFileCommands', () => {
expect(dispose).toHaveBeenCalledTimes(1)
})
it('keeps SSH runtime watches on the remote filesystem provider', async () => {
const remoteDispose = vi.fn()
const providerWatch = vi.fn(
(_rootPath: string, _callback: (events: FsChangeEvent[]) => void) => remoteDispose
)
vi.mocked(getSshFilesystemProvider).mockReturnValue({ watch: providerWatch } as never)
it('refuses an SSH runtime watch without watching this machine', async () => {
const { commands, store } = createRuntimeFileCommands({ path: '/remote/repo' })
store.getRepo.mockReturnValue({ connectionId: 'ssh-1' })
const onEvents = vi.fn()
const unsubscribe = await commands.watchFileExplorer('id:wt-1', onEvents)
expect(providerWatch).toHaveBeenCalledWith('/remote/repo', expect.any(Function), {
signal: undefined,
onTerminalError: expect.any(Function)
})
const callback = providerWatch.mock.calls[0]?.[1]
callback?.([{ kind: 'update', absolutePath: '/remote/repo/current.ts' }])
expect(onEvents).toHaveBeenCalledWith([
{ kind: 'update', absolutePath: '/remote/repo/current.ts' }
])
await expect(commands.watchFileExplorer('id:wt-1', vi.fn())).rejects.toThrow(
SshHostNotServedError
)
expect(watchInWatcherProcessMock).not.toHaveBeenCalled()
await unsubscribe()
expect(remoteDispose).toHaveBeenCalledTimes(1)
})
it('indexes SSH runtime watches so remote deletion can await them', async () => {
let resolveDispose: () => void = () => {}
const remoteDispose = vi.fn(
() =>
new Promise<void>((resolve) => {
resolveDispose = resolve
})
)
vi.mocked(getSshFilesystemProvider).mockReturnValue({
watch: vi.fn(() => remoteDispose)
} as never)
const { commands, store } = createRuntimeFileCommands({ path: '/remote/repo' })
store.getRepo.mockReturnValue({ connectionId: 'ssh-1' })
await commands.watchFileExplorer('id:wt-1', vi.fn())
let closed = false
const close = commands.closeFileExplorerWatchersForPath('/remote/repo', 'ssh-1').then(() => {
closed = true
})
await Promise.resolve()
expect(remoteDispose).toHaveBeenCalledTimes(1)
expect(closed).toBe(false)
resolveDispose()
await close
})
it('scopes same-path runtime watcher teardown to its SSH execution host', async () => {
const firstDispose = vi.fn()
const secondDispose = vi.fn()
const firstProvider = { watch: vi.fn(() => firstDispose) }
const secondProvider = { watch: vi.fn(() => secondDispose) }
getSshFilesystemProviderMock.mockImplementation((connectionId) =>
connectionId === 'ssh-1' ? firstProvider : secondProvider
)
const first = createRuntimeFileCommands({ path: '/same/repo' })
const second = createRuntimeFileCommands({ path: '/same/repo' })
first.store.getRepo.mockReturnValue({ connectionId: 'ssh-1' })
second.store.getRepo.mockReturnValue({ connectionId: 'ssh-2' })
await first.commands.watchFileExplorer('id:wt-1', vi.fn())
await second.commands.watchFileExplorer('id:wt-1', vi.fn())
await first.commands.closeFileExplorerWatchersForPath('/same/repo', 'ssh-1')
expect(firstDispose).toHaveBeenCalledTimes(1)
expect(secondDispose).not.toHaveBeenCalled()
await second.commands.closeFileExplorerWatchersForPath('/same/repo', 'ssh-2')
expect(secondDispose).toHaveBeenCalledTimes(1)
expect(watchMock).not.toHaveBeenCalled()
})
})
@@ -4,7 +4,6 @@ import type * as Fs from 'node:fs'
import type * as FsPromises from 'node:fs/promises'
import type * as FilesystemAuth from '../ipc/filesystem-auth'
import type { FsChangeEvent } from '../../shared/filesystem-entry-types'
import type { IFilesystemProvider } from '../providers/types'
import { WatcherProcessFailure } from '../ipc/parcel-watcher-process-failure'
import { acquireWatcherRemovalGate } from '../ipc/watcher-removal-gate'
@@ -13,15 +12,13 @@ const {
statMock,
watchMock,
watchInWatcherProcessMock,
closeWatcherInWatcherProcessMock,
getSshFilesystemProviderMock
closeWatcherInWatcherProcessMock
} = vi.hoisted(() => ({
resolveAuthorizedPathMock: vi.fn(),
statMock: vi.fn(),
watchMock: vi.fn(),
watchInWatcherProcessMock: vi.fn(),
closeWatcherInWatcherProcessMock: vi.fn(),
getSshFilesystemProviderMock: vi.fn()
closeWatcherInWatcherProcessMock: vi.fn()
}))
vi.mock('fs', async () => {
@@ -54,11 +51,6 @@ vi.mock('../ipc/filesystem-auth', async () => {
}
})
vi.mock('../providers/ssh-filesystem-dispatch', () => ({
getSshFilesystemProvider: getSshFilesystemProviderMock,
onSshFilesystemProviderRegistered: () => () => undefined
}))
import {
_getRuntimeFileWatcherReleaseCountForTests,
_resetRuntimeFileWatcherLeasesForTests,
@@ -109,7 +101,6 @@ describe('RuntimeFileCommands file watching', () => {
watchMock.mockReset()
watchInWatcherProcessMock.mockReset()
closeWatcherInWatcherProcessMock.mockReset()
getSshFilesystemProviderMock.mockReset()
Object.defineProperty(process, 'platform', {
configurable: true,
value: originalPlatform
@@ -543,63 +534,4 @@ describe('RuntimeFileCommands file watching', () => {
await drainPromise
expect(drained).toBe(true)
})
it.each(['abort', 'unsubscribe'] as const)(
'forwards the SSH watch signal and fences terminal errors after %s',
async (stop) => {
const dispose = vi.fn()
const watch = vi.fn<IFilesystemProvider['watch']>().mockResolvedValue(dispose)
getSshFilesystemProviderMock.mockReturnValue({ watch })
const store = { getRepo: vi.fn(() => ({ connectionId: 'ssh-1' })) }
const commands = new RuntimeFileCommands({
getRuntimeId: () => 'runtime-1',
requireStore: () => store,
resolveWorktreeSelector: vi.fn(async () => ({
id: 'wt-1',
repoId: 'repo-1',
path: '/remote/repo'
})),
resolveRuntimeFileTarget: vi.fn(async () => ({
worktree: {
id: 'wt-1',
repoId: 'repo-1',
path: '/remote/repo'
},
executionHostId: 'ssh:ssh-1'
})),
resolveRuntimeGitTarget: vi.fn(),
openFile: vi.fn()
} as never)
const controller = new AbortController()
const onTerminalError = vi.fn()
const unsubscribe = await commands.watchFileExplorer(
'id:wt-1',
vi.fn(),
onTerminalError,
controller.signal
)
expect(watch).toHaveBeenCalledWith('/remote/repo', expect.any(Function), {
signal: controller.signal,
onTerminalError: expect.any(Function)
})
const options = watch.mock.calls[0]?.[2]
expect(options?.signal).toBe(controller.signal)
const currentError = new Error('current remote watch failed')
options?.onTerminalError?.(currentError)
expect(onTerminalError).toHaveBeenCalledExactlyOnceWith(currentError)
if (stop === 'abort') {
controller.abort()
} else {
await unsubscribe()
expect(controller.signal.aborted).toBe(false)
}
options?.onTerminalError?.(new Error('late remote watch failure'))
expect(onTerminalError).toHaveBeenCalledExactlyOnceWith(currentError)
await unsubscribe()
expect(dispose).toHaveBeenCalledOnce()
expect(watchInWatcherProcessMock).not.toHaveBeenCalled()
}
)
})
@@ -2,11 +2,9 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'
import type { GlobalSettings } from '../../shared/global-settings-types'
import { RuntimeGitCommands, type ResolvedRuntimeGitWorktree } from './orca-runtime-git'
const getSshGitProviderMock = vi.hoisted(() => vi.fn())
const getBranchDiffMock = vi.hoisted(() => vi.fn())
vi.mock('../providers/ssh-git-dispatch', () => ({
getSshGitProvider: getSshGitProviderMock
}))
vi.mock('../git/status', () => ({ getBranchDiff: getBranchDiffMock }))
function makeWorktree(path: string): ResolvedRuntimeGitWorktree {
const worktree = {
@@ -27,10 +25,10 @@ function makeWorktree(path: string): ResolvedRuntimeGitWorktree {
describe('RuntimeGitCommands branch diff', () => {
beforeEach(() => {
getSshGitProviderMock.mockReset()
getBranchDiffMock.mockReset()
})
it('forwards the pinned head through normalized nested SSH paths', async () => {
it('forwards the pinned head through normalized nested paths', async () => {
const mergeBase = 'a'.repeat(40)
const headOid = 'b'.repeat(40)
const result = {
@@ -40,12 +38,11 @@ describe('RuntimeGitCommands branch diff', () => {
originalIsBinary: false,
modifiedIsBinary: false
}
const provider = { getBranchDiff: vi.fn().mockResolvedValue([result]) }
getSshGitProviderMock.mockReturnValue(provider)
getBranchDiffMock.mockResolvedValue(result)
const commands = new RuntimeGitCommands({
resolveRuntimeGitTarget: async () => ({
worktree: makeWorktree('/remote/repo'),
executionHostId: 'ssh:conn-1'
worktree: makeWorktree('/repo'),
executionHostId: 'local'
}),
getRuntimeSettings: () => ({}) as GlobalSettings
})
@@ -59,11 +56,10 @@ describe('RuntimeGitCommands branch diff', () => {
)
).resolves.toEqual(result)
expect(provider.getBranchDiff).toHaveBeenCalledWith('/remote/repo', mergeBase, {
includePatch: true,
headOid,
filePath: 'src/file.ts',
oldPath: 'src/old-file.ts'
})
expect(getBranchDiffMock).toHaveBeenCalledWith(
'/repo',
{ mergeBase, headOid, filePath: 'src/file.ts', oldPath: 'src/old-file.ts' },
{ admissionTier: 'interactive' }
)
})
})
@@ -1,5 +1,4 @@
// Why: the cap lives in orca-runtime-git.ts so both branches of all three diff readers are covered —
// an SSH host forwards its provider's payload verbatim, so an older relay cannot be relied on to clamp it.
// Why: the cap lives in orca-runtime-git.ts so all three diff readers are covered.
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { REMOTE_RPC_MAX_CONTENT_BYTES } from '../../shared/remote-rpc-content-budget'
import type { GitDiffResult } from '../../shared/git-diff-compare-types'
@@ -8,16 +7,11 @@ import type * as GitStatusModule from '../git/status'
import { RuntimeGitCommands, type ResolvedRuntimeGitWorktree } from './orca-runtime-git'
const mocks = vi.hoisted(() => ({
getSshGitProvider: vi.fn(),
getDiff: vi.fn(),
getBranchDiff: vi.fn(),
getCommitDiff: vi.fn()
}))
vi.mock('../providers/ssh-git-dispatch', () => ({
getSshGitProvider: mocks.getSshGitProvider
}))
vi.mock('../git/status', async () => ({
...(await vi.importActual<typeof GitStatusModule>('../git/status')),
getDiff: mocks.getDiff,
@@ -44,10 +38,7 @@ function oversizedResult(): GitDiffResult {
}
}
function commands(
connectionId?: string,
localGitOptions?: { wslDistro: string }
): RuntimeGitCommands {
function commands(localGitOptions?: { wslDistro: string }): RuntimeGitCommands {
const worktree = {
id: 'wt-1',
repoId: 'repo-1',
@@ -57,63 +48,20 @@ function commands(
return new RuntimeGitCommands({
resolveRuntimeGitTarget: async () => ({
worktree,
executionHostId: connectionId ? (`ssh:${connectionId}` as const) : ('local' as const),
executionHostId: 'local' as const,
...(localGitOptions ? { localGitOptions } : {})
}),
getRuntimeSettings: () => ({}) as GlobalSettings
})
}
function sshProvider(): {
getDiff: ReturnType<typeof vi.fn>
getBranchDiff: ReturnType<typeof vi.fn>
getCommitDiff: ReturnType<typeof vi.fn>
} {
return {
getDiff: vi.fn().mockResolvedValue(oversizedResult()),
getBranchDiff: vi.fn().mockResolvedValue([oversizedResult()]),
getCommitDiff: vi.fn().mockResolvedValue(oversizedResult())
}
}
describe('runtime git diff transport budget', () => {
beforeEach(() => {
mocks.getSshGitProvider.mockReset()
mocks.getDiff.mockReset().mockResolvedValue(oversizedResult())
mocks.getBranchDiff.mockReset().mockResolvedValue(oversizedResult())
mocks.getCommitDiff.mockReset().mockResolvedValue(oversizedResult())
})
it('caps an SSH-forwarded diff that exceeds the budget', async () => {
const provider = sshProvider()
mocks.getSshGitProvider.mockReturnValue(provider)
await expect(
commands('conn-1').getRuntimeGitDiff(
'id:wt-1',
'assets/logo.png',
false,
undefined,
REMOTE_RPC_MAX_CONTENT_BYTES
)
).rejects.toMatchObject(TOO_LARGE)
expect(provider.getDiff).toHaveBeenCalledWith(
'/remote/repo',
'assets/logo.png',
false,
undefined
)
expect(mocks.getDiff).not.toHaveBeenCalled()
})
it('leaves an SSH-forwarded diff uncapped when no budget is supplied', async () => {
mocks.getSshGitProvider.mockReturnValue(sshProvider())
await expect(
commands('conn-1').getRuntimeGitDiff('id:wt-1', 'assets/logo.png', false)
).resolves.toMatchObject({ modifiedContent: OVERSIZED_BASE64 })
})
it('caps a local-repo diff that exceeds the budget', async () => {
await expect(
commands().getRuntimeGitDiff(
@@ -125,7 +73,6 @@ describe('runtime git diff transport budget', () => {
)
).rejects.toMatchObject(TOO_LARGE)
expect(mocks.getDiff).toHaveBeenCalled()
expect(mocks.getSshGitProvider).not.toHaveBeenCalled()
})
it('leaves a local-repo diff uncapped when no budget is supplied', async () => {
@@ -134,23 +81,6 @@ describe('runtime git diff transport budget', () => {
).resolves.toMatchObject({ modifiedContent: OVERSIZED_BASE64 })
})
it('caps an SSH-forwarded branch diff that exceeds the budget', async () => {
const provider = sshProvider()
mocks.getSshGitProvider.mockReturnValue(provider)
await expect(
commands('conn-1').getRuntimeGitBranchDiff(
'id:wt-1',
BRANCH_COMPARE,
'assets/logo.png',
undefined,
REMOTE_RPC_MAX_CONTENT_BYTES
)
).rejects.toMatchObject(TOO_LARGE)
expect(provider.getBranchDiff).toHaveBeenCalled()
expect(mocks.getBranchDiff).not.toHaveBeenCalled()
})
it('caps a local-repo branch diff that exceeds the budget', async () => {
await expect(
commands().getRuntimeGitBranchDiff(
@@ -170,21 +100,6 @@ describe('runtime git diff transport budget', () => {
).resolves.toMatchObject({ modifiedContent: OVERSIZED_BASE64 })
})
it('caps an SSH-forwarded commit diff that exceeds the budget', async () => {
const provider = sshProvider()
mocks.getSshGitProvider.mockReturnValue(provider)
await expect(
commands('conn-1').getRuntimeGitCommitDiff(
'id:wt-1',
COMMIT_ARGS,
REMOTE_RPC_MAX_CONTENT_BYTES
)
).rejects.toMatchObject(TOO_LARGE)
expect(provider.getCommitDiff).toHaveBeenCalled()
expect(mocks.getCommitDiff).not.toHaveBeenCalled()
})
it('caps a local-repo commit diff that exceeds the budget', async () => {
await expect(
commands().getRuntimeGitCommitDiff('id:wt-1', COMMIT_ARGS, REMOTE_RPC_MAX_CONTENT_BYTES)
@@ -201,7 +116,7 @@ describe('runtime git diff transport budget', () => {
})
it('prioritizes local file diff reads without losing WSL routing', async () => {
const runtime = commands(undefined, { wslDistro: 'Ubuntu' })
const runtime = commands({ wslDistro: 'Ubuntu' })
await runtime.getRuntimeGitBranchDiff('id:wt-1', BRANCH_COMPARE, 'assets/logo.png')
await runtime.getRuntimeGitCommitDiff('id:wt-1', COMMIT_ARGS)
+11 -170
View File
@@ -6,6 +6,10 @@ import type { GlobalSettings } from '../../shared/global-settings-types'
import type * as GitStatusModule from '../git/status'
import type * as CommitMessageTextGenerationModule from '../text-generation/commit-message-text-generation'
import type * as PullRequestContextModule from '../text-generation/pull-request-context'
import {
SSH_HOST_NOT_SERVED_MESSAGE,
SshHostNotServedError
} from '../providers/ssh-host-not-served-error'
import { RuntimeGitCommands, type ResolvedRuntimeGitWorktree } from './orca-runtime-git'
const mocks = vi.hoisted(() => ({
@@ -19,7 +23,6 @@ const mocks = vi.hoisted(() => ({
resolveCommitMessageSettings: vi.fn(),
resolveHostedReviewBodyForGeneration: vi.fn(),
loadPullRequestLinkedIssue: vi.fn(),
getSshGitProvider: vi.fn(),
getStatus: vi.fn()
}))
@@ -51,10 +54,6 @@ vi.mock('../text-generation/pull-request-context', async () => ({
getPullRequestDraftContext: mocks.getPullRequestDraftContext
}))
vi.mock('../providers/ssh-git-dispatch', () => ({
getSshGitProvider: mocks.getSshGitProvider
}))
vi.mock('../source-control/pull-request-template', () => ({
resolveHostedReviewBodyForGeneration: mocks.resolveHostedReviewBodyForGeneration
}))
@@ -108,7 +107,6 @@ describe('RuntimeGitCommands', () => {
mocks.resolveHostedReviewBodyForGeneration.mockImplementation(async ({ body }) => body)
mocks.loadPullRequestLinkedIssue.mockReset()
mocks.loadPullRequestLinkedIssue.mockResolvedValue(null)
mocks.getSshGitProvider.mockReset()
mocks.getStatus.mockReset()
mocks.checkoutBranch.mockReset()
mocks.discardChanges.mockReset()
@@ -144,9 +142,7 @@ describe('RuntimeGitCommands', () => {
})
})
it('does not resolve shared link paths for a remote runtime status', async () => {
const provider = { getStatus: vi.fn().mockResolvedValue({ entries: [] }) }
mocks.getSshGitProvider.mockReturnValue(provider)
it('refuses an SSH runtime status without running git here', async () => {
const commands = new RuntimeGitCommands({
resolveRuntimeGitTarget: async () => ({
worktree: makeWorktree('/remote/repo'),
@@ -156,9 +152,7 @@ describe('RuntimeGitCommands', () => {
getRuntimeSettings: () => ({}) as GlobalSettings
})
await commands.getRuntimeGitStatus('id:wt-1')
expect(provider.getStatus).toHaveBeenCalledWith('/remote/repo')
await expect(commands.getRuntimeGitStatus('id:wt-1')).rejects.toThrow(SshHostNotServedError)
expect(mocks.getStatus).not.toHaveBeenCalled()
})
@@ -186,26 +180,6 @@ describe('RuntimeGitCommands', () => {
})
})
it('checks out a remote branch through the SSH git provider', async () => {
const provider = { checkoutBranch: vi.fn().mockResolvedValue(undefined) }
mocks.getSshGitProvider.mockReturnValue(provider)
const commands = new RuntimeGitCommands({
resolveRuntimeGitTarget: async () => ({
worktree: makeWorktree('/remote/repo'),
executionHostId: 'ssh:conn-1'
}),
getRuntimeSettings: () => ({}) as GlobalSettings
})
await expect(commands.checkoutRuntimeGitBranch('id:wt-1', 'feature/x')).resolves.toEqual({
ok: true,
branch: 'feature/x'
})
expect(provider.checkoutBranch).toHaveBeenCalledWith('/remote/repo', 'feature/x')
expect(mocks.checkoutBranch).not.toHaveBeenCalled()
})
it('lists local branches through the resolved worktree', async () => {
const worktreePath = mkdtempSync(join(tmpdir(), 'orca-runtime-git-'))
tempDirs.push(worktreePath)
@@ -238,45 +212,6 @@ describe('RuntimeGitCommands', () => {
})
})
it('keeps a remote single-file discard owned by the SSH provider', async () => {
const provider = { discardChanges: vi.fn() }
mocks.getSshGitProvider.mockReturnValue(provider)
const commands = new RuntimeGitCommands({
resolveRuntimeGitTarget: async () => ({
worktree: makeWorktree('/remote/repo'),
executionHostId: 'ssh:conn-1'
}),
getRuntimeSettings: () => ({}) as GlobalSettings
})
await commands.discardRuntimeGitPath('id:wt-1', 'src/app.ts')
expect(provider.discardChanges).toHaveBeenCalledWith('/remote/repo', 'src/app.ts')
expect(mocks.discardChanges).not.toHaveBeenCalled()
})
it('lists remote local branches through the SSH git provider', async () => {
const provider = {
listLocalBranches: vi.fn().mockResolvedValue({ current: 'main', branches: ['main'] })
}
mocks.getSshGitProvider.mockReturnValue(provider)
const commands = new RuntimeGitCommands({
resolveRuntimeGitTarget: async () => ({
worktree: makeWorktree('/remote/repo'),
executionHostId: 'ssh:conn-1'
}),
getRuntimeSettings: () => ({}) as GlobalSettings
})
await expect(commands.listRuntimeGitLocalBranches('id:wt-1')).resolves.toEqual({
current: 'main',
branches: ['main']
})
expect(provider.listLocalBranches).toHaveBeenCalledWith('/remote/repo')
expect(mocks.listLocalBranches).not.toHaveBeenCalled()
})
it('rejects slash-only git mutation paths before they can target the worktree root', async () => {
const worktreePath = mkdtempSync(join(tmpdir(), 'orca-runtime-git-'))
tempDirs.push(worktreePath)
@@ -582,24 +517,10 @@ describe('RuntimeGitCommands', () => {
)
})
it('resolves remote commit-message settings against the SSH host cache', async () => {
it('resolves SSH commit-message settings against the host cache, then reports it unserved', async () => {
const worktreePath = '/remote/repo'
const context = {
branch: 'main',
stagedSummary: 'M\tREADME.md',
stagedPatch: '+hello'
}
const params = { agentId: 'cursor', model: 'remote-model' }
mocks.resolveCommitMessageSettings.mockReturnValue({ ok: true, params })
mocks.generateCommitMessageFromContext.mockResolvedValue({
success: true,
message: 'docs: update remote readme'
})
const provider = {
getStagedCommitContext: vi.fn().mockResolvedValue(context),
executeCommitMessagePlan: vi.fn()
}
mocks.getSshGitProvider.mockReturnValue(provider)
const commands = new RuntimeGitCommands({
resolveRuntimeGitTarget: async () => ({
worktree: makeWorktree(worktreePath),
@@ -616,8 +537,8 @@ describe('RuntimeGitCommands', () => {
})
await expect(commands.generateRuntimeCommitMessage('id:wt-1')).resolves.toEqual({
success: true,
message: 'docs: update remote readme'
success: false,
error: SSH_HOST_NOT_SERVED_MESSAGE
})
expect(mocks.resolveCommitMessageSettings).toHaveBeenCalledWith(
@@ -626,14 +547,8 @@ describe('RuntimeGitCommands', () => {
'commitMessage',
null
)
expect(mocks.generateCommitMessageFromContext).toHaveBeenCalledWith(
context,
params,
expect.objectContaining({
kind: 'remote',
cwd: worktreePath
})
)
expect(mocks.getStagedCommitContext).not.toHaveBeenCalled()
expect(mocks.generateCommitMessageFromContext).not.toHaveBeenCalled()
})
it('enriches the local commit context with the workspace linked issue', async () => {
@@ -658,33 +573,6 @@ describe('RuntimeGitCommands', () => {
)
})
it('enriches the SSH commit context with the workspace linked issue', async () => {
const worktreePath = '/home/tester/wt'
const context = { branch: 'main', stagedSummary: 'M\tREADME.md', stagedPatch: '+hello' }
const params = { agentId: 'cursor', model: 'remote-model' }
mocks.resolveCommitMessageSettings.mockReturnValue({ ok: true, params })
mocks.generateCommitMessageFromContext.mockResolvedValue({ success: true, message: 'docs' })
mocks.getSshGitProvider.mockReturnValue({
getStagedCommitContext: vi.fn().mockResolvedValue(context),
executeCommitMessagePlan: vi.fn()
})
const commands = new RuntimeGitCommands({
resolveRuntimeGitTarget: async () => ({
worktree: makeWorktree(worktreePath, 77),
executionHostId: 'ssh:conn-1'
}),
getRuntimeSettings: () => ({}) as GlobalSettings
})
await commands.generateRuntimeCommitMessage('id:wt-1')
expect(mocks.generateCommitMessageFromContext).toHaveBeenCalledWith(
{ ...context, linkedIssue: 77 },
params,
expect.objectContaining({ kind: 'remote' })
)
})
it('prefers live meta over the linked issue projected onto the resolved worktree', async () => {
const worktreePath = mkdtempSync(join(tmpdir(), 'orca-runtime-git-'))
tempDirs.push(worktreePath)
@@ -806,53 +694,6 @@ describe('RuntimeGitCommands', () => {
)
})
it('shares one linked-issue attach across both pull-request branches', async () => {
const context = {
base: 'main',
branch: 'feature/login',
commitSummary: 'abc123 feat: test',
changeSummary: 'M README.md',
patch: '+hello',
currentTitle: '',
currentBody: '',
currentDraft: false
}
const params = { agentId: 'codex' as const, model: 'gpt-5.5' }
mocks.generatePullRequestFieldsFromContext.mockResolvedValue({ success: true, fields: {} })
mocks.getSshGitProvider.mockReturnValue({
exec: vi.fn(),
executeCommitMessagePlan: vi.fn()
})
for (const connectionId of [undefined, 'conn-1']) {
const worktreePath = connectionId
? '/home/tester/wt'
: mkdtempSync(join(tmpdir(), 'orca-runtime-git-'))
if (!connectionId) {
tempDirs.push(worktreePath)
}
mocks.getPullRequestDraftContext.mockResolvedValue(context)
const commands = new RuntimeGitCommands({
resolveRuntimeGitTarget: async () => ({
worktree: makeWorktree(worktreePath, 55),
executionHostId: connectionId ? (`ssh:${connectionId}` as const) : ('local' as const)
}),
getRuntimeSettings: () => ({}) as GlobalSettings
})
await commands.generateRuntimePullRequestFields(
'id:wt-1',
{ base: 'main', title: '', body: '', draft: false },
{ sourceControlAiResolvedParams: params }
)
}
expect(mocks.generatePullRequestFieldsFromContext.mock.calls).toHaveLength(2)
for (const call of mocks.generatePullRequestFieldsFromContext.mock.calls) {
expect(call[0]).toEqual({ ...context, linkedIssue: 55 })
}
})
it('leaves the pull-request context untouched when no issue is linked', async () => {
const worktreePath = mkdtempSync(join(tmpdir(), 'orca-runtime-git-'))
tempDirs.push(worktreePath)
@@ -28,7 +28,6 @@ import {
scanWorkspacePortsOnExecutionHost,
type WorkspacePortExecutionHostDeps
} from '../ports/workspace-port-execution-host'
import { getActiveMultiplexer } from '../ssh/ssh-target-registry'
import { resolveWorktreeHostRouting } from './worktree-launch-host-repo'
import { LOCAL_EXECUTION_HOST_ID } from '../../shared/execution-host'
@@ -178,8 +177,7 @@ export class OrcaRuntimeWithListManagedWorktrees extends OrcaRuntimeWithRestoreS
protected getWorkspacePortHostDeps(): WorkspacePortExecutionHostDeps {
return {
getLocalProbes: () => this.getWorkspacePortProbes(),
getSshMultiplexer: getActiveMultiplexer
getLocalProbes: () => this.getWorkspacePortProbes()
}
}
@@ -1,5 +1,5 @@
import { describe, expect, it, vi } from 'vitest'
import { getSshFilesystemProviderMock } from './orca-runtime-files-mock-registry'
import { readdirMock, wslAwareSpawnMock } from './orca-runtime-files-mock-registry'
import {
createRuntimeFileCommands,
useRuntimeFileCommandsLifecycle
@@ -26,214 +26,28 @@ vi.mock(
vi.mock('../ripgrep/bundled-ripgrep-path', async () =>
(await import('./orca-runtime-files-mock-registry')).bundledRipgrepPathModuleMock()
)
vi.mock(
'../providers/ssh-filesystem-dispatch',
async () => (await import('./orca-runtime-files-mock-registry')).sshFilesystemDispatchMock
)
function remoteSearch(version: number | null) {
const latePath = 'late/target.ts'
const legacy = Array.from({ length: 32 }, (_, i) => `early/unrelated${i}.ts`)
const listFiles = vi.fn(async (_root: string, options?: { searchQuery?: string }) =>
options?.searchQuery ? [latePath] : legacy
)
const supportsQuickOpenSearch = vi.fn(
async (options?: { minimumVersion?: number; signal?: AbortSignal }) => {
options?.signal?.throwIfAborted()
return version !== null && version >= (options?.minimumVersion ?? 3)
}
)
getSshFilesystemProviderMock.mockReturnValue({
listFiles,
...(version === null ? {} : { supportsQuickOpenSearch })
})
return {
...createRuntimeFileCommands({ hostId: 'ssh:ssh-1' }),
listFiles,
supportsQuickOpenSearch
}
}
describe('runtime SSH Quick Open capability requirements', () => {
// Why: this process serves no SSH filesystem, so Quick Open on an `ssh:` workspace reports no
// matches (version 0) instead of failing the keystroke, and never lists this machine.
describe('runtime SSH Quick Open', () => {
useRuntimeFileCommandsLifecycle()
it.each([1, 2, 3, 4])(
'uses host search for a simple late match on version %i',
async (version) => {
const { commands, listFiles, supportsQuickOpenSearch } = remoteSearch(version)
const controller = new AbortController()
await expect(
commands.searchQuickOpenFilePaths('id:wt-1', 'target', 7, ['nested'], controller.signal)
).resolves.toMatchObject({ files: [{ relativePath: 'late/target.ts' }], truncated: false })
expect(supportsQuickOpenSearch).toHaveBeenCalledWith({
signal: controller.signal,
minimumVersion: 1
})
expect(listFiles).toHaveBeenCalledWith('/repo', {
excludePaths: ['nested'],
maxResults: 8,
searchQuery: 'target',
signal: controller.signal
})
}
)
it('reports no matches and capability 0 without listing this machine', async () => {
const { commands } = createRuntimeFileCommands({ hostId: 'ssh:ssh-1' })
it.each([0, null])(
'keeps the bounded compatibility prefix only without v1 (%s)',
async (version) => {
const { commands, listFiles } = remoteSearch(version)
await expect(
commands.searchQuickOpenFilePaths('id:wt-1', 'target', 7)
).resolves.toMatchObject({
files: [],
truncated: true
})
expect(listFiles).toHaveBeenCalledWith('/repo', {
excludePaths: undefined,
maxResults: 32,
signal: undefined
})
}
)
it.each(['package-lock', 'my_file', 'file name'])(
'preserves existing filename search %s on older relays',
async (query) => {
for (const version of [1, 2, 3]) {
const { commands, listFiles } = remoteSearch(version)
await expect(commands.searchQuickOpenFilePaths('id:wt-1', query, 7)).resolves.toMatchObject(
{
files: [{ relativePath: 'late/target.ts' }]
}
)
expect(listFiles).toHaveBeenCalledWith(
'/repo',
expect.objectContaining({ searchQuery: query, maxResults: 8 })
)
}
}
)
it('does not require v3 for whitespace only around a simple query', async () => {
const { commands, supportsQuickOpenSearch } = remoteSearch(1)
await commands.searchQuickOpenFilePaths('id:wt-1', ' target ', 7)
expect(supportsQuickOpenSearch).toHaveBeenCalledWith({ signal: undefined, minimumVersion: 1 })
})
it.each([{ includeIgnored: false }, { followSymlinks: true }])(
'requires v2 for discovery %j',
async (options) => {
for (const version of [null, 0, 1]) {
const { commands, listFiles } = remoteSearch(version)
await expect(
commands.searchQuickOpenFilePaths('id:wt-1', 'target', 7, undefined, undefined, options)
).rejects.toThrow('listing options')
expect(listFiles).not.toHaveBeenCalled()
}
const { commands, listFiles } = remoteSearch(2)
await commands.searchQuickOpenFilePaths('id:wt-1', 'target', 7, undefined, undefined, options)
expect(listFiles).toHaveBeenCalledWith(
'/repo',
expect.objectContaining({ ...options, searchQuery: 'target', maxResults: 8 })
)
}
)
it('forwards abort to the capability probe and starts no listing after rejection', async () => {
const { commands, listFiles } = remoteSearch(1)
const controller = new AbortController()
controller.abort(new Error('abandoned'))
await expect(commands.searchQuickOpenFilePaths('id:wt-1', 'target', 7)).resolves.toMatchObject({
files: [],
totalCount: 0,
truncated: false,
quickOpenSearchVersion: 0
})
await expect(
commands.searchQuickOpenFilePaths('id:wt-1', 'target', 7, undefined, controller.signal)
).rejects.toThrow('abandoned')
expect(listFiles).not.toHaveBeenCalled()
})
})
describe('runtime SSH file-list capability requirements', () => {
useRuntimeFileCommandsLifecycle()
it.each([{ includeIgnored: false }, { followSymlinks: true }])(
'requires discovery v2 for %j',
async (options) => {
for (const version of [null, 0, 1]) {
const { commands, listFiles } = remoteSearch(version)
await expect(commands.listRuntimeFiles('id:wt-1', options)).rejects.toThrow(
'listing options'
)
expect(listFiles).not.toHaveBeenCalled()
}
for (const version of [2, 3, 4]) {
const { commands, listFiles, supportsQuickOpenSearch } = remoteSearch(version)
const controller = new AbortController()
await commands.listRuntimeFiles('id:wt-1', {
...options,
signal: controller.signal,
maxResults: 9
})
expect(supportsQuickOpenSearch).toHaveBeenCalledWith({
minimumVersion: 2,
signal: controller.signal
})
expect(listFiles).toHaveBeenCalledWith(
'/repo',
expect.objectContaining({ ...options, signal: controller.signal, maxResults: 9 })
)
}
}
)
it('requires v3 for candidate validation even with default discovery settings', async () => {
for (const version of [null, 0, 1, 2]) {
const { commands, listFiles } = remoteSearch(version)
await expect(
commands.listRuntimeFiles('id:wt-1', { candidatePaths: ['recent.ts'] })
).rejects.toThrow('validate Quick Open recent files')
expect(listFiles).not.toHaveBeenCalled()
}
const { commands, supportsQuickOpenSearch, listFiles } = remoteSearch(3)
await commands.listRuntimeFiles('id:wt-1', {
candidatePaths: ['recent.ts'],
includeIgnored: false
commands.listRuntimeFiles('id:wt-1', { candidatePaths: ['recent.ts'], followSymlinks: true })
).resolves.toEqual([])
await expect(commands.listMobileFiles('id:wt-1')).resolves.toMatchObject({
files: [],
totalCount: 0
})
expect(supportsQuickOpenSearch).toHaveBeenCalledWith({ minimumVersion: 3, signal: undefined })
expect(listFiles).toHaveBeenCalledWith(
'/repo',
expect.objectContaining({ candidatePaths: ['recent.ts'], includeIgnored: false })
)
})
it('keeps default legacy listings and disconnected results unchanged', async () => {
const { commands, supportsQuickOpenSearch, listFiles } = remoteSearch(0)
await commands.listRuntimeFiles('id:wt-1', { includeIgnored: true, followSymlinks: false })
expect(listFiles).toHaveBeenCalledOnce()
expect(supportsQuickOpenSearch).not.toHaveBeenCalled()
getSshFilesystemProviderMock.mockReturnValue(null)
await expect(commands.listRuntimeFiles('id:wt-1', { followSymlinks: true })).resolves.toEqual(
[]
)
expect(readdirMock).not.toHaveBeenCalled()
expect(wslAwareSpawnMock).not.toHaveBeenCalled()
})
})
it.each([0, 1, 2, 3, 4])(
'reports execution relay matching capability %i through a paired runtime',
async (version) => {
const { commands } = remoteSearch(version)
const result = await commands.searchQuickOpenFilePaths('id:wt-1', '', 7)
expect(result.quickOpenSearchVersion).toBe(Math.min(version, 3))
}
)
it('keeps inherited ignored-file visibility from breaking searches on an older relay', async () => {
const { commands, listFiles } = remoteSearch(1)
await expect(
commands.searchQuickOpenFilePaths('id:wt-1', 'target', 7, undefined, undefined, {
includeIgnored: false,
allowLegacyIncludeIgnored: true
})
).resolves.toMatchObject({ files: [{ relativePath: 'late/target.ts' }] })
expect(listFiles).toHaveBeenCalledWith(
'/repo',
expect.not.objectContaining({ includeIgnored: false })
)
})
@@ -1,3 +1,4 @@
import { SshHostNotServedError } from '../../providers/ssh-host-not-served-error'
import { describe, expect, it, vi } from 'vitest'
import {
ORIGIN_HEAD_COMPONENT,
@@ -9,7 +10,6 @@ import {
getGlabKnownHostsMock,
getPullRequestPushTargetMock,
gitRunner,
registerSshGitProvider,
setPlatform
} from '../orca-runtime-test-mocks.spec'
import {
@@ -178,7 +178,7 @@ describe('OrcaRuntimeService', () => {
}
})
it('resolves SSH GitHub fork PR heads through the write-capable fetch RPC', async () => {
it('refuses SSH GitHub fork PR heads instead of resolving them on this machine', async () => {
const remoteRepo = {
id: TEST_REPO_ID,
path: '/remote/repo',
@@ -193,54 +193,17 @@ describe('OrcaRuntimeService', () => {
getRepos: () => [remoteRepo],
getRepo: (id: string) => (id === remoteRepo.id ? remoteRepo : undefined)
}
const provider = {
exec: vi.fn(async (args: string[]) => {
if (args[0] === 'remote' && args[1] === 'get-url') {
return { stdout: `${ORIGIN_REMOTE_URL}\n`, stderr: '' }
}
if (args[0] === 'remote') {
return { stdout: 'origin\nupstream\n', stderr: '' }
}
if (
args[0] === 'rev-parse' &&
args[2] === `refs/orca/pull/${ORIGIN_HEAD_COMPONENT}/42^{commit}`
) {
return { stdout: 'remote-fork-pr-sha\n', stderr: '' }
}
throw new Error(`unexpected git call: ${args.join(' ')}`)
}),
fetchGitHubPullRequestHead: vi
.fn()
.mockResolvedValue(`refs/orca/pull/${ORIGIN_HEAD_COMPONENT}/42`),
fetchRemoteTrackingRef: vi.fn().mockResolvedValue(undefined)
}
registerSshGitProvider('ssh-1', provider as never)
const runtime = new OrcaRuntimeService(runtimeStore as never)
const result = await runtime.resolveManagedPrBase({
repoSelector: 'id:repo-1',
prNumber: 42,
headRefName: 'contributor/fix',
isCrossRepository: true
})
expect(result).toEqual({
baseBranch: 'remote-fork-pr-sha',
headSha: 'remote-fork-pr-sha',
branchNameOverride: 'contributor/fix'
})
expect(provider.fetchGitHubPullRequestHead).toHaveBeenCalledWith('/remote/repo', 'origin', 42)
expect(getPullRequestPushTargetMock).toHaveBeenCalledWith(
'/remote/repo',
42,
'ssh-1',
{},
'origin'
)
expect(provider.exec).not.toHaveBeenCalledWith(
expect.arrayContaining(['fetch']),
'/remote/repo'
)
await expect(
runtime.resolveManagedPrBase({
repoSelector: 'id:repo-1',
prNumber: 42,
headRefName: 'contributor/fix',
isCrossRepository: true
})
).rejects.toThrow(SshHostNotServedError)
expect(getPullRequestPushTargetMock).not.toHaveBeenCalled()
})
it('resolves local GitLab fork MR bases from the target project MR head ref', async () => {
@@ -1,17 +1,15 @@
import { SshHostNotServedError } from '../../providers/ssh-host-not-served-error'
import { describe, expect, it, vi } from 'vitest'
import {
ORIGIN_HEAD_COMPONENT,
ORIGIN_REMOTE_URL,
OrcaRuntimeService,
getGitLabProjectRefForRemoteMock,
getGlabKnownHostsMock,
gitRunner,
registerSshGitProvider
gitRunner
} from '../orca-runtime-test-mocks.spec'
import { TEST_REPO_ID, TEST_REPO_PATH, store } from '../orca-runtime-test-fixtures.spec'
describe('OrcaRuntimeService', () => {
it('resolves SSH GitLab fork MR bases from the target project MR head ref', async () => {
it('refuses SSH GitLab MR bases instead of resolving them on this machine', async () => {
const remoteRepo = {
id: TEST_REPO_ID,
path: '/remote/repo',
@@ -26,122 +24,21 @@ describe('OrcaRuntimeService', () => {
getRepos: () => [remoteRepo],
getRepo: (id: string) => (id === remoteRepo.id ? remoteRepo : undefined)
}
const provider = {
exec: vi.fn(async (args: string[]) => {
if (args[0] === 'remote' && args[1] === 'get-url') {
return { stdout: `${ORIGIN_REMOTE_URL}\n`, stderr: '' }
}
if (
args[0] === 'rev-parse' &&
args[1] === '--verify' &&
args[2] === `refs/orca/merge-requests/${ORIGIN_HEAD_COMPONENT}/77^{commit}`
) {
return { stdout: 'remote-fork-mr-sha\n', stderr: '' }
}
throw new Error(`unexpected git call: ${args.join(' ')}`)
}),
fetchGitLabMergeRequestHead: vi
.fn()
.mockResolvedValue(`refs/orca/merge-requests/${ORIGIN_HEAD_COMPONENT}/77`),
fetchRemoteTrackingRef: vi.fn().mockResolvedValue(undefined)
}
registerSshGitProvider('ssh-1', provider as never)
getGlabKnownHostsMock.mockResolvedValue(['gitlab.com', 'git.internal'])
const gitSpy = vi.spyOn(gitRunner, 'gitExecFileAsync')
const runtime = new OrcaRuntimeService(runtimeStore as never)
const result = await runtime.resolveManagedMrBase({
repoSelector: 'id:repo-1',
mrIid: 77,
sourceBranch: 'contrib/remote-fix',
targetBranch: 'main',
isCrossRepository: true
})
expect(result).toEqual({
baseBranch: 'remote-fork-mr-sha',
compareBaseRef: 'refs/remotes/origin/main'
})
expect(provider.fetchGitLabMergeRequestHead).toHaveBeenCalledWith('/remote/repo', 'origin', 77)
expect(provider.fetchRemoteTrackingRef).toHaveBeenCalledWith(
'/remote/repo',
'origin',
'main',
'refs/remotes/origin/main'
)
expect(provider.exec).toHaveBeenCalledWith(
['rev-parse', '--verify', `refs/orca/merge-requests/${ORIGIN_HEAD_COMPONENT}/77^{commit}`],
'/remote/repo'
)
expect(getGitLabProjectRefForRemoteMock).toHaveBeenCalledWith(
'/remote/repo',
'origin',
['gitlab.com', 'git.internal'],
'ssh-1'
)
})
it('resolves SSH GitLab same-repo MR bases through remote-tracking fetches', async () => {
const remoteRepo = {
id: TEST_REPO_ID,
path: '/remote/repo',
displayName: 'repo',
badgeColor: 'blue',
addedAt: 1,
connectionId: 'ssh-1',
issueSourcePreference: 'origin' as const
}
const runtimeStore = {
...store,
getRepos: () => [remoteRepo],
getRepo: (id: string) => (id === remoteRepo.id ? remoteRepo : undefined)
}
const provider = {
exec: vi.fn(async (args: string[]) => {
if (args[0] === 'rev-parse' && args[1] === '--verify' && args[2] === 'origin/feature/fix') {
return { stdout: 'same-repo-mr-sha\n', stderr: '' }
}
throw new Error(`unexpected git call: ${args.join(' ')}`)
}),
fetchGitLabMergeRequestHead: vi.fn().mockResolvedValue(undefined),
fetchRemoteTrackingRef: vi.fn().mockResolvedValue(undefined)
}
registerSshGitProvider('ssh-1', provider as never)
getGlabKnownHostsMock.mockResolvedValue(['gitlab.com', 'git.internal'])
getGitLabProjectRefForRemoteMock.mockResolvedValue({
host: 'gitlab.example',
path: 'group/repo'
})
const runtime = new OrcaRuntimeService(runtimeStore as never)
const result = await runtime.resolveManagedMrBase({
repoSelector: 'id:repo-1',
mrIid: 78,
sourceBranch: 'feature/fix',
targetBranch: 'main'
})
expect(result).toEqual({
baseBranch: 'origin/feature/fix',
compareBaseRef: 'refs/remotes/origin/main',
pushTarget: { remoteName: 'origin', branchName: 'feature/fix' }
})
expect(provider.fetchRemoteTrackingRef).toHaveBeenCalledWith(
'/remote/repo',
'origin',
'feature/fix',
'refs/remotes/origin/feature/fix'
)
expect(provider.fetchRemoteTrackingRef).toHaveBeenCalledWith(
'/remote/repo',
'origin',
'main',
'refs/remotes/origin/main'
)
expect(provider.fetchGitLabMergeRequestHead).not.toHaveBeenCalled()
expect(provider.exec).toHaveBeenCalledWith(
['rev-parse', '--verify', 'origin/feature/fix'],
'/remote/repo'
)
await expect(
runtime.resolveManagedMrBase({
repoSelector: 'id:repo-1',
mrIid: 77,
sourceBranch: 'feature',
targetBranch: 'main',
isCrossRepository: true
})
).rejects.toThrow(SshHostNotServedError)
expect(getGlabKnownHostsMock).not.toHaveBeenCalled()
expect(gitSpy).not.toHaveBeenCalled()
gitSpy.mockRestore()
})
it('keeps the MR source base when the optional compare-base fetch fails', async () => {
@@ -260,68 +157,4 @@ describe('OrcaRuntimeService', () => {
gitSpy.mockRestore()
}
})
it('keeps a cross-repo fork MR compare base when the fetch fails but the local ref resolves', async () => {
// Why: mirror the GitHub fork soft-fail-keep — a transient compare-base fetch
// failure must not drop a base we already have on disk onto the fork MR head SHA.
const remoteRepo = {
id: TEST_REPO_ID,
path: '/remote/repo',
displayName: 'repo',
badgeColor: 'blue',
addedAt: 1,
connectionId: 'ssh-1',
issueSourcePreference: 'origin' as const
}
const runtimeStore = {
...store,
getRepos: () => [remoteRepo],
getRepo: (id: string) => (id === remoteRepo.id ? remoteRepo : undefined)
}
const durableLocalRef = `refs/orca/merge-requests/${ORIGIN_HEAD_COMPONENT}/77`
const provider = {
exec: vi.fn(async (args: string[]) => {
if (args[0] === 'rev-parse' && args[2] === `${durableLocalRef}^{commit}`) {
return { stdout: 'remote-fork-mr-sha\n', stderr: '' }
}
if (args[0] === 'rev-parse' && args[2] === 'refs/remotes/origin/main^{commit}') {
return { stdout: 'base-commit-sha\n', stderr: '' }
}
throw new Error(`unexpected git call: ${args.join(' ')}`)
}),
fetchGitLabMergeRequestHead: vi.fn().mockResolvedValue(durableLocalRef),
fetchRemoteTrackingRef: vi.fn(async () => {
throw new Error('fatal: unable to access repo: Could not resolve host: gitlab.example')
})
}
registerSshGitProvider('ssh-1', provider as never)
getGlabKnownHostsMock.mockResolvedValue(['gitlab.com', 'git.internal'])
getGitLabProjectRefForRemoteMock.mockResolvedValue({
host: 'gitlab.example',
path: 'group/repo'
})
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
const runtime = new OrcaRuntimeService(runtimeStore as never)
try {
const result = await runtime.resolveManagedMrBase({
repoSelector: 'id:repo-1',
mrIid: 77,
sourceBranch: 'contrib/remote-fix',
targetBranch: 'main',
isCrossRepository: true
})
expect(result).toEqual({
baseBranch: 'remote-fork-mr-sha',
compareBaseRef: 'refs/remotes/origin/main'
})
expect(provider.exec).toHaveBeenCalledWith(
['rev-parse', '--verify', 'refs/remotes/origin/main^{commit}'],
'/remote/repo'
)
} finally {
warnSpy.mockRestore()
}
})
})
@@ -1,4 +1,4 @@
import { describe, expect, it, vi } from 'vitest'
import { describe, expect, it } from 'vitest'
import {
OrcaRuntimeService,
addGitHubIssueCommentMock,
@@ -15,12 +15,9 @@ import {
listGitHubIssuesMock,
listGitHubLabelsMock,
listGitHubWorkItemsMock,
parseOrcaYaml,
registerSshFilesystemProvider,
removeGitHubPRReviewersMock,
requestGitHubPRReviewersMock,
setPlatform,
unregisterSshFilesystemProvider,
updateGitHubIssueMock
} from '../orca-runtime-test-mocks.spec'
import { TEST_REPO_ID, TEST_REPO_PATH, store } from '../orca-runtime-test-fixtures.spec'
@@ -52,26 +49,13 @@ describe('OrcaRuntimeService', () => {
})
})
it('reports ok for a missing remote orca.yaml and error for any other read failure', async () => {
const readFile = vi.fn()
registerSshFilesystemProvider('ssh-1', { readFile } as never)
it('reports an SSH orca.yaml as an error without reading any filesystem', async () => {
const runtime = new OrcaRuntimeService(remoteStore as never)
try {
readFile.mockRejectedValueOnce(Object.assign(new Error('missing'), { code: 'ENOENT' }))
await expect(runtime.checkRepoHooks('id:repo-1')).resolves.toMatchObject({
status: 'ok',
hasHooks: false
})
readFile.mockRejectedValueOnce(Object.assign(new Error('down'), { code: 'ECONNRESET' }))
await expect(runtime.checkRepoHooks('id:repo-1')).resolves.toMatchObject({
status: 'error',
hasHooks: false
})
} finally {
unregisterSshFilesystemProvider('ssh-1')
}
await expect(runtime.checkRepoHooks('id:repo-1')).resolves.toMatchObject({
status: 'error',
hasHooks: false
})
})
it('reports ok for a local repo hook check', async () => {
@@ -81,7 +65,7 @@ describe('OrcaRuntimeService', () => {
})
})
it('resolves SSH issue commands from shared orca.yaml and deletes empty overrides', async () => {
it('reads SSH issue commands as absent and drops writes without touching any filesystem', async () => {
const remoteStore = {
...store,
getRepos: () => [
@@ -95,48 +79,18 @@ describe('OrcaRuntimeService', () => {
}
]
}
vi.mocked(parseOrcaYaml).mockReturnValue({
scripts: {},
issueCommand: 'claude -p "Fix #{{issue}}"'
})
const fsProvider = {
readFile: vi.fn(async (filePath: string) => {
if (filePath.endsWith('.orca/issue-command')) {
throw Object.assign(new Error('missing'), { code: 'ENOENT' })
}
if (filePath.endsWith('orca.yaml')) {
return { content: 'issueCommand: claude -p "Fix #{{issue}}"', isBinary: false }
}
return { content: '', isBinary: false }
}),
writeFile: vi.fn().mockResolvedValue(undefined),
createDir: vi.fn().mockResolvedValue(undefined),
deletePath: vi.fn().mockResolvedValue(undefined)
}
registerSshFilesystemProvider('ssh-1', fsProvider as never)
const runtime = new OrcaRuntimeService(remoteStore as never)
try {
await expect(runtime.readRepoIssueCommand('id:repo-1')).resolves.toMatchObject({
localContent: null,
sharedContent: 'claude -p "Fix #{{issue}}"',
effectiveContent: 'claude -p "Fix #{{issue}}"',
localFilePath: '/remote/repo/.orca/issue-command',
source: 'shared'
})
await expect(runtime.writeRepoIssueCommand('id:repo-1', ' ')).resolves.toEqual({
ok: true
})
} finally {
unregisterSshFilesystemProvider('ssh-1')
}
expect(fsProvider.readFile).toHaveBeenCalledWith('/remote/repo/orca.yaml')
expect(fsProvider.deletePath).toHaveBeenCalledWith('/remote/repo/.orca/issue-command', false)
expect(fsProvider.writeFile).not.toHaveBeenCalledWith(
'/remote/repo/.orca/issue-command',
expect.anything()
)
await expect(runtime.readRepoIssueCommand('id:repo-1')).resolves.toMatchObject({
localContent: null,
sharedContent: null,
effectiveContent: null,
localFilePath: '/remote/repo/.orca/issue-command',
source: 'none'
})
await expect(runtime.writeRepoIssueCommand('id:repo-1', ' ')).resolves.toEqual({
ok: true
})
})
it('allows host integration slug helpers for SSH repos through provider-aware GitHub clients', async () => {
@@ -8,7 +8,6 @@ import {
mkdir,
mkdtemp,
registerSshFilesystemProvider,
registerSshGitProvider,
rm,
tmpdir,
unregisterSshFilesystemProvider
@@ -38,7 +37,7 @@ describe('OrcaRuntimeService', () => {
)
})
it('returns capped SSH refs for empty runtime repo searches', async () => {
it('reports no refs for an SSH repo and still reports the execution cap', async () => {
const remoteRepo = {
id: 'remote-repo',
path: '/home/user/repo',
@@ -52,156 +51,16 @@ describe('OrcaRuntimeService', () => {
getRepos: () => [remoteRepo],
getRepo: () => remoteRepo
}
const provider = {
exec: vi.fn().mockImplementation((argv: string[]) => {
if (argv[0] === 'remote') {
return Promise.resolve({ stdout: 'origin\nupstream\n', stderr: '' })
}
return Promise.resolve({
stdout: [
'refs/remotes/origin/main\0origin/main',
'refs/remotes/upstream/feature-x\0upstream/feature-x',
'refs/remotes/upstream/HEAD\0upstream/HEAD',
'refs/heads/local-only\0local-only'
].join('\n'),
stderr: ''
})
})
}
registerSshGitProvider('ssh-1', provider as never)
const runtime = new OrcaRuntimeService(runtimeStore as never)
const result = await runtime.searchRepoRefs('id:remote-repo', '', 2)
expect(result).toEqual({
refs: ['origin/main', 'upstream/feature-x'],
refDetails: [
{ refName: 'origin/main', localBranchName: 'main' },
{ refName: 'upstream/feature-x', localBranchName: 'feature-x' }
],
truncated: true
await expect(runtime.searchRepoRefs('id:remote-repo', '', 5)).resolves.toEqual({
refs: [],
refDetails: [],
truncated: false
})
expect(provider.exec).toHaveBeenCalledWith(
expect.arrayContaining([
'--exclude=refs/remotes/*/HEAD',
'--count=12',
'refs/heads/**/**',
'refs/heads/**/**/**',
'refs/remotes/**/**',
'refs/remotes/**/**/**'
]),
'/home/user/repo'
)
expect(provider.exec).toHaveBeenCalledWith(['remote'], '/home/user/repo')
})
it('clamps oversized SSH ref-search limits and reports the execution cap', async () => {
const remoteRepo = {
id: 'remote-repo-large-limit',
path: '/home/user/repo',
displayName: 'remote',
badgeColor: 'blue',
addedAt: 1,
connectionId: 'ssh-large-limit'
}
const runtimeStore = {
...store,
getRepos: () => [remoteRepo],
getRepo: () => remoteRepo
}
const provider = {
exec: vi.fn().mockImplementation((argv: string[]) => {
if (argv[0] === 'remote') {
return Promise.resolve({ stdout: 'origin\n', stderr: '' })
}
return Promise.resolve({
stdout: 'refs/remotes/origin/main\0origin/main',
stderr: ''
})
})
}
registerSshGitProvider('ssh-large-limit', provider as never)
const runtime = new OrcaRuntimeService(runtimeStore as never)
const result = await runtime.searchRepoRefs(
'id:remote-repo-large-limit',
'',
REPO_SEARCH_REFS_MAX_LIMIT + 1
)
expect(result).toEqual({
refs: ['origin/main'],
refDetails: [{ refName: 'origin/main', localBranchName: 'main' }],
truncated: true
})
const forEachRefCall = provider.exec.mock.calls.find(
(call) => (call[0] as string[])[0] === 'for-each-ref'
)
expect(forEachRefCall?.[0]).toContain('--count=4004')
})
it('retries runtime SSH ref searches without --exclude for older git hosts', async () => {
const remoteRepo = {
id: 'remote-repo',
path: '/home/user/repo',
displayName: 'remote',
badgeColor: 'blue',
addedAt: 1,
connectionId: 'ssh-1'
}
const runtimeStore = {
...store,
getRepos: () => [remoteRepo],
getRepo: () => remoteRepo
}
const provider = {
exec: vi.fn().mockImplementation((argv: string[]) => {
if (argv[0] === 'remote') {
return Promise.resolve({ stdout: 'origin\n', stderr: '' })
}
if (argv.some((arg) => arg.startsWith('--exclude=refs/remotes/'))) {
return Promise.reject(
Object.assign(new Error("unknown option `exclude'"), {
stderr: "error: unknown option `exclude'"
})
)
}
return Promise.resolve({
stdout: [
'refs/remotes/origin/main\0origin/main',
'refs/remotes/origin/HEAD\0origin/HEAD',
'refs/remotes/origin/feature-x\0origin/feature-x'
].join('\n'),
stderr: ''
})
})
}
registerSshGitProvider('ssh-1', provider as never)
const runtime = new OrcaRuntimeService(runtimeStore as never)
const result = await runtime.searchRepoRefs('id:remote-repo', '', 1)
const repeatedResult = await runtime.searchRepoRefs('id:remote-repo', '', 1)
expect(result).toEqual({
refs: ['origin/main'],
refDetails: [{ refName: 'origin/main', localBranchName: 'main' }],
truncated: true
})
expect(repeatedResult).toEqual(result)
const forEachRefCalls = provider.exec.mock.calls.filter(
(call) => (call[0] as string[])[0] === 'for-each-ref'
)
expect(forEachRefCalls).toHaveLength(3)
expect(
(forEachRefCalls[0][0] as string[]).some((arg) => arg.startsWith('--exclude=refs/remotes/'))
).toBe(true)
expect(
(forEachRefCalls[1][0] as string[]).some((arg) => arg.startsWith('--exclude=refs/remotes/'))
).toBe(false)
expect(forEachRefCalls[1][0]).toContain('--count=108')
expect(
(forEachRefCalls[2][0] as string[]).some((arg) => arg.startsWith('--exclude=refs/remotes/'))
).toBe(false)
await expect(
runtime.searchRepoRefs('id:remote-repo', '', REPO_SEARCH_REFS_MAX_LIMIT + 1)
).resolves.toEqual({ refs: [], refDetails: [], truncated: true })
})
it('resolves SSH worktrees when manually updating lineage', async () => {
@@ -14,7 +14,6 @@ import {
listWorktrees,
loadHooks,
muxRequestMock,
parseOrcaYaml,
registerSshFilesystemProvider,
registerSshGitProvider,
removeWorktree,
@@ -537,7 +536,7 @@ describe('OrcaRuntimeService', () => {
expect(removeWorktree).not.toHaveBeenCalled()
})
it('reads SSH repo hooks through the SSH filesystem provider', async () => {
it('reports SSH repo hooks as absent without reading this machine', async () => {
const remoteStore = {
...store,
getRepos: () => [
@@ -551,31 +550,13 @@ describe('OrcaRuntimeService', () => {
}
]
}
const fsProvider = {
readFile: vi.fn().mockResolvedValue({
content: 'scripts:\n setup: pnpm install\n',
isBinary: false
})
}
vi.mocked(parseOrcaYaml).mockReturnValue({ scripts: { setup: 'pnpm install' } })
registerSshFilesystemProvider('ssh-1', fsProvider as never)
const runtime = new OrcaRuntimeService(remoteStore as never)
try {
await expect(runtime.getRepoHooks('id:repo-1')).resolves.toMatchObject({
hasHooksFile: true,
hooks: { scripts: { setup: 'pnpm install' } },
source: 'orca.yaml',
setupTrust: {
contentHash: '005d0b7e5c261dcc5e2f8568e69a0b30e889a3275b55b18ec20a7deef0081e90',
scriptContent: 'pnpm install'
}
})
} finally {
unregisterSshFilesystemProvider('ssh-1')
}
expect(fsProvider.readFile).toHaveBeenCalledWith('C:\\remote\\repo\\orca.yaml')
await expect(runtime.getRepoHooks('id:repo-1')).resolves.toMatchObject({
hasHooksFile: false,
hooks: null,
source: null
})
expect(hasHooksFile).not.toHaveBeenCalled()
expect(getEffectiveHooks).not.toHaveBeenCalled()
})
@@ -1,13 +1,9 @@
import { describe, expect, it, vi } from 'vitest'
import {
OrcaRuntimeService,
registerSshFilesystemProvider,
unregisterSshFilesystemProvider
} from '../orca-runtime-test-mocks.spec'
import { describe, expect, it } from 'vitest'
import { OrcaRuntimeService } from '../orca-runtime-test-mocks.spec'
import { TEST_REPO_ID, store } from '../orca-runtime-test-fixtures.spec'
describe('OrcaRuntimeService', () => {
it('uses remote path joins for SSH hook checks and issue-command files', async () => {
it('uses remote path joins for SSH issue-command files and touches no filesystem', async () => {
const remoteStore = {
...store,
getRepos: () => [
@@ -21,49 +17,19 @@ describe('OrcaRuntimeService', () => {
}
]
}
const fsProvider = {
readFile: vi.fn(async (filePath: string) => ({
content: filePath.endsWith('orca.yaml')
? 'scripts:\n setup: pnpm install\n'
: filePath.endsWith('.gitignore')
? 'node_modules\n'
: 'Fix it',
isBinary: false
})),
writeFile: vi.fn().mockResolvedValue(undefined),
createDir: vi.fn().mockResolvedValue(undefined),
deletePath: vi.fn().mockResolvedValue(undefined)
}
registerSshFilesystemProvider('ssh-1', fsProvider as never)
const runtime = new OrcaRuntimeService(remoteStore as never)
try {
await expect(runtime.checkRepoHooks('id:repo-1')).resolves.toMatchObject({
hasHooks: true,
mayNeedUpdate: false
})
await expect(runtime.readRepoIssueCommand('id:repo-1')).resolves.toMatchObject({
localContent: 'Fix it',
effectiveContent: 'Fix it',
localFilePath: 'C:\\remote\\repo\\.orca\\issue-command'
})
await expect(runtime.writeRepoIssueCommand('id:repo-1', 'Ship it')).resolves.toEqual({
ok: true
})
} finally {
unregisterSshFilesystemProvider('ssh-1')
}
expect(fsProvider.readFile).toHaveBeenCalledWith('C:\\remote\\repo\\orca.yaml')
expect(fsProvider.readFile).toHaveBeenCalledWith('C:\\remote\\repo\\.orca\\issue-command')
expect(fsProvider.createDir).toHaveBeenCalledWith('C:\\remote\\repo\\.orca')
expect(fsProvider.writeFile).toHaveBeenCalledWith(
'C:\\remote\\repo\\.orca\\issue-command',
'Ship it\n'
)
expect(fsProvider.writeFile).toHaveBeenCalledWith(
'C:\\remote\\repo\\.gitignore',
'node_modules\n.orca\n'
)
await expect(runtime.checkRepoHooks('id:repo-1')).resolves.toMatchObject({
status: 'error',
hasHooks: false
})
await expect(runtime.readRepoIssueCommand('id:repo-1')).resolves.toMatchObject({
localContent: null,
effectiveContent: null,
localFilePath: 'C:\\remote\\repo\\.orca\\issue-command'
})
await expect(runtime.writeRepoIssueCommand('id:repo-1', 'Ship it')).resolves.toEqual({
ok: true
})
})
})
@@ -22,7 +22,6 @@ import {
localWorktreeFilesystem,
lstat,
mkdir,
registerSshGitProvider,
removeWorktree,
removeWorktreeLinkedPathsMock,
restoreLocalWatcherAfterFailedRemovalMock,
@@ -30,10 +29,8 @@ import {
rm,
runHook,
setPlatform,
unregisterSshGitProvider,
writeFile
} from '../orca-runtime-test-mocks.spec'
import type { WorktreeMeta } from '../orca-runtime-test-mocks.spec'
import {
TEST_REPO_ID,
TEST_REPO_PATH,
@@ -609,75 +606,4 @@ describe('OrcaRuntimeService', () => {
'def456'
)
})
it('force-deletes an SSH branch that was preserved by runtime worktree removal', async () => {
const remoteRepo = {
...store.getRepo(TEST_REPO_ID)!,
path: '/remote/repo',
connectionId: 'ssh-1'
}
const remoteWorktree = {
path: '/remote/feature-wt',
head: 'def456',
branch: 'feature/test',
isBare: false,
isMainWorktree: false
}
const remoteWorktreeId = `${remoteRepo.id}::${remoteWorktree.path}`
const metaById: Record<string, WorktreeMeta> = {
[remoteWorktreeId]: makeWorktreeMeta()
}
const runtimeStore = {
...store,
getRepos: () => [remoteRepo],
getRepo: (id: string) => (id === remoteRepo.id ? remoteRepo : undefined),
getAllWorktreeMeta: () => metaById,
getWorktreeMeta: (worktreeId: string) => metaById[worktreeId],
setWorktreeMeta: (worktreeId: string, meta: Partial<WorktreeMeta>) => {
metaById[worktreeId] = { ...(metaById[worktreeId] ?? makeWorktreeMeta()), ...meta }
return metaById[worktreeId]
},
removeWorktreeMeta: (worktreeId: string) => {
delete metaById[worktreeId]
}
}
const provider = {
exec: vi.fn().mockResolvedValue({ stdout: '', stderr: '' }),
forceDeletePreservedBranch: vi.fn().mockResolvedValue(undefined),
listWorktrees: vi.fn().mockResolvedValue([
{
path: remoteRepo.path,
head: 'main',
branch: 'main',
isBare: false,
isMainWorktree: true
},
remoteWorktree
]),
removeWorktree: vi.fn().mockResolvedValue({
preservedBranch: { branchName: 'feature/test', head: 'def456' }
})
}
registerSshGitProvider('ssh-1', provider as never)
const runtime = createWorktreeRemovalRuntime(runtimeStore)
try {
await runtime.removeManagedWorktree(remoteWorktreeId)
const result = await runtime.forceDeletePreservedBranch(
remoteWorktreeId,
'feature/test',
'def456'
)
expect(result).toEqual({ deleted: true })
expect(provider.forceDeletePreservedBranch).toHaveBeenCalledWith(
'/remote/repo',
'feature/test',
'def456'
)
expect(forceDeleteLocalBranchMock).not.toHaveBeenCalled()
} finally {
unregisterSshGitProvider('ssh-1')
}
})
})
@@ -1,6 +1,7 @@
import { beforeEach, describe, expect, it, vi, type MockInstance } from 'vitest'
import { resetWorktreeTestSshHostHome } from '../../worktree-removal-test-ssh-host-home'
import { _settlePendingWorktreeRemovalsForTests } from '../../worktree-background-removal'
import { SshHostNotServedError } from '../../providers/ssh-host-not-served-error'
import {
OrcaRuntimeService,
@@ -109,19 +110,11 @@ describe('OrcaRuntimeService', () => {
})
expect(provider.removeWorktree).toHaveBeenCalledWith(TEST_WORKTREE_PATH, false)
expect(metaById[TEST_WORKTREE_ID]?.hostId).toBe('local')
const result = await runtime.forceDeletePreservedBranch(
TEST_WORKTREE_ID,
'feature/test',
'def456',
'ssh:ssh-1'
)
expect(result).toEqual({ deleted: true })
expect(provider.forceDeletePreservedBranch).toHaveBeenCalledWith(
remoteRepo.path,
'feature/test',
'def456'
)
// The SSH host is not served here, so the qualified host refuses instead of deleting locally.
await expect(
runtime.forceDeletePreservedBranch(TEST_WORKTREE_ID, 'feature/test', 'def456', 'ssh:ssh-1')
).rejects.toThrow(SshHostNotServedError)
expect(provider.forceDeletePreservedBranch).not.toHaveBeenCalled()
expect(forceDeleteLocalBranchMock).not.toHaveBeenCalled()
} finally {
unregisterSshGitProvider('ssh-1')
@@ -1,3 +1,4 @@
import { SshHostNotServedError } from '../../providers/ssh-host-not-served-error'
import { describe, expect, it, vi } from 'vitest'
import {
FLOATING_TERMINAL_WORKTREE_ID,
@@ -86,7 +87,7 @@ describe('OrcaRuntimeService', () => {
})
})
it('routes SSH-backed forward-slash UNC file and git paths without collapsing the root', async () => {
it('resolves SSH-backed forward-slash UNC paths without collapsing the root, then refuses the work', async () => {
vi.mocked(listWorktrees).mockClear()
vi.mocked(listWorktrees).mockRejectedValue(new Error('local git should not run for SSH repos'))
const remoteStore = {
@@ -134,8 +135,12 @@ describe('OrcaRuntimeService', () => {
const runtime = new OrcaRuntimeService(remoteStore as never)
try {
await runtime.readFileExplorerDir('path://server/share/repo', 'src')
await runtime.getRuntimeGitStatus('path://server/share/repo')
await expect(runtime.readFileExplorerDir('path://server/share/repo', 'src')).rejects.toThrow(
SshHostNotServedError
)
await expect(runtime.getRuntimeGitStatus('path://server/share/repo')).rejects.toThrow(
SshHostNotServedError
)
await expect(runtime.showRepo('path://server/share/repo')).resolves.toMatchObject({
path: '//Server/Share/Repo'
})
@@ -146,8 +151,8 @@ describe('OrcaRuntimeService', () => {
expect(listWorktrees).not.toHaveBeenCalled()
expect(gitProvider.listWorktrees).toHaveBeenCalledWith('//Server/Share/Repo')
expect(fsProvider.readDir).toHaveBeenCalledWith('\\\\Server\\Share\\Repo\\src', {})
expect(gitProvider.getStatus).toHaveBeenCalledWith('//Server/Share/Repo')
expect(fsProvider.readDir).not.toHaveBeenCalled()
expect(gitProvider.getStatus).not.toHaveBeenCalled()
})
it.each([
@@ -174,7 +179,7 @@ describe('OrcaRuntimeService', () => {
})
})
it('routes SSH folder workspace file explorer paths through the filesystem provider', async () => {
it('refuses SSH folder workspace file explorer reads instead of reading this machine', async () => {
const folderPath = '/srv/platform'
const fsProvider = {
stat: vi.fn(async (pathValue: string) => ({
@@ -201,24 +206,16 @@ describe('OrcaRuntimeService', () => {
try {
await expect(
runtime.readFileExplorerDir(`id:${TEST_FOLDER_WORKSPACE_KEY}`, 'src')
).resolves.toHaveLength(1)
).rejects.toThrow(SshHostNotServedError)
await expect(
runtime.readFileExplorerPreview(`id:${TEST_FOLDER_WORKSPACE_KEY}`, 'src/app.ts')
).resolves.toMatchObject({
content: 'remote\n',
isBinary: false
})
).rejects.toThrow(SshHostNotServedError)
} finally {
unregisterSshFilesystemProvider('ssh-folder')
}
expect(fsProvider.stat).toHaveBeenCalledWith(folderPath)
expect(fsProvider.readDir).toHaveBeenCalledWith('/srv/platform/src', {})
expect(fsProvider.stat).toHaveBeenCalledWith('/srv/platform/src/app.ts')
expect(fsProvider.readFile).toHaveBeenCalledWith('/srv/platform/src/app.ts', {
maxBinaryBytes: 10 * 1024 * 1024,
maxTextBytes: 512 * 1024
})
expect(fsProvider.readDir).not.toHaveBeenCalled()
expect(fsProvider.readFile).not.toHaveBeenCalled()
})
it('lists persisted SSH worktrees while the git provider is unavailable', async () => {
@@ -2,13 +2,8 @@ import './rpc/unused-default-rpc-methods.test-fixture'
import { afterEach, describe, expect, it, vi } from 'vitest'
import { OrcaRuntimeWithListManagedWorktrees } from './orca-runtime-list-managed-worktrees'
const muxes = vi.hoisted(() => new Map<string, { request: ReturnType<typeof vi.fn> }>())
const scanWorkspacePortsMock = vi.hoisted(() => vi.fn())
vi.mock('../ssh/ssh-target-registry', async (importOriginal) => ({
...(await importOriginal<Record<string, unknown>>()),
getActiveMultiplexer: (targetId: string) => muxes.get(targetId)
}))
vi.mock('../ports/local-workspace-port-scanner', () => ({
scanWorkspacePorts: scanWorkspacePortsMock
}))
@@ -31,29 +26,19 @@ function runtimeResolvingTo(executionHostId: string) {
describe('host-scoped workspace port scans on the server', () => {
afterEach(() => {
muxes.clear()
scanWorkspacePortsMock.mockReset()
vi.restoreAllMocks()
})
it("scans a folder workspace's SSH host, not this server", async () => {
const request = vi.fn(async () => ({ platform: 'linux', ports: [{ port: 3020, host: '::' }] }))
muxes.set('box', { request })
it("reports a folder workspace's SSH host unavailable instead of scanning this server", async () => {
const runtime = runtimeResolvingTo('ssh:box')
const result = await runtime.scan('folder:fw-1')
expect(runtime.host.resolveRuntimeFileTarget).toHaveBeenCalledWith('folder:fw-1')
expect(request).toHaveBeenCalledWith('ports.detect', undefined, expect.any(Object))
expect(scanWorkspacePortsMock).not.toHaveBeenCalled()
expect(result).toMatchObject({ executionHostId: 'ssh:box', ports: [{ port: 3020 }] })
})
it('answers unavailable for an SSH workspace whose host is not connected', async () => {
const result = await runtimeResolvingTo('ssh:box').scan('id:repo::/srv/app')
expect(result.unavailableReason).toMatch(/not connected/)
expect(scanWorkspacePortsMock).not.toHaveBeenCalled()
expect(result).toMatchObject({ executionHostId: 'ssh:box', ports: [] })
expect(result.unavailableReason).toMatch(/Reconnect/)
})
it('refuses Stop when the workspace now resolves to a different host than the row', async () => {
@@ -8,19 +8,11 @@ import {
} from '../../../../shared/remote-rpc-content-budget'
import { assertGitDiffWithinTransportBudget } from '../../../../shared/git-diff-transport-budget'
import type { GitDiffResult } from '../../../../shared/git-diff-compare-types'
import type { GlobalSettings } from '../../../../shared/global-settings-types'
import type { OrcaRuntimeService } from '../../orca-runtime'
import { RuntimeGitCommands, type ResolvedRuntimeGitWorktree } from '../../orca-runtime-git'
import type { RpcRequest, RpcResponse } from '../core'
import { RpcDispatcher } from '../dispatcher'
import { GIT_METHODS } from './git'
const sshMocks = vi.hoisted(() => ({ getSshGitProvider: vi.fn() }))
vi.mock('../../../providers/ssh-git-dispatch', () => ({
getSshGitProvider: sshMocks.getSshGitProvider
}))
const OVERSIZED_BASE64 = 'A'.repeat(REMOTE_RPC_MAX_CONTENT_BYTES + 1024)
const OVERSIZED_DIFF: GitDiffResult = {
@@ -152,26 +144,4 @@ describe('remote git diff transport budget', () => {
})
}
)
// Why: an SSH host forwards its provider's payload verbatim, so the cap cannot rely on the far
// side clamping — this walks the real RuntimeGitCommands with an unclamped forwarded diff.
it('caps an SSH-forwarded diff a remote client requested', async () => {
sshMocks.getSshGitProvider.mockReturnValue({
getDiff: vi.fn().mockResolvedValue(OVERSIZED_DIFF)
})
const commands = new RuntimeGitCommands({
resolveRuntimeGitTarget: async () => ({
worktree: { id: 'wt-1', path: '/remote/repo' } as unknown as ResolvedRuntimeGitWorktree,
executionHostId: 'ssh:conn-1'
}),
getRuntimeSettings: () => ({}) as GlobalSettings
})
const runtime = Object.assign(commands, {
getRuntimeId: () => 'test-runtime'
}) as unknown as OrcaRuntimeService
const response = await dispatchRemote(runtime, 'git.diff', CASES[0]!.params, 'mobile')
expect(response).toMatchObject({ ok: false, error: { code: 'diff_too_large' } })
})
})
@@ -22,7 +22,6 @@ import { joinWorktreeRelativePath, normalizeRuntimeRelativePath } from './runtim
import { isENOENT } from '../ipc/filesystem-path-containment'
import {
requireRuntimeFileProvider,
runtimeFileRouteForTarget,
type RuntimeFileExplorerPath
} from './runtime-file-command-target'
import { parseExecutionHostId, type ExecutionHostId } from '../../shared/execution-host'
@@ -91,15 +90,11 @@ export class RuntimeFileCommandAccess {
filePath: string,
target: { executionHostId: ExecutionHostId }
): Promise<void> {
const route = runtimeFileRouteForTarget(target)
let stats: FileStat
try {
stats = await requireRuntimeFileProvider(target, this.host, route).stat(filePath)
stats = await requireRuntimeFileProvider(target, this.host).stat(filePath)
} catch (error) {
if (
isENOENT(error) ||
(route.kind === 'ssh' && RuntimeFileCommandAccess.isRemoteNotFoundErrorMessage(error))
) {
if (isENOENT(error)) {
throw new Error(`ENOENT: no such file or directory, open '${filePath}'`)
}
throw error
@@ -190,12 +185,6 @@ export class RuntimeFileCommandAccess {
...(fileStat.ctimeMs === undefined ? {} : { ctime: fileStat.ctimeMs })
}
}
// The mux drops ErrnoException.code, so match not-found by message shape (vs transport/permission/provider errors).
protected static isRemoteNotFoundErrorMessage(error: unknown): boolean {
const message = error instanceof Error ? error.message : String(error)
return /\bENOENT\b|no such file|not found|does not exist/i.test(message)
}
}
// Why: name the resolved owner; undefined would let the renderer fall back to the focused server.
+19 -34
View File
@@ -10,11 +10,7 @@ import type {
LocalFilesystemProvider,
LocalFilesystemProviderOptions
} from '../providers/local-filesystem-provider'
import {
getSshFilesystemProvider,
SSH_FILESYSTEM_PROVIDER_UNAVAILABLE_MESSAGE
} from '../providers/ssh-filesystem-dispatch'
import type { IFilesystemProvider } from '../providers/types'
import { SshHostNotServedError } from '../providers/ssh-host-not-served-error'
export type ResolvedRuntimeFileWorktree = Worktree & { git: GitWorktreeInfo }
@@ -49,11 +45,8 @@ export type RuntimeFileExplorerPath = {
*/
export type RuntimeFileRoute =
| { kind: 'local'; createProvider: typeof createLocalFilesystemProvider }
/** `provider: null` is "remote and currently unreachable" — never "read it here". */
| { kind: 'ssh'; connectionId: string; provider: IFilesystemProvider | null }
/** The remote half of the route, for leaf helpers that only ever run against an SSH host. */
export type RuntimeFileSshRoute = Extract<RuntimeFileRoute, { kind: 'ssh' }>
/** An SSH target this process does not serve; it fails or reports empty, never reads here. */
| { kind: 'ssh'; connectionId: string }
export function runtimeFileRouteForTarget(target: {
executionHostId: ExecutionHostId
@@ -63,11 +56,7 @@ export function runtimeFileRouteForTarget(target: {
case 'local':
return { kind: 'local', createProvider: route.createProvider }
case 'ssh':
return {
kind: 'ssh',
connectionId: route.connectionId,
provider: getSshFilesystemProvider(route.connectionId) ?? null
}
return { kind: 'ssh', connectionId: route.connectionId }
case 'runtime':
throw new ExecutionHostNotDispatchableError(route.hostId)
}
@@ -76,32 +65,28 @@ export function runtimeFileRouteForTarget(target: {
/** What a runtime command supplies for local work; path authorization is always the store's. */
export type RuntimeLocalFileHost = Omit<LocalFilesystemProviderOptions, 'resolveAuthorizedPath'>
/**
* A provider for the target's own host. Local authorizes against `host`'s store; an unreachable SSH
* host and a `runtime:` host both throw.
*/
/** The local route for work this process runs itself; `ssh:` and `runtime:` hosts throw. */
export function requireLocalRuntimeFileRoute(target: {
executionHostId: ExecutionHostId
}): Extract<RuntimeFileRoute, { kind: 'local' }> {
const route = runtimeFileRouteForTarget(target)
if (route.kind === 'ssh') {
throw new SshHostNotServedError(route.connectionId)
}
return route
}
/** A provider for the target's own host, authorized against `host`'s store. */
export function requireRuntimeFileProvider(
target: { executionHostId: ExecutionHostId },
host: RuntimeLocalFileHost,
route: RuntimeFileRoute = runtimeFileRouteForTarget(target)
): LocalFilesystemProvider | IFilesystemProvider {
if (route.kind === 'ssh') {
return requireSshRuntimeFileProvider(route)
}
return route.createProvider({
host: RuntimeLocalFileHost
): LocalFilesystemProvider {
return requireLocalRuntimeFileRoute(target).createProvider({
requireStore: () => host.requireStore(),
resolveAuthorizedPath
})
}
/** For the commands whose local arm keeps its own limits or result shape. */
export function requireSshRuntimeFileProvider(route: RuntimeFileSshRoute): IFilesystemProvider {
if (!route.provider) {
throw new Error(SSH_FILESYSTEM_PROVIDER_UNAVAILABLE_MESSAGE)
}
return route.provider
}
/**
* The SSH target id for the leaf helpers that still address a connection by name — watcher release
* keys, re-arm registration, remote path stats. `undefined` is `local`; a `runtime:` host throws
@@ -1,19 +1,12 @@
// @ts-nocheck -- mechanically split class members.
import { RuntimeFileCommandsWithWriteTerminalArtifactFile } from './runtime-file-commands-write-terminal-artifact-file'
import type { TerminalFileGrant } from './runtime-file-commands-mobile-file-list-limit'
import {
runtimeFileWatcherLeasesByOwnerAndRoot,
runtimeWatcherReleaseKey
} from './runtime-file-commands-mobile-file-list-limit'
import type { IFilesystemProvider } from '../providers/types'
import {
SSH_FILESYSTEM_PROVIDER_UNAVAILABLE_MESSAGE,
getSshFilesystemProvider,
requireSshFilesystemProvider
} from '../providers/ssh-filesystem-dispatch'
import {
requireLocalRuntimeFileRoute,
requireRuntimeFileProvider,
runtimeFileRouteForTarget,
runtimeFileSshTargetId
} from './runtime-file-command-target'
import { stat } from 'node:fs/promises'
@@ -22,10 +15,6 @@ import { sortDirEntries } from '../../shared/file-name-sort'
import { resolveAuthorizedPath } from '../ipc/filesystem-auth'
import { watchWindowsRuntimeFileExplorer } from './runtime-file-command-host'
import { beginWatcherInstall } from '../ipc/watcher-removal-gate'
import {
armSshFileExplorerWatchRearm,
stopSshFileExplorerWatchRearms
} from './runtime-file-commands-ssh-file-watcher-rearm'
import {
closeFileExplorerWatcherInWatcherProcess,
watchFileExplorerInWatcherProcess
@@ -33,34 +22,13 @@ import {
import { registerRuntimeFileWatcherRelease } from './runtime-file-watcher-leases'
export class RuntimeFileCommandsWithAssertRemoteTerminalFileGrantPathStillCanonical extends RuntimeFileCommandsWithWriteTerminalArtifactFile {
protected async assertRemoteTerminalFileGrantPathStillCanonical(
grant: TerminalFileGrant
): Promise<IFilesystemProvider> {
if (!grant.connectionId) {
throw new Error('terminal_file_grant_mismatch')
}
const provider = requireSshFilesystemProvider(grant.connectionId)
const canonicalPath =
grant.provenance === 'native-chat'
? await provider.realpath(grant.absolutePath)
: await this.resolveAllowedRemoteTerminalArtifactPath(
grant.absolutePath,
grant.connectionId
)
// Why: relay I/O follows symlinks, so re-canonicalize after the remote process can mutate the path.
if (canonicalPath !== grant.absolutePath) {
throw new Error('terminal_file_grant_stale')
}
return provider
}
async readFileExplorerDir(
worktreeSelector: string,
relativePath: string,
options: { followSymlinks?: boolean } = {}
): Promise<DirEntry[]> {
const target = await this.resolveFileExplorerPath(worktreeSelector, relativePath)
// Why: re-sort here — the remote relay may be an older build with lexicographic ordering.
// Why: providers do not promise this order, so sort before returning.
return sortDirEntries(
await requireRuntimeFileProvider(target, this.host).readDir(target.path, options)
)
@@ -82,55 +50,7 @@ export class RuntimeFileCommandsWithAssertRemoteTerminalFileGrantPathStillCanoni
}> => {
const finishInstall = beginWatcherInstall(target.path, sshTargetId)
try {
// Re-resolved per open: a reconnect mints a fresh provider for the same target.
const route = runtimeFileRouteForTarget(target)
if (route.kind === 'ssh') {
if (!route.provider) {
throw new Error(SSH_FILESYSTEM_PROVIDER_UNAVAILABLE_MESSAGE)
}
const provider = route.provider
const isCurrentInitialProvider = (): boolean =>
!signal?.aborted && provider === getSshFilesystemProvider(route.connectionId)
let initialCallbacks = {
callback: (events: FsChangeEvent[]) => {
if (isCurrentInitialProvider()) {
callback(events)
}
},
onTerminalError: (error: Error) => {
if (isCurrentInitialProvider()) {
onTerminalError(error)
}
}
}
try {
// Initial callbacks join the rearm generation after setup succeeds.
const close = await provider.watch(
target.path,
(events) => initialCallbacks.callback(events),
{ signal, onTerminalError: (error) => initialCallbacks.onTerminalError(error) }
)
const rearm = armSshFileExplorerWatchRearm({
runtimeId: this.host.getRuntimeId(),
connectionId: route.connectionId,
rootPath: target.path,
callback,
onTerminalError,
signal,
initialUnwatch: close,
initialProvider: provider
})
initialCallbacks = rearm.initialCallbacks
return { unsubscribe: rearm.unsubscribe, rootPaths: [target.path] }
} catch (error) {
initialCallbacks = {
callback: () => undefined,
onTerminalError: () => undefined
}
throw error
}
}
requireLocalRuntimeFileRoute(target)
const rootPath = await resolveAuthorizedPath(target.path, this.host.requireStore())
const rootStats = await stat(rootPath)
if (!rootStats.isDirectory()) {
@@ -189,9 +109,6 @@ export class RuntimeFileCommandsWithAssertRemoteTerminalFileGrantPathStillCanoni
forgetFileExplorerWatchersAfterRemoval(rootPath: string, connectionId?: string): void {
const key = runtimeWatcherReleaseKey(this.host.getRuntimeId(), connectionId, rootPath)
// Why: forget() never runs the lease's unsubscribe, so the re-arm would outlive a deleted
// worktree and re-watch it on the next reconnect.
stopSshFileExplorerWatchRearms(key)
const leases = runtimeFileWatcherLeasesByOwnerAndRoot.get(key)
if (leases) {
for (const lease of Array.from(leases)) {
@@ -2,9 +2,8 @@
import { RuntimeFileCommandsWithWriteFileExplorerFile } from './runtime-file-commands-write-file-explorer-file'
import { assertRuntimeFileMutationExpectation } from './runtime-file-commands-mobile-file-list-limit'
import {
requireRuntimeFileProvider,
requireSshRuntimeFileProvider,
runtimeFileRouteForTarget
requireLocalRuntimeFileRoute,
requireRuntimeFileProvider
} from './runtime-file-command-target'
import { resolveAuthorizedPath } from '../ipc/filesystem-auth'
import { constants, copyFile, mkdir, rm } from 'node:fs/promises'
@@ -47,15 +46,8 @@ export class RuntimeFileCommandsWithCreateFileExplorerDirNoClobber extends Runti
expectedSshTargetId,
expectedSshConnectionGeneration
)
const route = runtimeFileRouteForTarget(tempTarget)
// Why: the local arm commits through the resolved (not symlink-preserving) temp and final paths.
if (route.kind === 'ssh') {
const provider = requireSshRuntimeFileProvider(route)
await provider.copy(tempTarget.path, finalTarget.path)
await provider.deletePath(tempTarget.path, false).catch(() => {})
return { ok: true }
}
requireLocalRuntimeFileRoute(tempTarget)
// Why: commit through the resolved (not symlink-preserving) temp and final paths.
const store = this.host.requireStore()
const tempPath = await resolveAuthorizedPath(tempTarget.path, store)
const finalPath = await resolveAuthorizedPath(finalTarget.path, store)
@@ -121,8 +121,6 @@ export const runtimeFileWatcherLeasesByOwnerAndRoot = new Map<
Set<RuntimeFileWatcherLease>
>()
export const sshFileExplorerWatchRearms = new Map<string, Set<() => void>>()
export const MOBILE_BINARY_EXTENSIONS = new Set([
'.avif',
'.bmp',
@@ -164,8 +162,7 @@ export type TerminalFileGrant = {
id: string
worktreeId: string
absolutePath: string
provider: 'local' | 'ssh'
connectionId?: string
provider: 'local'
clientId?: string
expiresAt: number
statIdentity: string | null
@@ -9,13 +9,11 @@ import {
MOBILE_FILE_READ_MAX_BYTES,
RUNTIME_PREVIEWABLE_BINARY_MIME_TYPES,
assertPreviewWithinTransportBudget,
previewableBinaryByteLimit,
readPreviewFileWithinCap
previewableBinaryByteLimit
} from './runtime-file-commands-mobile-file-list-limit'
import {
requireRuntimeFileProvider,
requireSshRuntimeFileProvider,
runtimeFileRouteForTarget
requireLocalRuntimeFileRoute,
requireRuntimeFileProvider
} from './runtime-file-command-target'
import { readLocalFileRange } from '../ipc/filesystem/local-file-range-read'
import { resolveAuthorizedPath } from '../ipc/filesystem-auth'
@@ -29,7 +27,6 @@ import type {
DocPreviewFileAccessRequest,
DocPreviewFileAccessResult
} from '../../shared/doc-preview-file-access'
import { readSshFileExplorerChunk } from './ssh-file-explorer-chunk-read'
export class RuntimeFileCommandsWithReadFileExplorerPreview extends RuntimeFileCommandsWithAssertRemoteTerminalFileGrantPathStillCanonical {
async readFileExplorerPreview(
@@ -42,36 +39,8 @@ export class RuntimeFileCommandsWithReadFileExplorerPreview extends RuntimeFileC
? LOCAL_PREVIEWABLE_BINARY_MAX_BYTES
: previewableBinaryByteLimit(maxContentBytes)
const target = await this.resolveFileExplorerPath(worktreeSelector, relativePath)
const route = runtimeFileRouteForTarget(target)
// Why: the local arm sizes its read by extension instead of a stat round trip.
if (route.kind === 'ssh') {
const provider = requireSshRuntimeFileProvider(route)
const fileStats = await provider.stat(target.path)
if (fileStats.size > binaryMaxBytes) {
throw new Error('file_too_large')
}
const result = await readPreviewFileWithinCap(provider, target.path, {
maxBinaryBytes: binaryMaxBytes,
maxTextBytes: MOBILE_FILE_READ_MAX_BYTES
})
// Why: the stat gate sizes base64 binaries; text crosses the wire JSON-escaped (up to 6x), so
// hold it to the same decoded limit the local branch enforces before reading.
if (
!result.isBinary &&
Buffer.byteLength(result.content, 'utf8') > MOBILE_FILE_READ_MAX_BYTES
) {
throw new Error('file_too_large')
}
if (
result.isBinary &&
maxContentBytes !== undefined &&
Buffer.byteLength(result.content, 'utf8') > maxContentBytes
) {
throw new Error('file_too_large')
}
return assertPreviewWithinTransportBudget(result, maxContentBytes)
}
requireLocalRuntimeFileRoute(target)
// Why: sized by extension instead of a stat round trip.
const filePath = await resolveAuthorizedPath(target.path, this.host.requireStore())
const mimeType = RUNTIME_PREVIEWABLE_BINARY_MIME_TYPES[extname(filePath).toLowerCase()]
const maxBytes = mimeType ? binaryMaxBytes : MOBILE_FILE_READ_MAX_BYTES
@@ -156,16 +125,7 @@ export class RuntimeFileCommandsWithReadFileExplorerPreview extends RuntimeFileC
length: number
): Promise<RuntimeFileReadChunkResult> {
const target = await this.resolveFileExplorerPath(worktreeSelector, relativePath)
const route = runtimeFileRouteForTarget(target)
if (route.kind === 'ssh') {
const provider = requireSshRuntimeFileProvider(route)
const fileStat = await provider.stat(target.path)
if (fileStat.type === 'directory') {
throw new Error('Cannot download a directory')
}
return readSshFileExplorerChunk(provider, target.path, fileStat.size, offset, length)
}
requireLocalRuntimeFileRoute(target)
const filePath = await resolveAuthorizedPath(target.path, this.host.requireStore())
return readLocalFileRange(filePath, offset, length)
}
@@ -1,15 +1,10 @@
import type { IFilesystemProvider } from '../providers/types'
import { MOBILE_FILE_READ_MAX_BYTES } from './runtime-file-commands-mobile-file-list-limit'
import { RuntimeFileCommandAccess } from './runtime-file-command-access'
import type { RuntimeFileReadResult } from '../../shared/runtime-types'
import { isMobileBinaryPath, isSafeMobileRelativePath } from './runtime-file-command-host'
import { joinWorktreeRelativePath } from './runtime-relative-paths'
import { readLocalMobileFile } from './runtime-file-commands-terminal-file-paths'
import { truncateMobileFilePreview } from './runtime-file-commands-terminal-artifact-access'
import {
requireSshRuntimeFileProvider,
runtimeFileRouteForTarget
} from './runtime-file-command-target'
import { requireLocalRuntimeFileRoute } from './runtime-file-command-target'
export class RuntimeFileCommandsWithReadMobileFile extends RuntimeFileCommandAccess {
async readMobileFile(
@@ -19,9 +14,8 @@ export class RuntimeFileCommandsWithReadMobileFile extends RuntimeFileCommandAcc
const store = this.host.requireStore()
const target = await this.host.resolveRuntimeFileTarget(worktreeSelector)
const { worktree } = target
const route = runtimeFileRouteForTarget(target)
// Why: an unreachable host fails before path validation, as it always has.
const sshProvider = route.kind === 'ssh' ? requireSshRuntimeFileProvider(route) : undefined
// Why: an `ssh:` host fails before path validation, as it always has.
requireLocalRuntimeFileRoute(target)
if (!isSafeMobileRelativePath(relativePath)) {
throw new Error('invalid_relative_path')
}
@@ -30,9 +24,7 @@ export class RuntimeFileCommandsWithReadMobileFile extends RuntimeFileCommandAcc
}
const filePath = joinWorktreeRelativePath(worktree.path, relativePath)
const content = sshProvider
? await this.readRemoteMobileFile(filePath, sshProvider)
: await readLocalMobileFile(filePath, store)
const content = await readLocalMobileFile(filePath, store)
const truncated = truncateMobileFilePreview(content)
return {
@@ -43,20 +35,4 @@ export class RuntimeFileCommandsWithReadMobileFile extends RuntimeFileCommandAcc
byteLength: truncated.byteLength
}
}
protected async readRemoteMobileFile(
filePath: string,
provider: IFilesystemProvider
): Promise<string> {
const fileStat = await provider.stat(filePath)
// Why: no ranged reads over SSH here, so reject oversized previews instead of streaming a whole file just to trim it.
if (fileStat.size > MOBILE_FILE_READ_MAX_BYTES) {
throw new Error('file_too_large')
}
const result = await provider.readFile(filePath)
if (result.isBinary) {
throw new Error('binary_file')
}
return result.content
}
}
@@ -18,14 +18,13 @@ import type {
} from './runtime-file-commands-mobile-file-list-limit'
import { TERMINAL_FILE_GRANT_TTL_MS } from './runtime-file-commands-mobile-file-list-limit'
import type { RuntimeTerminalPathResolution } from '../../shared/runtime-types'
import { isPathInsideOrEqual } from '../../shared/cross-platform-path'
import { randomUUID } from 'node:crypto'
import {
runtimeFileSshTargetId,
type ResolvedRuntimeFileTarget
} from './runtime-file-command-target'
import { requireReachableFilesystemRoute } from '../providers/execution-host-provider-dispatch'
import { requireSshFilesystemProvider } from '../providers/ssh-filesystem-dispatch'
import { SshHostNotServedError } from '../providers/ssh-host-not-served-error'
import { getConnectionExecutionHostId } from '../../shared/execution-host'
export class RuntimeFileCommandsWithResolveAllowedTerminalArtifactPath extends RuntimeFileCommandsWithResolveTerminalPath {
@@ -35,7 +34,7 @@ export class RuntimeFileCommandsWithResolveAllowedTerminalArtifactPath extends R
worktreePath: string
}): Promise<string | null> {
if (args.connectionId) {
return this.resolveAllowedRemoteTerminalArtifactPath(args.absolutePath, args.connectionId)
throw new SshHostNotServedError(args.connectionId)
}
return resolveAllowedLocalTerminalArtifactPath(args.absolutePath, args.worktreePath)
}
@@ -52,20 +51,11 @@ export class RuntimeFileCommandsWithResolveAllowedTerminalArtifactPath extends R
worktreeId: string
artifactPath: string
rejectedAbsolutePath?: string
connectionId?: string
clientId?: string
readOnly?: boolean
provenance?: TerminalFileGrant['provenance']
}): Promise<RuntimeTerminalPathResolution> {
let contentDigest: string | null = null
let stats: RuntimeFileStatLike & { isDirectory: () => boolean }
if (args.connectionId) {
stats = await this.statRemoteTerminalPath(args.artifactPath, args.connectionId)
} else {
const local = await this.statLocalTerminalArtifact(args.artifactPath)
stats = local.stats
contentDigest = local.contentDigest
}
const { stats, contentDigest } = await this.statLocalTerminalArtifact(args.artifactPath)
const isDirectory = stats.isDirectory()
if (!isDirectory && isTerminalArtifactHardLinked(stats)) {
return {
@@ -81,8 +71,6 @@ export class RuntimeFileCommandsWithResolveAllowedTerminalArtifactPath extends R
: this.createTerminalFileGrant({
worktreeId: args.worktreeId,
absolutePath: args.artifactPath,
provider: args.connectionId ? 'ssh' : 'local',
connectionId: args.connectionId,
clientId: args.clientId,
readOnly: args.readOnly === true,
provenance: args.provenance ?? 'terminal-output',
@@ -107,29 +95,6 @@ export class RuntimeFileCommandsWithResolveAllowedTerminalArtifactPath extends R
}
}
protected async resolveAllowedRemoteTerminalArtifactPath(
absolutePath: string,
connectionId: string
): Promise<string | null> {
const provider = requireSshFilesystemProvider(connectionId)
const roots = ['/tmp', '/private/tmp']
const providerTempDir = await provider.getTempDir?.().catch(() => null)
if (providerTempDir) {
roots.push(providerTempDir)
}
if (!roots.some((root) => isPathInsideOrEqual(root, absolutePath))) {
return null
}
const [realArtifactPath, ...realRoots] = await Promise.all([
provider.realpath(absolutePath),
...roots.map((root) => provider.realpath(root).catch(() => root))
])
// Why: SSH I/O follows symlinks on the relay; grant the canonical target so a /tmp link can't escape the temp boundary.
return realRoots.some((root) => isPathInsideOrEqual(root, realArtifactPath))
? realArtifactPath
: null
}
protected async statLocalTerminalPath(
absolutePath: string
): Promise<RuntimeFileStatLike & { isDirectory: () => boolean }> {
@@ -157,8 +122,6 @@ export class RuntimeFileCommandsWithResolveAllowedTerminalArtifactPath extends R
protected createTerminalFileGrant(args: {
worktreeId: string
absolutePath: string
provider: 'local' | 'ssh'
connectionId?: string
clientId?: string
readOnly?: boolean
provenance: TerminalFileGrant['provenance']
@@ -170,8 +133,7 @@ export class RuntimeFileCommandsWithResolveAllowedTerminalArtifactPath extends R
id: randomUUID(),
worktreeId: args.worktreeId,
absolutePath: args.absolutePath,
provider: args.provider,
...(args.connectionId ? { connectionId: args.connectionId } : {}),
provider: 'local',
...(args.clientId ? { clientId: args.clientId } : {}),
expiresAt: Date.now() + TERMINAL_FILE_GRANT_TTL_MS,
statIdentity: terminalFileStatIdentity(args.stats),
@@ -203,7 +165,8 @@ export class RuntimeFileCommandsWithResolveAllowedTerminalArtifactPath extends R
if (
grant.worktreeId !== target.worktree.id ||
grant.absolutePath !== absolutePath ||
grant.connectionId !== runtimeFileSshTargetId(target) ||
// Why: grants are only minted for local paths, so an `ssh:` target never matches one.
runtimeFileSshTargetId(target) !== undefined ||
grant.clientId !== clientId
) {
throw new Error('terminal_file_grant_mismatch')
@@ -1,6 +1,5 @@
// @ts-nocheck -- mechanically split class members.
import { RuntimeFileCommandsWithReadMobileFile } from './runtime-file-commands-read-mobile-file'
import { RuntimeFileCommandAccess as RuntimeFileCommands } from './runtime-file-command-access'
import type {
RuntimeNativeChatFileContext,
RuntimeTerminalPathResolution
@@ -18,8 +17,6 @@ import {
} from './runtime-file-command-target'
import { isSafeMobileRelativePath } from './runtime-file-command-host'
import { isENOENT } from '../ipc/filesystem-path-containment'
import type { RuntimeFileStatLike } from './runtime-file-commands-mobile-file-list-limit'
import { requireSshFilesystemProvider } from '../providers/ssh-filesystem-dispatch'
export class RuntimeFileCommandsWithResolveTerminalPath extends RuntimeFileCommandsWithReadMobileFile {
// Resolves a mobile terminal tap to a worktree-relative path; relatives resolve against cwd, else the worktree root.
@@ -91,11 +88,9 @@ export class RuntimeFileCommandsWithResolveTerminalPath extends RuntimeFileComma
) {
const isDirectory =
(
await requireRuntimeFileProvider(
knownWorkspaceTarget ?? target,
this.host,
ownedRoute
).stat(absolutePath)
await requireRuntimeFileProvider(knownWorkspaceTarget ?? target, this.host).stat(
absolutePath
)
).type === 'directory'
return {
worktree: ownedWorktree.id,
@@ -127,7 +122,6 @@ export class RuntimeFileCommandsWithResolveTerminalPath extends RuntimeFileComma
return await this.resolveAbsoluteFileGrant({
worktreeId: worktree.id,
artifactPath,
connectionId,
clientId,
readOnly: true,
provenance: 'native-chat'
@@ -167,15 +161,11 @@ export class RuntimeFileCommandsWithResolveTerminalPath extends RuntimeFileComma
worktreeId: worktree.id,
artifactPath,
rejectedAbsolutePath: absolutePath,
connectionId,
clientId
})
} catch (error) {
// Report genuine not-found as missing; let transport/permission errors surface so remote taps aren't all reported missing.
if (
isENOENT(error) ||
(ownedRoute.kind === 'ssh' && RuntimeFileCommands.isRemoteNotFoundErrorMessage(error))
) {
if (isENOENT(error)) {
return {
...empty,
worktree: ownedWorktree.id,
@@ -186,14 +176,4 @@ export class RuntimeFileCommandsWithResolveTerminalPath extends RuntimeFileComma
throw error
}
}
// Leaf helper: only ever reached from a route already resolved to `ssh`, so the id it takes is
// this client's dialable target rather than a repo row's raw `connectionId`.
protected async statRemoteTerminalPath(
absolutePath: string,
connectionId: string
): Promise<RuntimeFileStatLike & { isDirectory: () => boolean }> {
const stats = await requireSshFilesystemProvider(connectionId).stat(absolutePath)
return { ...stats, isDirectory: () => stats.type === 'directory' }
}
}
@@ -1,10 +1,7 @@
// @ts-nocheck -- mechanically split class members.
import { RuntimeFileCommandsWithResolveAllowedTerminalArtifactPath } from './runtime-file-commands-resolve-allowed-terminal-artifact-path'
import type { TerminalFileGrant } from './runtime-file-commands-mobile-file-list-limit'
import {
MOBILE_FILE_READ_MAX_BYTES,
assertPreviewWithinTransportBudget
} from './runtime-file-commands-mobile-file-list-limit'
import { assertPreviewWithinTransportBudget } from './runtime-file-commands-mobile-file-list-limit'
import type { RuntimeFilePreviewResult, RuntimeFileReadResult } from '../../shared/runtime-types'
import { isMobileBinaryPath } from './runtime-file-command-host'
import {
@@ -63,20 +60,11 @@ export class RuntimeFileCommandsWithRevokeTerminalFileGrantsForClient extends Ru
throw new Error('binary_file')
}
let content: string
if (grant.connectionId) {
const provider = await this.assertRemoteTerminalFileGrantFreshForRead(grant)
content = await this.readRemoteTerminalArtifactFile(
provider,
grant,
MOBILE_FILE_READ_MAX_BYTES
)
} else {
const handle = await openLocalTerminalArtifactGrant(grant, constants.O_RDONLY)
try {
content = await readLocalTerminalArtifactFileFromHandle(handle, grant)
} finally {
await handle.close()
}
const handle = await openLocalTerminalArtifactGrant(grant, constants.O_RDONLY)
try {
content = await readLocalTerminalArtifactFileFromHandle(handle, grant)
} finally {
await handle.close()
}
this.refreshTerminalFileGrant(grant)
const truncated = truncateMobileFilePreview(content)
@@ -103,14 +91,6 @@ export class RuntimeFileCommandsWithRevokeTerminalFileGrantsForClient extends Ru
absolutePath,
clientId
)
if (grant.connectionId) {
const provider = await this.assertRemoteTerminalFileGrantFreshForRead(grant)
this.refreshTerminalFileGrant(grant)
return assertPreviewWithinTransportBudget(
await this.readRemoteTerminalArtifactPreview(provider, grant, maxContentBytes),
maxContentBytes
)
}
const handle = await openLocalTerminalArtifactGrant(grant, constants.O_RDONLY)
try {
const preview = await readLocalTerminalArtifactPreviewFromHandle(
@@ -1,176 +0,0 @@
import type { FsChangeEvent } from '../../shared/filesystem-entry-types'
import type { IFilesystemProvider } from '../providers/types'
import { toRemoteRuntimeClientErrorLike } from '../../shared/remote-runtime-client-error-classification'
import {
runtimeWatcherReleaseKey,
sshFileExplorerWatchRearms
} from './runtime-file-commands-mobile-file-list-limit'
import {
getSshFilesystemProvider,
onSshFilesystemProviderRegistered
} from '../providers/ssh-filesystem-dispatch'
export function armSshFileExplorerWatchRearm(args: {
runtimeId: string
connectionId: string
rootPath: string
callback: (events: FsChangeEvent[]) => void
onTerminalError: (error: Error) => void
signal?: AbortSignal
initialUnwatch: () => void
initialProvider: Pick<IFilesystemProvider, 'watch'>
}): {
initialCallbacks: {
callback: (events: FsChangeEvent[]) => void
onTerminalError: (error: Error) => void
}
unsubscribe: () => Promise<void>
} {
const key = runtimeWatcherReleaseKey(args.runtimeId, args.connectionId, args.rootPath)
let currentUnwatch = args.initialUnwatch
let currentProvider = args.initialProvider
let stopped = false
let reinstalling: Promise<void> | null = null
let providerGeneration = 0
const reportTerminalError = (error: unknown): void => {
// Connection loss leaves the established stream armed for the next provider.
if (toRemoteRuntimeClientErrorLike(error).code === 'CONNECTION_LOST') {
return
}
args.onTerminalError(error instanceof Error ? error : new Error(String(error)))
}
const reinstall = async (
generation: number,
provider: Pick<IFilesystemProvider, 'watch'> | undefined
): Promise<void> => {
if (stopped || generation !== providerGeneration || !provider) {
return
}
const isCurrent = (): boolean =>
!stopped &&
!args.signal?.aborted &&
generation === providerGeneration &&
provider === getSshFilesystemProvider(args.connectionId)
let nextUnwatch: () => void
try {
nextUnwatch = await provider.watch(
args.rootPath,
(events) => {
if (isCurrent()) {
args.callback(events)
}
},
{
signal: args.signal,
onTerminalError: (error) => {
if (isCurrent()) {
reportTerminalError(error)
}
}
}
)
} catch (error) {
if (!isCurrent()) {
return
}
throw error
}
if (!isCurrent()) {
nextUnwatch()
return
}
// Why: a dead transport's handle must never unwatch the replacement transport's root.
if (currentProvider === provider) {
currentUnwatch()
}
currentUnwatch = nextUnwatch
currentProvider = provider
args.callback([{ kind: 'overflow', absolutePath: args.rootPath }])
}
const scheduleReinstall = (): void => {
if (stopped) {
return
}
const generation = ++providerGeneration
let attemptProvider: Pick<IFilesystemProvider, 'watch'> | undefined
// Why: obsolete reconnect attempts cannot terminate the stream or publish a stale refresh.
const attempt = (reinstalling ?? Promise.resolve())
.then(() => {
attemptProvider = getSshFilesystemProvider(args.connectionId)
return reinstall(generation, attemptProvider)
})
.catch((error: unknown) => {
if (
!stopped &&
!args.signal?.aborted &&
generation === providerGeneration &&
attemptProvider === getSshFilesystemProvider(args.connectionId)
) {
reportTerminalError(error)
}
})
.finally(() => {
if (reinstalling === attempt) {
reinstalling = null
}
})
reinstalling = attempt
}
const unsubscribeRearm = onSshFilesystemProviderRegistered((registeredId) => {
if (registeredId === args.connectionId) {
scheduleReinstall()
}
})
const stop = (): void => {
stopped = true
unsubscribeRearm()
const rearms = sshFileExplorerWatchRearms.get(key)
rearms?.delete(stop)
if (rearms?.size === 0) {
sshFileExplorerWatchRearms.delete(key)
}
}
const rearms = sshFileExplorerWatchRearms.get(key) ?? new Set<() => void>()
rearms.add(stop)
sshFileExplorerWatchRearms.set(key, rearms)
if (getSshFilesystemProvider(args.connectionId) !== args.initialProvider) {
scheduleReinstall()
}
const isInitialCurrent = (): boolean =>
!stopped &&
!args.signal?.aborted &&
providerGeneration === 0 &&
args.initialProvider === getSshFilesystemProvider(args.connectionId)
return {
initialCallbacks: {
callback: (events) => {
if (isInitialCurrent()) {
args.callback(events)
}
},
onTerminalError: (error) => {
if (isInitialCurrent()) {
reportTerminalError(error)
}
}
},
unsubscribe: () => {
stop()
const close = async (): Promise<void> => currentUnwatch()
// Why: awaiting an absent reinstall costs a microtask, and removal gating relies on the
// unwatch being issued on the same turn the lease releases it.
return reinstalling ? reinstalling.catch(() => undefined).then(close) : close()
}
}
}
export function stopSshFileExplorerWatchRearms(key: string): void {
for (const stop of Array.from(sshFileExplorerWatchRearms.get(key) ?? [])) {
stop()
}
}
@@ -1,11 +1,6 @@
// @ts-nocheck -- mechanically split class members.
import { RuntimeFileCommandsWithRevokeTerminalFileGrantsForClient } from './runtime-file-commands-revoke-terminal-file-grants-for-client'
import type { TerminalFileGrant } from './runtime-file-commands-mobile-file-list-limit'
import {
LOCAL_PREVIEWABLE_BINARY_MAX_BYTES,
MOBILE_FILE_READ_MAX_BYTES,
previewableBinaryByteLimit
} from './runtime-file-commands-mobile-file-list-limit'
import { MOBILE_FILE_READ_MAX_BYTES } from './runtime-file-commands-mobile-file-list-limit'
import { isBinaryBuffer } from '../../shared/binary-buffer'
import { isMobileBinaryPath } from './runtime-file-command-host'
import {
@@ -18,8 +13,6 @@ import { openLocalTerminalArtifactGrant } from './runtime-file-commands-terminal
import { chmod, constants, rename, rm, writeFile } from 'node:fs/promises'
import { basename, dirname, join } from 'node:path'
import { randomUUID } from 'node:crypto'
import type { FileStat, IFilesystemProvider } from '../providers/types'
import type { RuntimeFilePreviewResult } from '../../shared/runtime-types'
export class RuntimeFileCommandsWithWriteTerminalArtifactFile extends RuntimeFileCommandsWithRevokeTerminalFileGrantsForClient {
async writeTerminalArtifactFile(
@@ -44,27 +37,6 @@ export class RuntimeFileCommandsWithWriteTerminalArtifactFile extends RuntimeFil
if (isMobileBinaryPath(grant.absolutePath)) {
throw new Error('binary_file')
}
if (grant.connectionId) {
const { provider, fileStat } = await this.assertRemoteTerminalFileGrantFresh(grant)
if (fileStat.type === 'directory') {
throw new Error('Cannot write to a directory')
}
if (fileStat.size > MOBILE_FILE_READ_MAX_BYTES) {
throw new Error('file_too_large')
}
if (!provider.writeTerminalArtifact) {
throw new Error('terminal_file_grant_unavailable')
}
const nextStat = await provider.writeTerminalArtifact(
grant.absolutePath,
content,
this.terminalArtifactAccessOptions(grant, MOBILE_FILE_READ_MAX_BYTES)
)
grant.statIdentity = terminalFileStatIdentity(nextStat)
this.refreshTerminalFileGrant(grant)
return { ok: true }
}
let originalMode: number | null = null
const handle = await openLocalTerminalArtifactGrant(grant, constants.O_RDONLY)
try {
@@ -114,83 +86,4 @@ export class RuntimeFileCommandsWithWriteTerminalArtifactFile extends RuntimeFil
await rm(tempPath, { force: true }).catch(() => {})
}
}
protected async readRemoteTerminalArtifactPreview(
provider: IFilesystemProvider,
grant: TerminalFileGrant,
maxContentBytes: number | undefined
): Promise<RuntimeFilePreviewResult> {
const binaryMaxBytes =
maxContentBytes === undefined
? LOCAL_PREVIEWABLE_BINARY_MAX_BYTES
: previewableBinaryByteLimit(maxContentBytes)
const preview = await this.readRemoteTerminalArtifact(provider, grant, binaryMaxBytes)
if (
!preview.isBinary &&
Buffer.byteLength(preview.content, 'utf8') > MOBILE_FILE_READ_MAX_BYTES
) {
throw new Error('file_too_large')
}
if (
preview.isBinary &&
maxContentBytes !== undefined &&
Buffer.byteLength(preview.content, 'utf8') > maxContentBytes
) {
throw new Error('file_too_large')
}
return preview
}
protected async readRemoteTerminalArtifactFile(
provider: IFilesystemProvider,
grant: TerminalFileGrant,
maxBytes: number
): Promise<string> {
const result = await this.readRemoteTerminalArtifact(provider, grant, maxBytes)
if (result.isBinary) {
throw new Error('binary_file')
}
return result.content
}
protected async readRemoteTerminalArtifact(
provider: IFilesystemProvider,
grant: TerminalFileGrant,
maxBytes: number
): Promise<RuntimeFilePreviewResult> {
if (!provider.readTerminalArtifact) {
throw new Error('terminal_file_grant_unavailable')
}
return provider.readTerminalArtifact(
grant.absolutePath,
this.terminalArtifactAccessOptions(grant, maxBytes)
)
}
protected terminalArtifactAccessOptions(
grant: TerminalFileGrant,
maxBytes: number
): { expectedRealPath: string; expectedStatIdentity: string | null; maxBytes: number } {
return {
expectedRealPath: grant.absolutePath,
expectedStatIdentity: grant.statIdentity,
maxBytes
}
}
protected async assertRemoteTerminalFileGrantFreshForRead(
grant: TerminalFileGrant
): Promise<IFilesystemProvider> {
const { provider } = await this.assertRemoteTerminalFileGrantFresh(grant)
return provider
}
protected async assertRemoteTerminalFileGrantFresh(
grant: TerminalFileGrant
): Promise<{ provider: IFilesystemProvider; fileStat: FileStat }> {
const provider = await this.assertRemoteTerminalFileGrantPathStillCanonical(grant)
const fileStat = await provider.stat(grant.absolutePath)
assertTerminalFileGrantFresh(grant, fileStat)
return { provider, fileStat }
}
}
@@ -1,39 +1,14 @@
import { describe, expect, it, vi } from 'vitest'
import { getSshFilesystemProviderMock } from './orca-runtime-files-mock-registry'
import { SshHostNotServedError } from '../providers/ssh-host-not-served-error'
import {
createRuntimeFileCommands,
useRuntimeFileCommandsLifecycle
} from './orca-runtime-files-test-harness'
const { localList } = vi.hoisted(() => ({ localList: vi.fn() }))
vi.mock('../ipc/filesystem-list-files', () => ({ listQuickOpenFiles: localList }))
vi.mock(
'../providers/ssh-filesystem-dispatch',
async () => (await import('./orca-runtime-files-mock-registry')).sshFilesystemDispatchMock
)
function inventory(count: number) {
const paths = Array.from({ length: count }, (_, index) => `src/file-${index}.ts`)
return vi.fn(async (_root: string, options?: { maxResults?: number }) =>
paths.slice(0, options?.maxResults)
)
}
describe('runtime producer listing budgets', () => {
useRuntimeFileCommandsLifecycle()
it.each([5000, 5001, 5002])(
'passes the mobile sentinel budget on SSH for %i paths',
async (count) => {
const listFiles = inventory(count)
getSshFilesystemProviderMock.mockReturnValue({ listFiles })
const { commands } = createRuntimeFileCommands({ hostId: 'ssh:host' })
const result = await commands.listMobileFiles('id:wt-1')
expect(listFiles).toHaveBeenCalledWith('/repo', { maxResults: 5001, signal: undefined })
expect(result.files).toHaveLength(5000)
expect(result.totalCount).toBe(Math.min(count, 5001))
expect(result.truncated).toBe(count > 5000)
}
)
it('passes the same sentinel budget before local enumeration', async () => {
localList.mockImplementation(async (_root, _store, _excluded, _signal, maxResults) =>
Array.from({ length: Math.min(5002, maxResults) }, (_, i) => `file-${i}.txt`)
@@ -45,19 +20,6 @@ describe('runtime producer listing budgets', () => {
expect(result.truncated).toBe(true)
})
it.each([25002, 100000])(
'keeps late files in an unqualified %i-file SSH inventory',
async (count) => {
const listFiles = inventory(count)
getSshFilesystemProviderMock.mockReturnValue({ listFiles })
const { commands } = createRuntimeFileCommands({ hostId: 'ssh:host' })
const files = await commands.listRuntimeFiles('id:wt-1')
expect(files).toHaveLength(count)
expect(files.at(-1)).toBe(`src/file-${count - 1}.ts`)
expect(listFiles.mock.calls[0][1]?.maxResults).toBeUndefined()
}
)
it('preserves the full local inventory and explicit caller limits', async () => {
localList.mockClear()
localList.mockImplementation(async (_root, _store, _excluded, _signal, maxResults) =>
@@ -69,13 +31,12 @@ describe('runtime producer listing budgets', () => {
expect(await commands.listRuntimeFiles('id:wt-1', { maxResults: 3 })).toHaveLength(3)
})
it('requests Markdown from its semantic producer, without retaining unrelated paths', async () => {
const listFiles = vi.fn()
const listMarkdownDocuments = vi.fn().mockResolvedValue([])
getSshFilesystemProviderMock.mockReturnValue({ listFiles, listMarkdownDocuments })
it('refuses SSH Markdown listings without listing this machine', async () => {
localList.mockClear()
const { commands } = createRuntimeFileCommands({ hostId: 'ssh:host' })
await commands.listRuntimeMarkdownDocuments('id:wt-1')
expect(listMarkdownDocuments).toHaveBeenCalledWith('/repo')
expect(listFiles).not.toHaveBeenCalled()
await expect(commands.listRuntimeMarkdownDocuments('id:wt-1')).rejects.toThrow(
SshHostNotServedError
)
expect(localList).not.toHaveBeenCalled()
})
})
+15 -90
View File
@@ -5,11 +5,8 @@ import {
import type { RuntimeFileListResult } from '../../shared/runtime-types'
import type { SearchOptions, SearchResult } from '../../shared/code-search-types'
import { throwIfSignalAborted, waitForPromiseWithSignal } from '../../shared/abort-signal-reason'
import { QUICK_OPEN_LISTING_MAX_RESULTS } from '../../shared/quick-open-listing-limits'
import { limitQuickOpenFilesBySerializedBytes } from '../../shared/quick-open-transport-budget'
import { listQuickOpenFiles } from '../ipc/filesystem-list-files'
import { searchQuickOpenFilePaths as searchHostQuickOpenFilePaths } from '../ipc/filesystem-search-file-paths'
import type { IFilesystemProvider } from '../providers/types'
import { isMobileBinaryPath, isSafeMobileRelativePath } from './runtime-file-command-host'
import {
requireRuntimeFileProvider,
@@ -28,7 +25,6 @@ import {
RuntimeMobileFilePathSearchCache,
rankRuntimeMobileFilePaths
} from './runtime-mobile-file-path-search'
import { searchRemoteQuickOpenFilePaths } from './runtime-remote-quick-open-search'
type RuntimeFileSearchTarget = {
worktree: Pick<ResolvedRuntimeFileTarget['worktree'], 'id' | 'path'>
@@ -56,14 +52,10 @@ export class RuntimeFileSearch {
const target = await this.host.resolveRuntimeFileTarget(worktreeSelector)
const { worktree } = target
const route = runtimeFileRouteForTarget(target)
// Why: an `ssh:` host lists nothing rather than failing, and never lists this machine.
const files =
route.kind === 'ssh'
? await this.listRemoteMobileFiles(
worktree.path,
route.provider,
MOBILE_FILE_LIST_LIMIT + 1,
options.signal
)
? []
: await listQuickOpenFiles(
worktree.path,
store,
@@ -105,11 +97,7 @@ export class RuntimeFileSearch {
const inventory = await this.mobileFilePathSearchCache.get(cacheKey, async () => {
const listed =
route.kind === 'ssh'
? await this.listRemoteMobileFiles(
worktree.path,
route.provider,
MOBILE_FILE_PATH_SEARCH_CACHE_LIMIT + 1
)
? []
: await listQuickOpenFiles(
worktree.path,
store,
@@ -155,36 +143,18 @@ export class RuntimeFileSearch {
const target = await this.host.resolveRuntimeFileTarget(worktreeSelector)
const { worktree } = target
const route = runtimeFileRouteForTarget(target)
const quickOpenSearchVersion =
route.kind !== 'ssh'
? QUICK_OPEN_SEARCH_VERSION
: (await route.provider?.supportsQuickOpenSearch?.({ signal, minimumVersion: 3 }))
? 3
: (await route.provider?.supportsQuickOpenSearch?.({ signal, minimumVersion: 2 }))
? 2
: (await route.provider?.supportsQuickOpenSearch?.({ signal, minimumVersion: 1 }))
? 1
: 0
// Why: an `ssh:` host reports no matches (version 0) rather than failing the keystroke.
const quickOpenSearchVersion = route.kind === 'ssh' ? 0 : QUICK_OPEN_SEARCH_VERSION
const result =
!query.trim() || isQuickOpenQueryTooLarge(query)
!query.trim() || isQuickOpenQueryTooLarge(query) || route.kind === 'ssh'
? { paths: [], totalCount: 0, truncated: false }
: route.kind === 'ssh'
? await searchRemoteQuickOpenFilePaths(
worktree.path,
route.provider,
query,
limit,
excludePaths,
signal,
options
)
: await searchHostQuickOpenFilePaths(worktree.path, this.host.requireStore(), {
...options,
query,
limit,
excludePaths,
signal
})
: await searchHostQuickOpenFilePaths(worktree.path, this.host.requireStore(), {
...options,
query,
limit,
excludePaths,
signal
})
return {
worktree: worktree.id,
rootPath: worktree.path,
@@ -231,40 +201,8 @@ export class RuntimeFileSearch {
const target = await this.host.resolveRuntimeFileTarget(worktreeSelector)
const route = runtimeFileRouteForTarget(target)
if (route.kind === 'ssh') {
// Why: quick-open listings degrade to empty for an unreachable host rather than throwing.
const provider = route.provider
if (!provider) {
return []
}
const maxResults =
options.maxResults ??
(options.maxContentBytes === undefined ? undefined : QUICK_OPEN_LISTING_MAX_RESULTS)
if (
(options.candidatePaths !== undefined ||
options.includeIgnored === false ||
options.followSymlinks) &&
!(await provider.supportsQuickOpenSearch?.({
signal: options.signal,
minimumVersion: options.candidatePaths !== undefined ? 3 : 2
}))
) {
throw new Error(
options.candidatePaths !== undefined
? 'Update the remote host to validate Quick Open recent files.'
: 'Update the remote host to use Quick Open listing options.'
)
}
const files = await provider.listFiles(target.worktree.path, {
excludePaths: options.excludePaths,
...(options.candidatePaths === undefined ? {} : { candidatePaths: options.candidatePaths }),
includeIgnored: options.includeIgnored,
followSymlinks: options.followSymlinks,
maxResults,
signal: options.signal
})
return options.maxContentBytes === undefined
? files
: limitQuickOpenFilesBySerializedBytes(files, options.maxContentBytes)
// Why: quick-open listings degrade to empty for an `ssh:` host rather than throwing.
return []
}
return listQuickOpenFiles(
target.worktree.path,
@@ -277,17 +215,4 @@ export class RuntimeFileSearch {
options
)
}
// `null` provider is the caller's "this host is unreachable" answer, not "list it here".
private async listRemoteMobileFiles(
rootPath: string,
provider: IFilesystemProvider | null,
maxResults?: number,
signal?: AbortSignal
): Promise<string[]> {
if (!provider) {
return []
}
return provider.listFiles(rootPath, { maxResults, signal })
}
}
@@ -1,9 +1,9 @@
// `resolveRuntimeFileTarget` read `store.getRepo(worktree.repoId)?.connectionId` and never looked
// at `worktree.hostId`, so one arbitrarily chosen row decided the execution host for ~30 downstream
// filesystem dispatches. `undefined` there meant "runtime host", "unresolved" and "genuinely local"
// at once (#11163). These cases pin all four answers end to end, through the real SSH filesystem
// provider table. Companion to runtime-git-target-execution-host.test.ts.
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
// at once (#11163). These cases pin all four answers end to end; an `ssh:` host is refused by
// name. Companion to runtime-git-target-execution-host.test.ts.
import { beforeEach, describe, expect, it, vi } from 'vitest'
vi.mock('electron', () => ({
BrowserWindow: { fromId: vi.fn(() => null) },
@@ -22,10 +22,7 @@ vi.mock('../ipc/markdown-documents', async () => ({
}))
import { ExecutionHostNotDispatchableError } from '../providers/execution-host-provider-dispatch'
import {
registerSshFilesystemProvider,
unregisterSshFilesystemProvider
} from '../providers/ssh-filesystem-dispatch'
import { SshHostNotServedError } from '../providers/ssh-host-not-served-error'
import { OrcaRuntimeService } from './orca-runtime'
const REMOTE_PATH = '/srv/app-feature'
@@ -63,36 +60,25 @@ function makeRuntime(repos: readonly Record<string, unknown>[], hostId?: string)
return runtime
}
function stubProvider() {
return { listMarkdownDocuments: vi.fn().mockResolvedValue([]) }
async function expectRefusedFor(promise: Promise<unknown>, connectionId: string): Promise<void> {
const error = await promise.then(
() => null,
(caught: unknown) => caught
)
expect(error).toBeInstanceOf(SshHostNotServedError)
expect(error).toMatchObject({ connectionId })
expect(mocks.listMarkdownDocuments).not.toHaveBeenCalled()
}
describe('runtime file target execution host', () => {
const registered: string[] = []
function register(connectionId: string) {
const provider = stubProvider()
registerSshFilesystemProvider(connectionId, provider as never)
registered.push(connectionId)
return provider
}
beforeEach(() => {
vi.restoreAllMocks()
mocks.listMarkdownDocuments.mockReset().mockResolvedValue([])
})
afterEach(() => {
for (const connectionId of registered.splice(0)) {
unregisterSshFilesystemProvider(connectionId)
}
})
// The case whose absence let the original cross-host leak through review: two SSH hosts
// registered at once, and the rival row is the one `getRepo` returns first.
it('lists an ssh workspace from the host it names, not from a rival row on another ssh host', async () => {
const openclaw = register('openclaw')
const m4air = register('m4air')
// The case whose absence let the original cross-host leak through review: two SSH hosts,
// and the rival row is the one `getRepo` returns first.
it('routes an ssh workspace to the host it names, not to a rival row on another ssh host', async () => {
const runtime = makeRuntime(
[
{ id: 'repo-shared', path: '/home/me/app', connectionId: 'openclaw' },
@@ -101,31 +87,21 @@ describe('runtime file target execution host', () => {
'ssh:m4air'
)
await runtime.listRuntimeMarkdownDocuments(`id:${WORKTREE_ID}`)
expect(m4air.listMarkdownDocuments).toHaveBeenCalledWith(REMOTE_PATH)
expect(openclaw.listMarkdownDocuments).not.toHaveBeenCalled()
expect(mocks.listMarkdownDocuments).not.toHaveBeenCalled()
await expectRefusedFor(runtime.listRuntimeMarkdownDocuments(`id:${WORKTREE_ID}`), 'm4air')
})
it("routes to the workspace's host even when the only repo row names a different ssh host", async () => {
const openclaw = register('openclaw')
const m4air = register('m4air')
const runtime = makeRuntime(
[{ id: 'repo-shared', path: '/home/me/app', connectionId: 'openclaw' }],
'ssh:m4air'
)
await runtime.listRuntimeMarkdownDocuments(`id:${WORKTREE_ID}`)
expect(m4air.listMarkdownDocuments).toHaveBeenCalledWith(REMOTE_PATH)
expect(openclaw.listMarkdownDocuments).not.toHaveBeenCalled()
await expectRefusedFor(runtime.listRuntimeMarkdownDocuments(`id:${WORKTREE_ID}`), 'm4air')
})
// `local` has no SSH namespace to nest in, so a surviving `connectionId` is a row contradicting
// itself. The old shape handed it out and read a local workspace off a remote host.
it('ignores a stale connection on a row that declares itself local', async () => {
const m4air = register('m4air')
const runtime = makeRuntime(
[
{
@@ -140,15 +116,11 @@ describe('runtime file target execution host', () => {
await runtime.listRuntimeMarkdownDocuments(`id:${WORKTREE_ID}`)
expect(m4air.listMarkdownDocuments).not.toHaveBeenCalled()
expect(mocks.listMarkdownDocuments).toHaveBeenCalledWith(REMOTE_PATH, {})
})
// A `runtime:` row's `connectionId` names a target in the *server's* namespace. Reading it here
// reaches a same-named target on this client — a silent-wrong-host answer, worse than the
// silent-local one it replaced.
it('refuses a runtime host whose nested ssh target is also registered on this client', async () => {
const impostor = register('nested-1')
// A `runtime:` row's `connectionId` names a target in the *server's* namespace, never this client's.
it('refuses a runtime host whose row carries a nested ssh target', async () => {
const runtime = makeRuntime(
[
{
@@ -164,7 +136,6 @@ describe('runtime file target execution host', () => {
await expect(runtime.listRuntimeMarkdownDocuments(`id:${WORKTREE_ID}`)).rejects.toThrow(
ExecutionHostNotDispatchableError
)
expect(impostor.listMarkdownDocuments).not.toHaveBeenCalled()
expect(mocks.listMarkdownDocuments).not.toHaveBeenCalled()
})
@@ -187,7 +158,6 @@ describe('runtime file target execution host', () => {
})
it('refuses rather than guessing when rival rows disagree and the workspace names no host', async () => {
register('m4air')
const runtime = makeRuntime([
{ id: 'repo-shared', path: '/srv/app', connectionId: 'm4air' },
{ id: 'repo-shared', path: '/home/me/app' }
@@ -199,13 +169,10 @@ describe('runtime file target execution host', () => {
expect(mocks.listMarkdownDocuments).not.toHaveBeenCalled()
})
it('still answers from the single row when the workspace names no host', async () => {
const m4air = register('m4air')
it('still routes by the single row when the workspace names no host', async () => {
const runtime = makeRuntime([{ id: 'repo-shared', path: '/srv/app', connectionId: 'm4air' }])
await runtime.listRuntimeMarkdownDocuments(`id:${WORKTREE_ID}`)
expect(m4air.listMarkdownDocuments).toHaveBeenCalledWith(REMOTE_PATH)
await expectRefusedFor(runtime.listRuntimeMarkdownDocuments(`id:${WORKTREE_ID}`), 'm4air')
})
// Losing contact with a remote host is never evidence that its files are here
@@ -225,8 +192,6 @@ describe('runtime file target execution host', () => {
// The mutation guard recomputed the host from `connectionId`, so a client could satisfy a host
// expectation the workspace never named.
it('rejects a mutation whose expected host is the row rather than the resolved one', async () => {
register('m4air')
register('openclaw')
const runtime = makeRuntime(
[{ id: 'repo-shared', path: '/home/me/app', connectionId: 'openclaw' }],
'ssh:m4air'
@@ -4,11 +4,9 @@ import {
pendingRuntimeFileWatcherUnsubscribes,
runtimeFileWatcherLeasesByOwnerAndRoot,
runtimeWatcherReleaseKey,
sshFileExplorerWatchRearms,
trackRuntimeFileWatcherUnsubscribe
} from './runtime-file-commands-mobile-file-list-limit'
import { isWatcherProcessFailure } from '../ipc/parcel-watcher-process-failure'
import { stopSshFileExplorerWatchRearms } from './runtime-file-commands-ssh-file-watcher-rearm'
export function registerRuntimeFileWatcherRelease(
runtimeId: string,
@@ -170,8 +168,5 @@ export function _resetRuntimeFileWatcherLeasesForTests(): void {
for (const lease of leases) {
lease.forget()
}
for (const key of Array.from(sshFileExplorerWatchRearms.keys())) {
stopSshFileExplorerWatchRearms(key)
}
runtimeFileWatcherLeasesByOwnerAndRoot.clear()
}
@@ -2,8 +2,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'
import type { RuntimeGitCommandHost, RuntimeGitTarget } from './runtime-git-command-target'
const mocks = vi.hoisted(() => ({
getBranchCompare: vi.fn(),
getSshGitProvider: vi.fn()
getBranchCompare: vi.fn()
}))
vi.mock('../git/status', () => ({
@@ -15,11 +14,8 @@ vi.mock('../git/status', () => ({
}))
vi.mock('../git/repo', () => ({ getRemoteCommitUrl: vi.fn(), getRemoteFileUrl: vi.fn() }))
vi.mock('../git/runner', () => ({ awaitWindowsHostGitEnvironmentReady: vi.fn() }))
vi.mock('../providers/ssh-git-dispatch', () => ({
getSshGitProvider: mocks.getSshGitProvider,
SSH_GIT_PROVIDER_UNAVAILABLE_MESSAGE: 'provider unavailable'
}))
import { SshHostNotServedError } from '../providers/ssh-host-not-served-error'
import { RuntimeGitDiffCommands } from './runtime-git-diff-commands'
function makeCommands(overrides: Partial<RuntimeGitTarget> = {}): RuntimeGitDiffCommands {
@@ -69,16 +65,12 @@ describe('RuntimeGitDiffCommands branch-compare admission', () => {
})
})
it('forwards background admission to the SSH execution host', async () => {
const getBranchCompare = vi.fn().mockResolvedValue({ summary: {}, entries: [] })
mocks.getSshGitProvider.mockReturnValue({ getBranchCompare })
it('refuses an SSH execution host without running git here', async () => {
const commands = makeCommands({ executionHostId: 'ssh:conn-1' })
await commands.getRuntimeGitBranchCompare('id:wt-1', 'origin/main', 'background')
expect(getBranchCompare).toHaveBeenCalledWith('C:\\repo', 'origin/main', {
admissionTier: 'background'
})
await expect(
commands.getRuntimeGitBranchCompare('id:wt-1', 'origin/main', 'background')
).rejects.toThrow(SshHostNotServedError)
expect(mocks.getBranchCompare).not.toHaveBeenCalled()
})
})
@@ -5,7 +5,7 @@ vi.mock('../git/status', () => ({ getStatus }))
import { ExecutionHostNotDispatchableError } from '../providers/execution-host-provider-dispatch'
import { createLocalGitProvider } from '../providers/local-git-provider'
import { registerSshGitProvider, unregisterSshGitProvider } from '../providers/ssh-git-dispatch'
import { SshHostNotServedError } from '../providers/ssh-host-not-served-error'
import {
localGitOptionsForTarget,
requireRuntimeGitProvider,
@@ -25,31 +25,8 @@ function target(overrides: Partial<RuntimeGitTarget>): RuntimeGitTarget {
}
describe('runtime Git target routing', () => {
const registered: string[] = []
function register(connectionId: string) {
const provider = { getStatus: async () => ({ entries: [] }) }
registerSshGitProvider(connectionId, provider as never)
registered.push(connectionId)
return provider
}
afterEach(() => {
for (const connectionId of registered.splice(0)) {
unregisterSshGitProvider(connectionId)
}
})
it('routes each ssh host to its own provider', () => {
const m4air = register('m4air')
const openclaw = register('openclaw')
expect(runtimeGitRouteForTarget(target({ executionHostId: 'ssh:m4air' }))).toEqual({
kind: 'ssh',
connectionId: 'm4air',
provider: m4air
})
expect(requireRuntimeGitProvider(target({ executionHostId: 'ssh:openclaw' }))).toBe(openclaw)
getStatus.mockReset()
})
it('answers `local` with a provider bound to this worktree, built per call', async () => {
@@ -68,18 +45,17 @@ describe('runtime Git target routing', () => {
})
// Loss of contact is never evidence of locality (docs/reference/ssh-execution-boundary.md).
it('keeps an unreachable ssh host remote instead of degrading it to local', () => {
it('keeps an ssh host remote instead of degrading it to local', () => {
const route = runtimeGitRouteForTarget(target({ executionHostId: 'ssh:gone' }))
expect(route).toEqual({ kind: 'ssh', connectionId: 'gone', provider: null })
expect(route).toEqual({ kind: 'ssh', connectionId: 'gone' })
expect(() => requireRuntimeGitProvider(target({ executionHostId: 'ssh:gone' }))).toThrow(
/Remote connection dropped/
SshHostNotServedError
)
expect(getStatus).not.toHaveBeenCalled()
})
it('refuses a runtime host even when a same-named target is registered here', () => {
register('nested-1')
it('refuses a runtime host instead of routing it', () => {
expect(() => runtimeGitRouteForTarget(target({ executionHostId: 'runtime:env-a' }))).toThrow(
ExecutionHostNotDispatchableError
)
+16 -26
View File
@@ -9,8 +9,7 @@ import {
resolveGitRouteForHost
} from '../providers/execution-host-provider-dispatch'
import type { createLocalGitProvider, LocalGitProvider } from '../providers/local-git-provider'
import { getSshGitProvider, sshGitProviderMissingError } from '../providers/ssh-git-dispatch'
import type { SshGitProvider } from '../providers/ssh-git-provider'
import { SshHostNotServedError } from '../providers/ssh-host-not-served-error'
import type { CommitMessageAgentEnvironmentResolvers } from '../text-generation/commit-message-agent-environment'
import type { PullRequestLinkedIssueMeta } from '../source-control/pull-request-linked-issue'
import { normalizeRuntimeRelativePath } from './runtime-relative-paths'
@@ -62,8 +61,8 @@ export type RuntimeGitCommandHost = {
*/
export type RuntimeGitRoute =
| { kind: 'local'; createProvider: typeof createLocalGitProvider }
/** `provider: null` is "remote and currently unreachable" — never "run it here". */
| { kind: 'ssh'; connectionId: string; provider: SshGitProvider | null }
/** An SSH target this process does not serve; it fails or reports unavailable, never runs here. */
| { kind: 'ssh'; connectionId: string }
export function runtimeGitRouteForTarget(target: RuntimeGitTarget): RuntimeGitRoute {
const route = resolveGitRouteForHost(target.executionHostId)
@@ -71,40 +70,31 @@ export function runtimeGitRouteForTarget(target: RuntimeGitTarget): RuntimeGitRo
case 'local':
return { kind: 'local', createProvider: route.createProvider }
case 'ssh':
return {
kind: 'ssh',
connectionId: route.connectionId,
provider: getSshGitProvider(route.connectionId) ?? null
}
return { kind: 'ssh', connectionId: route.connectionId }
case 'runtime':
throw new ExecutionHostNotDispatchableError(route.hostId)
}
}
/** A provider for the target's own host; an unreachable SSH host and a `runtime:` host both throw. */
export function requireRuntimeGitProvider(
target: RuntimeGitTarget,
route: RuntimeGitRoute = runtimeGitRouteForTarget(target)
): LocalGitProvider {
/** The local route for work this process runs itself; `ssh:` and `runtime:` hosts throw. */
export function requireLocalRuntimeGitRoute(
target: RuntimeGitTarget
): Extract<RuntimeGitRoute, { kind: 'local' }> {
const route = runtimeGitRouteForTarget(target)
if (route.kind === 'ssh') {
return requireSshRuntimeGitProvider(route)
throw new SshHostNotServedError(route.connectionId)
}
return route.createProvider({
return route
}
/** A provider for the target's own host; an `ssh:` host and a `runtime:` host both throw. */
export function requireRuntimeGitProvider(target: RuntimeGitTarget): LocalGitProvider {
return requireLocalRuntimeGitRoute(target).createProvider({
...localGitOptionsForTarget(target),
getSharedLinkPaths: () => (target.repo ? getWorktreeSharedLinkPaths(target.repo) : [])
})
}
/** For the few commands whose SSH arm needs relay-only methods. */
export function requireSshRuntimeGitProvider(
route: Extract<RuntimeGitRoute, { kind: 'ssh' }>
): SshGitProvider {
if (!route.provider) {
throw sshGitProviderMissingError(route.connectionId)
}
return route.provider
}
export function localGitOptionsForTarget(target: RuntimeGitTarget): GitRuntimeOptions {
// WSL routing describes *this* machine; no remote host may inherit it.
return target.executionHostId === LOCAL_EXECUTION_HOST_ID ? (target.localGitOptions ?? {}) : {}
+3 -23
View File
@@ -10,16 +10,15 @@ import { normalizeRuntimeRelativePath } from './runtime-relative-paths'
import {
localGitOptionsForTarget,
normalizeRuntimeGitRelativePath,
requireLocalRuntimeGitRoute,
requireRuntimeGitProvider,
requireSshRuntimeGitProvider,
runtimeGitRouteForTarget,
type RuntimeGitCommandHost
} from './runtime-git-command-target'
export class RuntimeGitDiffCommands {
constructor(private readonly host: RuntimeGitCommandHost) {}
// Why: cap both local and forwarded SSH payloads after execution-host dispatch.
// Why: cap the payload after execution-host dispatch.
async getRuntimeGitDiff(
worktreeSelector: string,
filePath: string,
@@ -69,26 +68,7 @@ export class RuntimeGitDiffCommands {
const target = await this.host.resolveRuntimeGitTarget(worktreeSelector)
const relativePath = normalizeRuntimeGitRelativePath(filePath)
const oldRelativePath = oldPath ? normalizeRuntimeGitRelativePath(oldPath) : undefined
const route = runtimeGitRouteForTarget(target)
if (route.kind === 'ssh') {
const provider = requireSshRuntimeGitProvider(route)
const results = await provider.getBranchDiff(target.worktree.path, compare.mergeBase, {
includePatch: true,
headOid: compare.headOid,
filePath: relativePath,
oldPath: oldRelativePath
})
return assertGitDiffWithinTransportBudget(
results[0] ?? {
kind: 'text',
originalContent: '',
modifiedContent: '',
originalIsBinary: false,
modifiedIsBinary: false
},
maxContentBytes
)
}
requireLocalRuntimeGitRoute(target)
return assertGitDiffWithinTransportBudget(
await getBranchDiff(
target.worktree.path,
@@ -8,7 +8,6 @@ const mocks = vi.hoisted(() => ({
gitExecFileAsync: vi.fn(),
getPullRequestDraftContext: vi.fn(),
loadPullRequestLinkedIssue: vi.fn(),
getSshGitProvider: vi.fn(),
prepareLocalCommitMessageAgentEnv: vi.fn(),
generateCommitMessageFromContext: vi.fn(),
generatePullRequestFieldsFromContext: vi.fn(),
@@ -29,10 +28,6 @@ vi.mock('../source-control/pull-request-linked-issue', () => ({
return linkedLookup.run()
}
}))
vi.mock('../providers/ssh-git-dispatch', () => ({
getSshGitProvider: mocks.getSshGitProvider,
SSH_GIT_PROVIDER_UNAVAILABLE_MESSAGE: 'provider unavailable'
}))
vi.mock('../text-generation/commit-message-agent-environment', () => ({
prepareLocalCommitMessageAgentEnv: mocks.prepareLocalCommitMessageAgentEnv
}))
@@ -44,6 +39,7 @@ vi.mock('../source-control/pull-request-template', () => ({
resolveHostedReviewBodyForGeneration: mocks.resolveHostedReviewBodyForGeneration
}))
import { SSH_HOST_NOT_SERVED_MESSAGE } from '../providers/ssh-host-not-served-error'
import { RuntimeGitGenerationCommands } from './runtime-git-generation-commands'
const params = { agentId: 'codex' as const, model: 'gpt-5.5' }
@@ -140,49 +136,29 @@ describe('RuntimeGitGenerationCommands admission', () => {
})
expect(mocks.loadPullRequestLinkedIssue).toHaveBeenCalledWith(
expect.objectContaining({
connectionId: undefined,
localGitOptions: { wslDistro: 'Ubuntu', admissionTier: 'interactive' }
})
)
})
it('keeps SSH pull-request context reads on the remote provider', async () => {
const exec = vi.fn().mockResolvedValue({ stdout: '', stderr: '' })
mocks.getSshGitProvider.mockReturnValue({ exec, executeCommitMessagePlan: vi.fn() })
mocks.getPullRequestDraftContext.mockImplementation(async (execute) => {
await execute(['log', '--oneline'])
await execute(['show-ref', '--verify'], { maxBuffer: 456, timeoutMs: 789 })
return pullRequestContext
})
const commands = makeCommands(
makeTarget('/remote/repo', {
executionHostId: 'ssh:conn-1',
localGitOptions: { wslDistro: 'Ubuntu' }
})
)
it('reports an SSH pull-request target unserved without reading git anywhere', async () => {
const commands = makeCommands(makeTarget('/remote/repo', { executionHostId: 'ssh:conn-1' }))
await commands.generateRuntimePullRequestFields(
'id:wt-1',
{ base: 'main', title: '', body: '', draft: false },
settingsOverride
)
expect(exec).toHaveBeenCalledWith(['log', '--oneline'], '/remote/repo')
expect(exec).toHaveBeenCalledWith(['show-ref', '--verify'], '/remote/repo', { timeoutMs: 789 })
await expect(
commands.generateRuntimePullRequestFields(
'id:wt-1',
{ base: 'main', title: '', body: '', draft: false },
settingsOverride
)
).resolves.toEqual({ success: false, error: SSH_HOST_NOT_SERVED_MESSAGE })
expect(mocks.getPullRequestDraftContext).not.toHaveBeenCalled()
expect(mocks.gitExecFileAsync).not.toHaveBeenCalled()
expect(mocks.loadPullRequestLinkedIssue).toHaveBeenCalledWith(
expect.objectContaining({ connectionId: 'conn-1', localGitOptions: {} })
)
expect(mocks.loadPullRequestLinkedIssue).not.toHaveBeenCalled()
})
describe.each(['local', 'SSH'])('linked-issue lifetime on %s', (host) => {
describe('linked-issue lifetime', () => {
function commands(): RuntimeGitGenerationCommands {
mocks.getSshGitProvider.mockReturnValue({ exec: vi.fn(), executeCommitMessagePlan: vi.fn() })
return makeCommands(
makeTarget('/repo', {
executionHostId: host === 'SSH' ? 'ssh:conn-1' : 'local'
})
)
return makeCommands(makeTarget('/repo'))
}
const input = { base: 'main', title: '', body: '', draft: false, provider: 'gitlab' as const }
@@ -4,7 +4,7 @@ import type { HostedReviewProvider } from '../../shared/hosted-review'
import { withLinkedIssueDraftContext } from '../../shared/source-control-ai-action-variables'
import type { TuiAgent } from '../../shared/tui-agent'
import { getStagedCommitContext } from '../git/status'
import { SSH_GIT_PROVIDER_UNAVAILABLE_MESSAGE } from '../providers/ssh-git-dispatch'
import { SSH_HOST_NOT_SERVED_MESSAGE } from '../providers/ssh-host-not-served-error'
import { loadPullRequestLinkedIssue } from '../source-control/pull-request-linked-issue'
import { resolveHostedReviewBodyForGeneration } from '../source-control/pull-request-template'
import { prepareLocalCommitMessageAgentEnv } from '../text-generation/commit-message-agent-environment'
@@ -12,7 +12,6 @@ import {
cancelGenerateCommitMessageLocal,
cancelGeneratePullRequestFieldsLocal,
discoverCommitMessageModelsLocal,
discoverCommitMessageModelsRemote,
generateCommitMessageFromContext,
generatePullRequestFieldsFromContext,
resolveCommitMessageSettings,
@@ -65,28 +64,7 @@ export class RuntimeGitGenerationCommands {
}
if (route.kind === 'ssh') {
const provider = route.provider
if (!provider) {
return { success: false, error: SSH_GIT_PROVIDER_UNAVAILABLE_MESSAGE }
}
let context: CommitMessageDraftContext | null
try {
context = await provider.getStagedCommitContext(target.worktree.path)
} catch (error) {
console.error('[runtime-git] Failed to read remote staged commit context:', error)
return { success: false, error: 'Failed to read staged changes.' }
}
if (!context) {
return { success: false, error: 'No staged changes to summarize.' }
}
context = withLinkedIssueDraftContext(context, linkedIssueForTarget(this.host, target))
return generateCommitMessageFromContext(context, resolvedSettings.params, {
kind: 'remote',
cwd: target.worktree.path,
execute: (plan, cwd, timeoutMs, operation) =>
provider.executeCommitMessagePlan(plan, cwd, timeoutMs, operation),
missingBinaryLocation: 'remote PATH'
})
return { success: false, error: SSH_HOST_NOT_SERVED_MESSAGE }
}
let context: CommitMessageDraftContext | null
@@ -121,10 +99,9 @@ export class RuntimeGitGenerationCommands {
async cancelRuntimeGenerateCommitMessage(worktreeSelector: string): Promise<{ ok: true }> {
const target = await this.host.resolveRuntimeGitTarget(worktreeSelector)
const route = runtimeGitRouteForTarget(target)
// Why: an `ssh:` cancel is a no-op; the local registry is keyed by path and would abort an
// unrelated generation running here for the same path.
if (route.kind === 'ssh') {
// Cancelling an unreachable host is a no-op, not a local cancel: the local registry is keyed
// by path and would abort an unrelated generation running here for the same path.
await route.provider?.cancelGenerateCommitMessage(target.worktree.path, 'commit-message')
return { ok: true }
}
cancelGenerateCommitMessageLocal(target.worktree.path)
@@ -164,23 +141,18 @@ export class RuntimeGitGenerationCommands {
return { success: false, error: resolvedSettings.error }
}
const provider = route.kind === 'ssh' ? route.provider : null
if (route.kind === 'ssh' && !provider) {
return { success: false, error: SSH_GIT_PROVIDER_UNAVAILABLE_MESSAGE }
if (route.kind === 'ssh') {
return { success: false, error: SSH_HOST_NOT_SERVED_MESSAGE }
}
const issueMeta = linkedIssueMetaForTarget(this.host, target)
const linkedIssueDetailsPromise = loadPullRequestLinkedIssue({
meta: issueMeta,
provider: input.provider,
repoPath: target.worktree.path,
connectionId: route.kind === 'ssh' ? route.connectionId : undefined,
localGitOptions:
route.kind === 'ssh'
? {}
: {
...localGitOptionsForTarget(target),
admissionTier: 'interactive'
}
localGitOptions: {
...localGitOptionsForTarget(target),
admissionTier: 'interactive'
}
})
void linkedIssueDetailsPromise.catch(() => undefined)
let context: Awaited<ReturnType<typeof getPullRequestDraftContext>>
@@ -188,11 +160,10 @@ export class RuntimeGitGenerationCommands {
const currentBody = await resolveHostedReviewBodyForGeneration({
body: input.body,
repoPath: target.worktree.path,
connectionId: route.kind === 'ssh' ? route.connectionId : undefined,
provider: input.provider,
useTemplate: input.useTemplate
})
context = await getPullRequestDraftContext(pullRequestDraftGitExec(target, route), {
context = await getPullRequestDraftContext(pullRequestDraftGitExec(target), {
base: input.base,
currentTitle: input.title,
currentBody,
@@ -214,15 +185,6 @@ export class RuntimeGitGenerationCommands {
...(linkedIssueDetails ? { linkedIssueDetails } : {})
}
if (route.kind === 'ssh') {
return generatePullRequestFieldsFromContext(context, resolvedSettings.params, {
kind: 'remote',
cwd: target.worktree.path,
execute: (plan, cwd, timeoutMs, operation) =>
provider!.executeCommitMessagePlan(plan, cwd, timeoutMs, operation),
missingBinaryLocation: 'remote PATH'
})
}
const localEnv = await prepareLocalCommitMessageAgentEnv(
resolvedSettings.params.agentId,
this.host.getCommitMessageAgentEnvironment?.(),
@@ -242,7 +204,6 @@ export class RuntimeGitGenerationCommands {
const target = await this.host.resolveRuntimeGitTarget(worktreeSelector)
const route = runtimeGitRouteForTarget(target)
if (route.kind === 'ssh') {
await route.provider?.cancelGenerateCommitMessage(target.worktree.path, 'pull-request-fields')
return { ok: true }
}
cancelGeneratePullRequestFieldsLocal(target.worktree.path)
@@ -261,16 +222,7 @@ export class RuntimeGitGenerationCommands {
this.host.getRuntimeSettings().agentCmdOverrides?.[typedAgentId]
const route = runtimeGitRouteForTarget(target)
if (route.kind === 'ssh') {
const provider = route.provider
if (!provider) {
return { success: false, error: `No git provider for connection "${route.connectionId}"` }
}
return discoverCommitMessageModelsRemote(
typedAgentId,
target.worktree.path,
(plan, cwd, timeoutMs) => provider.executeCommitMessagePlan(plan, cwd, timeoutMs),
agentCommandOverride
)
return { success: false, error: `No git provider for connection "${route.connectionId}"` }
}
const localEnv = await prepareLocalCommitMessageAgentEnv(
typedAgentId,
@@ -9,28 +9,16 @@ import type { SourceControlAiOperation } from '../../shared/source-control-ai-ty
import type { CommitMessageAgentRuntimeTarget } from '../text-generation/commit-message-agent-environment'
import type { CommitMessageGenerationTarget } from '../text-generation/commit-message-text-generation'
import type { PullRequestLinkedIssueMeta } from '../source-control/pull-request-linked-issue'
import { execSshReviewDraft } from '../providers/ssh-review-draft-context'
import {
localGitOptionsForTarget,
type RuntimeGitCommandHost,
type RuntimeGitRoute,
type RuntimeGitTarget
} from './runtime-git-command-target'
type PullRequestDraftGitExec = Parameters<typeof getPullRequestDraftContext>[0]
/** Runs the PR draft-context probes on whichever host `route` resolved to. */
export function pullRequestDraftGitExec(
target: RuntimeGitTarget,
route: RuntimeGitRoute
): PullRequestDraftGitExec {
if (route.kind === 'ssh') {
const provider = route.provider
if (!provider) {
throw new Error('ssh_git_provider_unavailable')
}
return (argv, options) => execSshReviewDraft(provider, argv, target.worktree.path, options)
}
/** Runs the PR draft-context probes on a local target. */
export function pullRequestDraftGitExec(target: RuntimeGitTarget): PullRequestDraftGitExec {
return (argv, options) =>
gitExecFileAsync(argv, {
cwd: target.worktree.path,
@@ -6,12 +6,12 @@ import type * as GitStatusModule from '../git/status'
import type * as GitForkSyncModule from '../git/fork-sync'
import type { ResolvedRuntimeGitWorktree } from './runtime-git-command-target'
import { RuntimeGitSyncCommands } from './runtime-git-sync-commands'
import { SshHostNotServedError } from '../providers/ssh-host-not-served-error'
const mocks = vi.hoisted(() => ({
abortMerge: vi.fn(),
abortRebase: vi.fn(),
commitChanges: vi.fn(),
getSshGitProvider: vi.fn(),
gitSyncForkDefaultBranch: vi.fn(),
gitFastForward: vi.fn(),
gitFetch: vi.fn(),
@@ -37,10 +37,6 @@ vi.mock('../git/remote', async () => ({
gitPull: mocks.gitPull
}))
vi.mock('../providers/ssh-git-dispatch', () => ({
getSshGitProvider: mocks.getSshGitProvider
}))
const worktree = {
id: 'wt-1',
path: '/workspace/repo'
@@ -94,17 +90,7 @@ describe('RuntimeGitSyncCommands admission', () => {
)
})
it('keeps remote runtime git actions owned by the SSH provider', async () => {
const provider = {
abortMerge: vi.fn(),
abortRebase: vi.fn(),
commit: vi.fn().mockResolvedValue({ success: true }),
fastForwardBranch: vi.fn(),
fetchRemote: vi.fn(),
syncForkDefaultBranch: vi.fn().mockResolvedValue({ status: 'up-to-date' }),
pullBranch: vi.fn()
}
mocks.getSshGitProvider.mockReturnValue(provider)
it('refuses every runtime git action on an SSH host without running git here', async () => {
const commands = new RuntimeGitSyncCommands({
resolveRuntimeGitTarget: async () => ({
worktree,
@@ -113,27 +99,19 @@ describe('RuntimeGitSyncCommands admission', () => {
getRuntimeSettings: () => ({}) as GlobalSettings
})
await commands.abortRuntimeGitMerge('id:wt-1')
await commands.abortRuntimeGitRebase('id:wt-1')
await commands.fetchRuntimeGit('id:wt-1', pushTarget)
await commands.syncRuntimeGitForkDefaultBranch('id:wt-1', expectedUpstream)
await commands.pullRuntimeGit('id:wt-1', pushTarget)
await commands.fastForwardRuntimeGit('id:wt-1', pushTarget)
await commands.commitRuntimeGit('id:wt-1', 'feat: keep execution remote')
expect(provider.abortMerge).toHaveBeenCalledWith(worktree.path)
expect(provider.abortRebase).toHaveBeenCalledWith(worktree.path)
expect(provider.fetchRemote).toHaveBeenCalledWith(worktree.path, pushTarget)
expect(provider.syncForkDefaultBranch).toHaveBeenCalledWith(worktree.path, expectedUpstream)
expect(provider.pullBranch).toHaveBeenCalledWith(worktree.path, pushTarget)
expect(provider.fastForwardBranch).toHaveBeenCalledWith(worktree.path, pushTarget)
expect(provider.commit).toHaveBeenCalledWith(worktree.path, 'feat: keep execution remote')
await expect(commands.abortRuntimeGitMerge('id:wt-1')).rejects.toThrow(SshHostNotServedError)
await expect(commands.fetchRuntimeGit('id:wt-1', pushTarget)).rejects.toThrow(
SshHostNotServedError
)
await expect(commands.pullRuntimeGit('id:wt-1', pushTarget)).rejects.toThrow(
SshHostNotServedError
)
await expect(commands.commitRuntimeGit('id:wt-1', 'feat: remote')).rejects.toThrow(
SshHostNotServedError
)
expect(mocks.abortMerge).not.toHaveBeenCalled()
expect(mocks.abortRebase).not.toHaveBeenCalled()
expect(mocks.gitFetch).not.toHaveBeenCalled()
expect(mocks.gitSyncForkDefaultBranch).not.toHaveBeenCalled()
expect(mocks.gitPull).not.toHaveBeenCalled()
expect(mocks.gitFastForward).not.toHaveBeenCalled()
expect(mocks.commitChanges).not.toHaveBeenCalled()
})
})
+11 -31
View File
@@ -1,16 +1,11 @@
import type { GitForkSyncExpectedUpstream, GitForkSyncResult } from '../../shared/git-fork-sync'
import type { GitUpstreamStatus } from '../../shared/git-status-types'
import type { GitPushTarget } from '../../shared/worktree/types'
import {
materializeWorktreePushTargetRemote,
materializeWorktreePushTargetRemoteSsh
} from '../ipc/worktree-remote'
import { materializeWorktreePushTargetRemote } from '../ipc/worktree-remote'
import type { LocalGitProvider } from '../providers/local-git-provider'
import {
localGitOptionsForTarget,
requireRuntimeGitProvider,
requireSshRuntimeGitProvider,
runtimeGitRouteForTarget,
type RuntimeGitCommandHost,
type RuntimeGitTarget
} from './runtime-git-command-target'
@@ -30,31 +25,16 @@ export class RuntimeGitSyncCommands {
target: RuntimeGitTarget,
pushTarget: GitPushTarget | undefined
): Promise<{ provider: LocalGitProvider; pushTarget: GitPushTarget | undefined }> {
const route = runtimeGitRouteForTarget(target)
let provider: LocalGitProvider
let materialized: GitPushTarget | undefined
if (route.kind === 'ssh') {
const sshProvider = requireSshRuntimeGitProvider(route)
provider = sshProvider
materialized = pushTarget
? await materializeWorktreePushTargetRemoteSsh(
sshProvider,
target.worktree.path,
pushTarget
)
: undefined
} else {
provider = requireRuntimeGitProvider(target, route)
materialized = pushTarget
? await materializeWorktreePushTargetRemote(
target.worktree.path,
pushTarget,
undefined,
target.repo?.id,
localGitOptionsForTarget(target)
)
: undefined
}
const provider = requireRuntimeGitProvider(target)
const materialized = pushTarget
? await materializeWorktreePushTargetRemote(
target.worktree.path,
pushTarget,
undefined,
target.repo?.id,
localGitOptionsForTarget(target)
)
: undefined
if (materialized?.remoteCreated) {
this.host.persistMaterializedPushTarget?.(target.worktree.id, materialized)
}
@@ -1,8 +1,8 @@
// `resolveRuntimeGitTarget` read `store.getRepo(worktree.repoId)?.connectionId` and never looked at
// `worktree.hostId`, so one arbitrarily chosen row decided the execution host for ~36 downstream
// Git dispatches. `undefined` there meant "runtime host", "unresolved" and "genuinely local" at
// once (#11163). These cases pin all four answers end to end, through the real SSH provider table.
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
// once (#11163). These cases pin all four answers end to end; an `ssh:` host is refused by name.
import { beforeEach, describe, expect, it, vi } from 'vitest'
vi.mock('electron', () => ({
BrowserWindow: { fromId: vi.fn(() => null) },
@@ -21,7 +21,7 @@ vi.mock('../git/status', async () => ({
}))
import { ExecutionHostNotDispatchableError } from '../providers/execution-host-provider-dispatch'
import { registerSshGitProvider, unregisterSshGitProvider } from '../providers/ssh-git-dispatch'
import { SshHostNotServedError } from '../providers/ssh-host-not-served-error'
import { OrcaRuntimeService } from './orca-runtime'
const REMOTE_PATH = '/srv/app-feature'
@@ -50,36 +50,25 @@ function makeRuntime(repos: readonly Record<string, unknown>[], hostId?: string)
return runtime
}
function stubProvider() {
return { getStatus: vi.fn().mockResolvedValue({ entries: [] }) }
async function expectRefusedFor(promise: Promise<unknown>, connectionId: string): Promise<void> {
const error = await promise.then(
() => null,
(caught: unknown) => caught
)
expect(error).toBeInstanceOf(SshHostNotServedError)
expect(error).toMatchObject({ connectionId })
expect(mocks.getStatus).not.toHaveBeenCalled()
}
describe('runtime Git target execution host', () => {
const registered: string[] = []
function register(connectionId: string) {
const provider = stubProvider()
registerSshGitProvider(connectionId, provider as never)
registered.push(connectionId)
return provider
}
beforeEach(() => {
vi.restoreAllMocks()
mocks.getStatus.mockReset().mockResolvedValue({ entries: [] })
})
afterEach(() => {
for (const connectionId of registered.splice(0)) {
unregisterSshGitProvider(connectionId)
}
})
// The case whose absence let the original cross-host leak through review: two SSH hosts, and the
// rival row is the one `getRepo` returns first.
it('serves an ssh worktree from the host it names, not from a rival row on another ssh host', async () => {
const openclaw = register('openclaw')
const m4air = register('m4air')
it('routes an ssh worktree to the host it names, not to a rival row on another ssh host', async () => {
const runtime = makeRuntime(
[
{ id: 'repo-shared', path: '/home/me/app', connectionId: 'openclaw' },
@@ -88,31 +77,21 @@ describe('runtime Git target execution host', () => {
'ssh:m4air'
)
await runtime.getRuntimeGitStatus(`id:${WORKTREE_ID}`)
expect(m4air.getStatus).toHaveBeenCalledWith(REMOTE_PATH)
expect(openclaw.getStatus).not.toHaveBeenCalled()
expect(mocks.getStatus).not.toHaveBeenCalled()
await expectRefusedFor(runtime.getRuntimeGitStatus(`id:${WORKTREE_ID}`), 'm4air')
})
it("routes to the worktree's host even when the only repo row names a different ssh host", async () => {
const openclaw = register('openclaw')
const m4air = register('m4air')
const runtime = makeRuntime(
[{ id: 'repo-shared', path: '/home/me/app', connectionId: 'openclaw' }],
'ssh:m4air'
)
await runtime.getRuntimeGitStatus(`id:${WORKTREE_ID}`)
expect(m4air.getStatus).toHaveBeenCalledWith(REMOTE_PATH)
expect(openclaw.getStatus).not.toHaveBeenCalled()
await expectRefusedFor(runtime.getRuntimeGitStatus(`id:${WORKTREE_ID}`), 'm4air')
})
// `local` has no SSH namespace to nest in, so a surviving `connectionId` is a row contradicting
// itself. The old shape handed it out and dialled a remote host for a local workspace.
it('ignores a stale connection on a row that declares itself local', async () => {
const m4air = register('m4air')
const runtime = makeRuntime(
[
{
@@ -127,15 +106,13 @@ describe('runtime Git target execution host', () => {
await runtime.getRuntimeGitStatus(`id:${WORKTREE_ID}`)
expect(m4air.getStatus).not.toHaveBeenCalled()
expect(mocks.getStatus).toHaveBeenCalled()
})
// A `runtime:` row's `connectionId` names a target in the *server's* namespace. Dialling it here
// reaches a same-named target on this client — a silent-wrong-host answer, worse than the
// silent-local one it replaced.
it('refuses a runtime host whose nested ssh target is also registered on this client', async () => {
const impostor = register('nested-1')
it('refuses a runtime host whose row carries a nested ssh target', async () => {
const runtime = makeRuntime(
[
{
@@ -151,7 +128,6 @@ describe('runtime Git target execution host', () => {
await expect(runtime.getRuntimeGitStatus(`id:${WORKTREE_ID}`)).rejects.toThrow(
ExecutionHostNotDispatchableError
)
expect(impostor.getStatus).not.toHaveBeenCalled()
expect(mocks.getStatus).not.toHaveBeenCalled()
})
@@ -168,7 +144,6 @@ describe('runtime Git target execution host', () => {
})
it('refuses rather than guessing when rival rows disagree and the worktree names no host', async () => {
register('m4air')
const runtime = makeRuntime([
{ id: 'repo-shared', path: '/srv/app', connectionId: 'm4air' },
{ id: 'repo-shared', path: '/home/me/app' }
@@ -180,13 +155,10 @@ describe('runtime Git target execution host', () => {
expect(mocks.getStatus).not.toHaveBeenCalled()
})
it('still answers from the single row when the worktree names no host', async () => {
const m4air = register('m4air')
it('still routes by the single row when the worktree names no host', async () => {
const runtime = makeRuntime([{ id: 'repo-shared', path: '/srv/app', connectionId: 'm4air' }])
await runtime.getRuntimeGitStatus(`id:${WORKTREE_ID}`)
expect(m4air.getStatus).toHaveBeenCalledWith(REMOTE_PATH)
await expectRefusedFor(runtime.getRuntimeGitStatus(`id:${WORKTREE_ID}`), 'm4air')
})
// Losing contact with a remote host is never evidence that its files are here
@@ -11,7 +11,7 @@ import {
getLocalProjectGitExecOptions,
getLocalProjectGhExecOptions
} from '../project-runtime-git-options'
import { requireSshGitProvider } from '../providers/ssh-git-dispatch'
import { SshHostNotServedError } from '../providers/ssh-host-not-served-error'
import { isFolderRepo } from '../../shared/repo-kind'
import type { Store } from '../persistence'
@@ -39,42 +39,24 @@ export async function resolveRuntimeGitHubWorktreeBase(
if (isFolderRepo(repo)) {
return { error: 'Folder mode does not support creating worktrees.' }
}
const sshGitProvider = repo.connectionId ? requireSshGitProvider(repo.connectionId) : null
const localGitExecOptions = sshGitProvider
? undefined
: getLocalProjectGitExecOptions(deps.store, repo)
const localGhOptions = sshGitProvider
? repo.ghAccount
? { ghAccount: repo.ghAccount }
: undefined
: getLocalProjectGhExecOptions(deps.store, repo)
const gitExec = sshGitProvider
? (gitArgs: string[]) => sshGitProvider.exec(gitArgs, repo.path)
: (gitArgs: string[]) => gitExecFileAsync(gitArgs, localGitExecOptions ?? { cwd: repo.path })
if (repo.connectionId) {
throw new SshHostNotServedError(repo.connectionId)
}
const localGitExecOptions = getLocalProjectGitExecOptions(deps.store, repo)
const localGhOptions = getLocalProjectGhExecOptions(deps.store, repo)
const gitExec = (gitArgs: string[]) => gitExecFileAsync(gitArgs, localGitExecOptions)
const resolveRemote = (): Promise<string> =>
resolveGitHubReviewHeadRemote({
repoPath: repo.path,
issueSourcePreference: repo.issueSourcePreference,
connectionId: repo.connectionId ?? null,
connectionId: null,
localGitOptions: localGhOptions,
gitExec
})
const fetchRemoteTrackingRef = (remote: string, branch: string): Promise<void> =>
fetchPrHeadTrackingRef(
repo,
sshGitProvider,
remote,
branch,
localGitExecOptions ? { localGitExecOptions } : {}
)
fetchPrHeadTrackingRef(repo, null, remote, branch, { localGitExecOptions })
const fetchPullRequestHeadRef = (remote: string, prNumber: number): Promise<string> =>
fetchGitHubPullRequestHeadRef(
repo,
sshGitProvider,
remote,
prNumber,
localGitExecOptions ? { localGitExecOptions } : {}
)
fetchGitHubPullRequestHeadRef(repo, null, remote, prNumber, { localGitExecOptions })
return resolveGitHubPrStartPoint({
repoPath: repo.path,
prNumber: args.prNumber,
@@ -82,7 +64,7 @@ export async function resolveRuntimeGitHubWorktreeBase(
baseRefName: args.baseRefName,
isCrossRepository: args.isCrossRepository,
issueSourcePreference: repo.issueSourcePreference,
connectionId: repo.connectionId ?? null,
connectionId: null,
localGitOptions: localGhOptions,
gitExec,
fetchRemoteTrackingRef,
@@ -5,14 +5,10 @@ import {
} from '../../shared/review-head-tracking-ref'
import { isTransientReviewHeadFetchError } from '../git/fetch-error-classification'
import { fetchGitLabMergeRequestHeadRef } from '../gitlab/mr-head-tracking-ref'
import type { requireSshGitProvider } from '../providers/ssh-git-dispatch'
type SshGitProvider = ReturnType<typeof requireSshGitProvider>
type GitExec = (args: string[]) => Promise<{ stdout: string }>
export async function resolveRuntimeGitLabForkMrBase(args: {
repo: Repo
sshGitProvider: SshGitProvider | null
remote: string
mrIid: number
localGitExecOptions?: { cwd: string; wslDistro?: string }
@@ -53,7 +49,7 @@ export async function resolveRuntimeGitLabForkMrBase(args: {
try {
const localRef = await fetchGitLabMergeRequestHeadRef(
args.repo,
args.sshGitProvider,
null,
args.remote,
args.mrIid,
args.localGitExecOptions ? { localGitExecOptions: args.localGitExecOptions } : {}
@@ -2,20 +2,17 @@ import type { Repo } from '../../shared/repo-types'
import { getDefaultRemote } from '../git/repo'
import { getProjectRefForRemote } from '../gitlab/client'
import { getGlabKnownHosts } from '../gitlab/gl-utils'
import { requireSshGitProvider } from '../providers/ssh-git-dispatch'
import { pickPreferredGitRemote } from '../../shared/preferred-git-remote'
export async function resolveRuntimeGitLabIssueSourceRemote(
repoPath: string,
preference?: Repo['issueSourcePreference'],
connectionId?: string | null,
localGitOptions: { wslDistro?: string } = {}
): Promise<string> {
const knownHosts = await getGlabKnownHosts(connectionId, localGitOptions)
const knownHosts = await getGlabKnownHosts(null, localGitOptions)
const localGitOptionArgs =
Object.keys(localGitOptions).length > 0 ? ([localGitOptions] as const) : []
const resolveProject = (remote: string) =>
getProjectRefForRemote(repoPath, remote, knownHosts, connectionId, ...localGitOptionArgs)
getProjectRefForRemote(repoPath, remote, knownHosts, null, ...localGitOptionArgs)
if (preference === 'origin') {
if (await resolveProject('origin')) {
return 'origin'
@@ -37,10 +34,5 @@ export async function resolveRuntimeGitLabIssueSourceRemote(
if (await resolveProject('origin')) {
return 'origin'
}
if (connectionId) {
const provider = requireSshGitProvider(connectionId)
const { stdout } = await provider.exec(['remote'], repoPath)
return pickPreferredGitRemote(stdout.split('\n'))
}
return getDefaultRemote(repoPath, localGitOptions)
}
@@ -9,7 +9,7 @@ import {
getLocalProjectGitExecOptions,
getLocalProjectWorktreeGitOptions
} from '../project-runtime-git-options'
import { requireSshGitProvider } from '../providers/ssh-git-dispatch'
import { SshHostNotServedError } from '../providers/ssh-host-not-served-error'
import type { Store } from '../persistence'
import { resolveRuntimeGitLabForkMrBase } from './runtime-gitlab-fork-mr-base'
import { resolveRuntimeGitLabIssueSourceRemote } from './runtime-gitlab-issue-source-remote'
@@ -42,16 +42,12 @@ export async function resolveRuntimeGitLabWorktreeBase(
if (isFolderRepo(repo)) {
return { error: 'Folder mode does not support creating worktrees.' }
}
const sshGitProvider = repo.connectionId ? requireSshGitProvider(repo.connectionId) : null
const localGitExecOptions = sshGitProvider
? undefined
: getLocalProjectGitExecOptions(deps.store, repo)
const localWorktreeGitOptions = sshGitProvider
? {}
: getLocalProjectWorktreeGitOptions(deps.store, repo)
const gitExec = sshGitProvider
? (gitArgs: string[]) => sshGitProvider.exec(gitArgs, repo.path)
: (gitArgs: string[]) => gitExecFileAsync(gitArgs, localGitExecOptions ?? { cwd: repo.path })
if (repo.connectionId) {
throw new SshHostNotServedError(repo.connectionId)
}
const localGitExecOptions = getLocalProjectGitExecOptions(deps.store, repo)
const localWorktreeGitOptions = getLocalProjectWorktreeGitOptions(deps.store, repo)
const gitExec = (gitArgs: string[]) => gitExecFileAsync(gitArgs, localGitExecOptions)
let sourceBranch = args.sourceBranch?.trim() ?? ''
let targetBranch = args.targetBranch?.trim() ?? ''
let isCrossRepository = args.isCrossRepository === true
@@ -61,18 +57,17 @@ export async function resolveRuntimeGitLabWorktreeBase(
discoveryRemote = await resolveRuntimeGitLabIssueSourceRemote(
repo.path,
repo.issueSourcePreference,
repo.connectionId ?? null,
localWorktreeGitOptions
)
} catch (error) {
return { error: error instanceof Error ? error.message : 'Could not resolve git remote.' }
}
const knownHosts = await getGlabKnownHosts(repo.connectionId ?? null, localWorktreeGitOptions)
const knownHosts = await getGlabKnownHosts(null, localWorktreeGitOptions)
const projectRef = await getProjectRefForRemote(
repo.path,
discoveryRemote,
knownHosts,
repo.connectionId ?? null,
null,
localWorktreeGitOptions
)
if (!projectRef) {
@@ -83,7 +78,7 @@ export async function resolveRuntimeGitLabWorktreeBase(
projectRef,
args.mrIid,
'mr',
repo.connectionId ?? null,
null,
localWorktreeGitOptions
)
if (!item || item.type !== 'mr') {
@@ -103,7 +98,6 @@ export async function resolveRuntimeGitLabWorktreeBase(
remote = await resolveRuntimeGitLabIssueSourceRemote(
repo.path,
repo.issueSourcePreference,
repo.connectionId ?? null,
localWorktreeGitOptions
)
} catch (error) {
@@ -111,9 +105,7 @@ export async function resolveRuntimeGitLabWorktreeBase(
}
const compareBaseRef = targetBranch ? `refs/remotes/${remote}/${targetBranch}` : undefined
const fetchRemoteTrackingRef = async (branch: string, ref: string): Promise<void> => {
await (sshGitProvider
? sshGitProvider.fetchRemoteTrackingRef(repo.path, remote, branch, ref)
: gitExec(['fetch', remote, `+refs/heads/${branch}:${ref}`]))
await gitExec(['fetch', remote, `+refs/heads/${branch}:${ref}`])
}
const fetchCompareBaseRef = (): Promise<boolean> =>
fetchCompareBaseRefWithLocalFallback({
@@ -126,7 +118,6 @@ export async function resolveRuntimeGitLabWorktreeBase(
if (isCrossRepository) {
return resolveRuntimeGitLabForkMrBase({
repo,
sshGitProvider,
remote,
mrIid: args.mrIid,
localGitExecOptions,
@@ -8,12 +8,9 @@ import { parseExecutionHostId, type ExecutionHostId } from '../../shared/executi
import { isFolderRepo } from '../../shared/repo-kind'
import { forceDeleteLocalBranch } from '../git/worktree'
import { gitExecFileAsync } from '../git/runner'
import {
cleanupUnusedWorktreePushTargetRemote,
cleanupUnusedWorktreePushTargetRemoteSsh
} from '../ipc/worktree-remote'
import { cleanupUnusedWorktreePushTargetRemote } from '../ipc/worktree-remote'
import { getLocalProjectWorktreeGitOptions } from '../project-runtime-git-options'
import { requireSshGitProvider } from '../providers/ssh-git-dispatch'
import { SshHostNotServedError } from '../providers/ssh-host-not-served-error'
import type { Store } from '../persistence'
import { resolveWorktreeRemovalRepoOwner } from '../worktree-removal-repo-owner'
import { parseExactWorktreeIdSelector } from './runtime-worktree-selection'
@@ -124,34 +121,25 @@ export class RuntimePreservedBranchCleanup {
}
if (repo.connectionId) {
const provider = requireSshGitProvider(repo.connectionId)
await provider.forceDeletePreservedBranch(repo.path, target.branchName, target.head)
await cleanupUnusedWorktreePushTargetRemoteSsh(
provider,
repo.path,
removalTarget.id,
target.pushTarget,
store
)
} else {
const options = getLocalProjectWorktreeGitOptions(store, repo)
await (Object.keys(options).length > 0
? forceDeleteLocalBranch(
repo.path,
target.branchName,
target.head,
(argv, cwd) => gitExecFileAsync(argv, { cwd, ...options }),
options
)
: forceDeleteLocalBranch(repo.path, target.branchName, target.head))
await cleanupUnusedWorktreePushTargetRemote(
repo.path,
removalTarget.id,
target.pushTarget,
store,
options
)
throw new SshHostNotServedError(repo.connectionId)
}
const options = getLocalProjectWorktreeGitOptions(store, repo)
await (Object.keys(options).length > 0
? forceDeleteLocalBranch(
repo.path,
target.branchName,
target.head,
(argv, cwd) => gitExecFileAsync(argv, { cwd, ...options }),
options
)
: forceDeleteLocalBranch(repo.path, target.branchName, target.head))
await cleanupUnusedWorktreePushTargetRemote(
repo.path,
removalTarget.id,
target.pushTarget,
store,
options
)
this.delete(removalTarget.id, target.hostId)
return { deleted: true }
}
@@ -1,56 +0,0 @@
import { resolveSshQuickOpenDiscoveryOptions } from '../providers/ssh-quick-open-discovery-options'
import type { IFilesystemProvider } from '../providers/types'
import { QUICK_OPEN_LEGACY_REMOTE_RESULT_LIMIT } from './runtime-file-commands-mobile-file-list-limit'
import { QuickOpenPathRanker } from '../../shared/quick-open-path-search'
export async function searchRemoteQuickOpenFilePaths(
rootPath: string,
// `null` is "remote and currently unreachable": quick open reports no matches rather than
// failing the keystroke, but it never falls back to searching this machine.
provider: IFilesystemProvider | null,
query: string,
limit: number,
excludePaths?: string[],
signal?: AbortSignal,
options: {
includeIgnored?: boolean
followSymlinks?: boolean
allowLegacyIncludeIgnored?: boolean
} = {}
): Promise<{ paths: string[]; totalCount: number; truncated: boolean }> {
if (!provider) {
return { paths: [], totalCount: 0, truncated: false }
}
const discovery = await resolveSshQuickOpenDiscoveryOptions(provider, options, signal)
if (!(await provider.supportsQuickOpenSearch?.({ signal, minimumVersion: 1 }))) {
// Old relays ignore searchQuery. Keep the compatibility request below the
// 4 MiB frame ceiling even when legacy paths are near the 64 KiB path cap.
const legacyFiles = await provider.listFiles(rootPath, {
excludePaths,
maxResults: QUICK_OPEN_LEGACY_REMOTE_RESULT_LIMIT,
signal
})
const ranker = new QuickOpenPathRanker(query, limit)
for (const file of legacyFiles) {
ranker.consider(file)
}
const result = ranker.result()
return {
...result,
truncated:
legacyFiles.length >= QUICK_OPEN_LEGACY_REMOTE_RESULT_LIMIT || result.totalCount > limit
}
}
const files = await provider.listFiles(rootPath, {
excludePaths,
maxResults: limit + 1,
...discovery,
searchQuery: query,
signal
})
return {
paths: files.slice(0, limit),
totalCount: files.length,
truncated: files.length > limit
}
}
@@ -2,19 +2,12 @@ import { getStoredRepoSshConnectionId } from '../repo-execution-host'
import { createHash } from 'node:crypto'
import { readFile } from 'node:fs/promises'
import type { Repo } from '../../shared/repo-types'
import {
getEffectiveHooks,
hasHooksFile,
hasUnrecognizedOrcaYamlKeys,
loadHooks,
parseOrcaYaml
} from '../hooks'
import { getEffectiveHooks, hasHooksFile, hasUnrecognizedOrcaYamlKeys, loadHooks } from '../hooks'
import {
getDefaultTabCommandTrustContent,
getEffectiveSetupRunPolicy
} from '../effective-hook-config'
import { isENOENT } from '../ipc/filesystem-auth'
import { getSshFilesystemProvider } from '../providers/ssh-filesystem-dispatch'
import { isFolderRepo } from '../../shared/repo-kind'
import { inspectSetupScriptImportCandidates } from '../../shared/setup-script-imports'
import { joinWorktreeRelativePath } from './runtime-relative-paths'
@@ -29,33 +22,13 @@ export class RuntimeRepositoryHooksCommands {
async getRepoHooks(repoSelector: string) {
const repo = await this.deps.resolveRepo(repoSelector)
const connectionId = getStoredRepoSshConnectionId(repo)
// Why: an `ssh:` repo is not served here; it reports no hooks file.
if (connectionId) {
const fsProvider = getSshFilesystemProvider(connectionId)
if (!fsProvider) {
return {
hasHooksFile: false,
hooks: null,
setupRunPolicy: getEffectiveSetupRunPolicy(repo),
source: null
}
}
try {
const result = await fsProvider.readFile(joinWorktreeRelativePath(repo.path, 'orca.yaml'))
const hooks = result.isBinary ? null : parseOrcaYaml(result.content)
return {
hasHooksFile: Boolean(hooks),
hooks,
setupRunPolicy: getEffectiveSetupRunPolicy(repo),
source: hooks ? ('orca.yaml' as const) : null,
setupTrust: setupTrust(repo, getDefaultTabCommandTrustContent(hooks))
}
} catch {
return {
hasHooksFile: false,
hooks: null,
setupRunPolicy: getEffectiveSetupRunPolicy(repo),
source: null
}
return {
hasHooksFile: false,
hooks: null,
setupRunPolicy: getEffectiveSetupRunPolicy(repo),
source: null
}
}
const hasFile = hasHooksFile(repo.path)
@@ -77,29 +50,7 @@ export class RuntimeRepositoryHooksCommands {
}
const connectionId = getStoredRepoSshConnectionId(repo)
if (connectionId) {
const fsProvider = getSshFilesystemProvider(connectionId)
if (!fsProvider) {
return { status: 'error' as const, hasHooks: false, hooks: null, mayNeedUpdate: false }
}
try {
const result = await fsProvider.readFile(joinWorktreeRelativePath(repo.path, 'orca.yaml'))
if (result.isBinary) {
return { status: 'ok' as const, hasHooks: false, hooks: null, mayNeedUpdate: false }
}
return {
status: 'ok' as const,
hasHooks: true,
hooks: parseOrcaYaml(result.content),
mayNeedUpdate: false
}
} catch (error) {
return {
status: isENOENT(error) ? ('ok' as const) : ('error' as const),
hasHooks: false,
hooks: null,
mayNeedUpdate: false
}
}
return { status: 'error' as const, hasHooks: false, hooks: null, mayNeedUpdate: false }
}
const has = hasHooksFile(repo.path)
const hooks = has ? loadHooks(repo.path) : null
@@ -118,18 +69,8 @@ export class RuntimeRepositoryHooksCommands {
}
return inspectSetupScriptImportCandidates(async (relativePath) => {
const filePath = joinWorktreeRelativePath(repo.path, relativePath)
const connectionId = getStoredRepoSshConnectionId(repo)
if (connectionId) {
const fsProvider = getSshFilesystemProvider(connectionId)
if (!fsProvider) {
return null
}
try {
const result = await fsProvider.readFile(filePath)
return result.isBinary ? null : result.content
} catch {
return null
}
if (getStoredRepoSshConnectionId(repo)) {
return null
}
try {
return await readFile(filePath, 'utf-8')
@@ -5,28 +5,14 @@ import * as issueCommandFile from '../issue-command-file'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { RuntimeRepositoryIssueCommand } from './runtime-repository-issue-command'
const mocks = vi.hoisted(() => ({
localCheck: vi.fn(),
remoteCheck: vi.fn(),
requireGit: vi.fn(),
fs: {
createDir: vi.fn(),
readFile: vi.fn(),
writeFile: vi.fn(),
deletePath: vi.fn()
}
}))
const mocks = vi.hoisted(() => ({ localCheck: vi.fn() }))
vi.mock('../git/check-ignored-paths', () => ({ checkIgnoredPaths: mocks.localCheck }))
vi.mock('../providers/ssh-git-dispatch', () => ({ requireSshGitProvider: mocks.requireGit }))
vi.mock('../providers/ssh-filesystem-dispatch', () => ({
getSshFilesystemProvider: () => mocks.fs
}))
describe('remote issue command ignore rules', () => {
describe('SSH issue command', () => {
const repo = {
id: 'repo-ssh',
path: '/remote/repo with spaces',
path: '/remote/repo',
displayName: 'remote',
badgeColor: '#000',
addedAt: 0,
@@ -39,66 +25,27 @@ describe('remote issue command ignore rules', () => {
beforeEach(() => {
vi.resetAllMocks()
mocks.requireGit.mockReturnValue({ checkIgnoredPaths: mocks.remoteCheck })
mocks.remoteCheck.mockResolvedValue([])
mocks.fs.createDir.mockResolvedValue(undefined)
mocks.fs.writeFile.mockResolvedValue(undefined)
mocks.fs.deletePath.mockResolvedValue(undefined)
mocks.fs.readFile.mockResolvedValue({ content: 'node_modules/\n', isBinary: false })
})
it('uses the remote ignore rules and leaves .gitignore untouched', async () => {
mocks.remoteCheck.mockResolvedValue(['.orca/issue-command'])
it('reads as no command and never reads this machine', async () => {
const read = vi.spyOn(issueCommandFile, 'readIssueCommand')
await expect(commands.read(repo.id)).resolves.toEqual({
localContent: null,
sharedContent: null,
effectiveContent: null,
localFilePath: '/remote/repo/.orca/issue-command',
source: 'none'
})
expect(read).not.toHaveBeenCalled()
read.mockRestore()
})
await commands.write(repo.id, 'local command')
expect(mocks.requireGit).toHaveBeenCalledWith('conn-1')
expect(mocks.remoteCheck).toHaveBeenCalledWith(repo.path, ['.orca/issue-command'])
it('drops a write instead of writing this machine', async () => {
const write = vi.spyOn(issueCommandFile, 'writeIssueCommand')
await expect(commands.write(repo.id, 'command')).resolves.toEqual({ ok: true })
expect(write).not.toHaveBeenCalled()
expect(mocks.localCheck).not.toHaveBeenCalled()
expect(mocks.fs.readFile).not.toHaveBeenCalled()
expect(mocks.fs.writeFile).toHaveBeenCalledExactlyOnceWith(
`${repo.path}/.orca/issue-command`,
'local command\n'
)
})
it('adds the rule if the remote host does not ignore .orca', async () => {
await commands.write(repo.id, 'local command')
expect(mocks.fs.writeFile).toHaveBeenCalledWith(
`${repo.path}/.gitignore`,
'node_modules/\n.orca\n'
)
expect(mocks.localCheck).not.toHaveBeenCalled()
})
it.each(['unavailable', 'failed'])(
'keeps the remote fallback when Git is %s',
async (failure) => {
if (failure === 'unavailable') {
mocks.requireGit.mockImplementation(() => {
throw new Error('remote Git unavailable')
})
} else {
mocks.remoteCheck.mockRejectedValue(new Error('remote Git failed'))
}
await expect(commands.write(repo.id, 'local command')).resolves.toEqual({ ok: true })
expect(mocks.localCheck).not.toHaveBeenCalled()
expect(mocks.fs.writeFile).toHaveBeenCalledWith(
`${repo.path}/.gitignore`,
'node_modules/\n.orca\n'
)
}
)
it('does not inspect ignore rules when clearing an override', async () => {
await commands.write(repo.id, ' ')
expect(mocks.requireGit).not.toHaveBeenCalled()
expect(mocks.fs.writeFile).not.toHaveBeenCalled()
expect(mocks.fs.deletePath).toHaveBeenCalledWith(`${repo.path}/.orca/issue-command`, false)
write.mockRestore()
})
})
@@ -1,15 +1,7 @@
import { getStoredRepoSshConnectionId } from '../repo-execution-host'
import type { GitRuntimeOptions } from '../git/git-runtime-options'
import type { Repo } from '../../shared/repo-types'
import { parseOrcaYaml } from '../hooks'
import {
isIssueCommandIgnoredByGit,
readIssueCommand,
writeIssueCommand
} from '../issue-command-file'
import { isENOENT } from '../ipc/filesystem-auth'
import { getSshFilesystemProvider } from '../providers/ssh-filesystem-dispatch'
import type { IFilesystemProvider } from '../providers/types'
import { readIssueCommand, writeIssueCommand } from '../issue-command-file'
import { isFolderRepo } from '../../shared/repo-kind'
import { joinWorktreeRelativePath } from './runtime-relative-paths'
@@ -36,29 +28,13 @@ export class RuntimeRepositoryIssueCommand {
if (!connectionId) {
return readIssueCommand(repo.path)
}
const issueCommandPath = joinWorktreeRelativePath(repo.path, '.orca/issue-command')
const fsProvider = getSshFilesystemProvider(connectionId)
if (!fsProvider) {
return {
localContent: null,
sharedContent: null,
effectiveContent: null,
localFilePath: issueCommandPath,
source: 'none' as const
}
}
const localContent = await readRemoteOverride(fsProvider, issueCommandPath)
const sharedContent = await readRemoteShared(fsProvider, repo.path)
// Why: an `ssh:` repo is not served here; it reads as having no command.
return {
localContent,
sharedContent,
effectiveContent: localContent ?? sharedContent,
localFilePath: issueCommandPath,
source: localContent
? ('local' as const)
: sharedContent
? ('shared' as const)
: ('none' as const)
localContent: null,
sharedContent: null,
effectiveContent: null,
localFilePath: joinWorktreeRelativePath(repo.path, '.orca/issue-command'),
source: 'none' as const
}
}
@@ -68,85 +44,10 @@ export class RuntimeRepositoryIssueCommand {
if (isFolderRepo(repo)) {
return { ok: true }
}
const connectionId = getStoredRepoSshConnectionId(repo)
if (!connectionId) {
// Why: an `ssh:` repo is not served here; the write is dropped, as when it was unreachable.
if (!getStoredRepoSshConnectionId(repo)) {
await writeIssueCommand(repo.path, content, () => this.deps.getLocalGitArgs(repo)[0] ?? {})
return { ok: true }
}
const issueCommandPath = joinWorktreeRelativePath(repo.path, '.orca/issue-command')
const fsProvider = getSshFilesystemProvider(connectionId)
if (!fsProvider) {
return { ok: true }
}
const trimmed = content.trim()
if (!trimmed) {
await fsProvider.deletePath(issueCommandPath, false).catch((error: unknown) => {
if (!isENOENT(error)) {
throw error
}
})
return { ok: true }
}
await fsProvider.createDir(joinWorktreeRelativePath(repo.path, '.orca'))
if (!(await isIssueCommandIgnoredByGit(repo.path, connectionId))) {
await ensureRemoteOrcaDirIgnored(fsProvider, repo.path)
}
await fsProvider.writeFile(issueCommandPath, `${trimmed}\n`)
return { ok: true }
}
}
async function readRemoteOverride(
fsProvider: IFilesystemProvider,
issueCommandPath: string
): Promise<string | null> {
try {
const result = await fsProvider.readFile(issueCommandPath)
return result.isBinary ? null : result.content.trim() || null
} catch {
return null
}
}
async function readRemoteShared(
fsProvider: IFilesystemProvider,
repoPath: string
): Promise<string | null> {
try {
const result = await fsProvider.readFile(joinWorktreeRelativePath(repoPath, 'orca.yaml'))
return result.isBinary ? null : parseOrcaYaml(result.content)?.issueCommand?.trim() || null
} catch {
return null
}
}
async function ensureRemoteOrcaDirIgnored(
fsProvider: IFilesystemProvider,
repoPath: string
): Promise<void> {
const gitignorePath = joinWorktreeRelativePath(repoPath, '.gitignore')
let result: Awaited<ReturnType<IFilesystemProvider['readFile']>>
try {
result = await fsProvider.readFile(gitignorePath)
} catch (error) {
if (!isENOENT(error)) {
console.warn('[runtime] Could not inspect remote .gitignore for .orca', error)
return
}
try {
await fsProvider.writeFile(gitignorePath, '.orca\n')
} catch (writeError) {
console.warn('[runtime] Could not update remote .gitignore to exclude .orca', writeError)
}
return
}
if (result.isBinary || /^\.orca\/?$/m.test(result.content)) {
return
}
const separator = result.content.endsWith('\n') ? '' : '\n'
try {
await fsProvider.writeFile(gitignorePath, `${result.content}${separator}.orca\n`)
} catch (writeError) {
console.warn('[runtime] Could not update remote .gitignore to exclude .orca', writeError)
}
}
@@ -1,4 +1,4 @@
import type { BaseRefSearchResult, Repo } from '../../shared/repo-types'
import type { Repo } from '../../shared/repo-types'
import type { RuntimeRepoSearchRefs } from '../../shared/runtime-types'
import { isFolderRepo } from '../../shared/repo-kind'
import {
@@ -6,20 +6,7 @@ import {
getRepoSearchRefsProbeLimit,
isRepoSearchRefsRequestLimit
} from '../../shared/repo-search-limits'
import {
buildSearchBaseRefsArgv,
getBaseRefDefault,
getRemoteCount,
isForEachRefExcludeUnsupportedError,
mergeBaseRefSearchResultGroups,
normalizeRefSearchQuery,
parseAndFilterSearchRefDetails,
parseRemoteCount,
resolveDefaultBaseRefViaExec,
searchBaseRefDetails
} from '../git/repo'
import { getSshGitCapabilityCache } from '../git/git-capability-state'
import { getSshGitProvider } from '../providers/ssh-git-dispatch'
import { getBaseRefDefault, getRemoteCount, searchBaseRefDetails } from '../git/repo'
type RuntimeRepositoryRefQueryDependencies = {
resolveRepo: (selector: string) => Promise<Repo>
@@ -43,8 +30,9 @@ export class RuntimeRepositoryRefQueries {
if (isFolderRepo(repo)) {
return { refs: [], truncated: false }
}
// Why: an `ssh:` repo is not served here; it reports no refs and never searches this machine.
const refDetails = repo.connectionId
? await this.searchRemote(repo, query, probeLimit, includeQualifiedRefs)
? []
: await searchBaseRefDetails(repo.path, query, probeLimit, includeQualifiedRefs)
return {
refs: refDetails.slice(0, effectiveLimit).map((entry) => entry.refName),
@@ -64,7 +52,7 @@ export class RuntimeRepositoryRefQueries {
return { defaultBaseRef: null, remoteCount: 0 }
}
if (repo.connectionId) {
return this.getRemoteDefault(repo)
return { defaultBaseRef: null, remoteCount: 0 }
}
const [defaultBaseRef, remoteCount] = await Promise.all([
getBaseRefDefault(repo.path),
@@ -72,103 +60,4 @@ export class RuntimeRepositoryRefQueries {
])
return { defaultBaseRef, remoteCount }
}
private async getRemoteDefault(
repo: Repo
): Promise<{ defaultBaseRef: string | null; remoteCount: number }> {
const provider = repo.connectionId ? getSshGitProvider(repo.connectionId) : null
if (!provider) {
return { defaultBaseRef: null, remoteCount: 0 }
}
const [defaultBaseRef, remoteCount] = await Promise.all([
resolveDefaultBaseRefViaExec(async (argv) => {
try {
return await provider.exec(argv, repo.path)
} catch (error) {
if (argv[0] === 'symbolic-ref') {
console.warn('[runtime:repo.baseRefDefault] SSH symbolic-ref failed', {
path: repo.path,
err: error
})
}
throw error
}
}),
provider
.exec(['remote'], repo.path)
.then((result) => parseRemoteCount(result.stdout))
.catch((error) => {
console.warn('[runtime:repo.baseRefDefault] SSH git remote count failed', {
path: repo.path,
err: error
})
return 0
})
])
return { defaultBaseRef, remoteCount }
}
private async searchRemote(
repo: Repo,
query: string,
limit: number,
includeQualifiedRefs: boolean
): Promise<BaseRefSearchResult[]> {
const provider = repo.connectionId ? getSshGitProvider(repo.connectionId) : null
if (!provider) {
return []
}
const normalizedQuery = normalizeRefSearchQuery(query)
try {
const remotesResult = await provider.exec(['remote'], repo.path).catch(() => ({ stdout: '' }))
const remotes = remotesResult.stdout
.split('\n')
.map((line) => line.trim())
.filter(Boolean)
const capabilities = getSshGitCapabilityCache(provider)
const runSearch = async (patternGroup?: 'segmented' | 'branchRoot'): Promise<string> => {
return capabilities.runWithFallback(
'for-each-ref-exclude',
async () =>
(
await provider.exec(
buildSearchBaseRefsArgv(normalizedQuery, limit, {
remoteNames: remotes,
patternGroup
}),
repo.path
)
).stdout,
async () =>
(
await provider.exec(
buildSearchBaseRefsArgv(normalizedQuery, limit, {
excludeRemoteHead: false,
remoteNames: remotes,
patternGroup
}),
repo.path
)
).stdout,
isForEachRefExcludeUnsupportedError
)
}
if (normalizedQuery.split('/').filter((token) => token.length > 0).length > 1) {
const results = await Promise.all([runSearch('segmented'), runSearch('branchRoot')])
return mergeBaseRefSearchResultGroups(
results.map((stdout) =>
parseAndFilterSearchRefDetails(stdout, limit, remotes, includeQualifiedRefs)
),
limit
)
}
return parseAndFilterSearchRefDetails(await runSearch(), limit, remotes, includeQualifiedRefs)
} catch (error) {
console.warn('[runtime:repo.searchRefs] SSH for-each-ref failed', {
path: repo.path,
err: error
})
return []
}
}
}
@@ -3,23 +3,12 @@ import type { GitPushTarget } from '../../shared/worktree/types'
import type { Repo } from '../../shared/repo-types'
import type { Store } from '../persistence'
const {
materializeLocalMock,
materializeSshMock,
getSshGitProviderMock,
getLocalProjectWorktreeGitOptionsMock
} = vi.hoisted(() => ({
const { materializeLocalMock, getLocalProjectWorktreeGitOptionsMock } = vi.hoisted(() => ({
materializeLocalMock: vi.fn(),
materializeSshMock: vi.fn(),
getSshGitProviderMock: vi.fn(),
getLocalProjectWorktreeGitOptionsMock: vi.fn()
}))
vi.mock('../ipc/worktree-remote', () => ({
materializeWorktreePushTargetRemote: materializeLocalMock,
materializeWorktreePushTargetRemoteSsh: materializeSshMock
}))
vi.mock('../providers/ssh-git-dispatch', () => ({
getSshGitProvider: getSshGitProviderMock
materializeWorktreePushTargetRemote: materializeLocalMock
}))
vi.mock('../project-runtime-git-options', () => ({
getLocalProjectWorktreeGitOptions: getLocalProjectWorktreeGitOptionsMock
@@ -51,8 +40,6 @@ describe('triggerTerminalSpawnPushTargetMaterialization', () => {
beforeEach(() => {
materializeLocalMock.mockReset().mockResolvedValue(undefined)
materializeSshMock.mockReset().mockResolvedValue(undefined)
getSshGitProviderMock.mockReset()
getLocalProjectWorktreeGitOptionsMock.mockReset().mockReturnValue({})
warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
})
@@ -64,7 +51,6 @@ describe('triggerTerminalSpawnPushTargetMaterialization', () => {
it('is a no-op when there is no push target', () => {
triggerTerminalSpawnPushTargetMaterialization(WORKTREE_PATH, undefined, LOCAL_REPO, STORE)
expect(materializeLocalMock).not.toHaveBeenCalled()
expect(materializeSshMock).not.toHaveBeenCalled()
})
it('is a no-op for a same-repo push target with no remoteUrl', () => {
@@ -108,41 +94,14 @@ describe('triggerTerminalSpawnPushTargetMaterialization', () => {
{ wslDistro: 'Ubuntu' },
'worktree-1'
)
expect(materializeSshMock).not.toHaveBeenCalled()
})
it('materializes over SSH when the repo has a connectionId and a provider is registered', () => {
const provider = { exec: vi.fn() }
getSshGitProviderMock.mockReturnValue(provider)
const target = forkTarget()
triggerTerminalSpawnPushTargetMaterialization(
WORKTREE_PATH,
target,
SSH_REPO,
STORE,
REPO_ID,
'worktree-1'
)
expect(getSshGitProviderMock).toHaveBeenCalledWith('conn-1')
expect(materializeSshMock).toHaveBeenCalledWith(
provider,
WORKTREE_PATH,
target,
STORE,
undefined,
'worktree-1'
)
it('is a no-op for an SSH repo, never materializing on this machine', () => {
triggerTerminalSpawnPushTargetMaterialization(WORKTREE_PATH, forkTarget(), SSH_REPO, STORE)
expect(materializeLocalMock).not.toHaveBeenCalled()
expect(getLocalProjectWorktreeGitOptionsMock).not.toHaveBeenCalled()
})
it('is a no-op when the SSH connection has dropped (no registered provider)', () => {
getSshGitProviderMock.mockReturnValue(undefined)
triggerTerminalSpawnPushTargetMaterialization(WORKTREE_PATH, forkTarget(), SSH_REPO, STORE)
expect(materializeSshMock).not.toHaveBeenCalled()
expect(materializeLocalMock).not.toHaveBeenCalled()
})
it('falls back to default git options when WSL project runtime resolution throws', () => {
getLocalProjectWorktreeGitOptionsMock.mockImplementation(() => {
throw new Error('repair-required')
@@ -1,8 +1,4 @@
import { getSshGitProvider } from '../providers/ssh-git-dispatch'
import {
materializeWorktreePushTargetRemote,
materializeWorktreePushTargetRemoteSsh
} from '../ipc/worktree-remote'
import { materializeWorktreePushTargetRemote } from '../ipc/worktree-remote'
import { getLocalProjectWorktreeGitOptions } from '../project-runtime-git-options'
import type { GitPushTarget } from '../../shared/worktree/types'
import type { Repo } from '../../shared/repo-types'
@@ -21,21 +17,18 @@ export function triggerTerminalSpawnPushTargetMaterialization(
repoId?: string,
worktreeId?: string
): void {
if (!pushTarget?.remoteUrl || pushTarget.remoteCreated) {
// Why: an `ssh:` repo is not served here; its next sync action reports that itself.
if (!pushTarget?.remoteUrl || pushTarget.remoteCreated || repo?.connectionId) {
return
}
const connectionId = repo?.connectionId ?? undefined
const materialized = connectionId
? materializeOverSsh(connectionId, worktreePath, pushTarget, store, worktreeId)
: materializeWorktreePushTargetRemote(
worktreePath,
pushTarget,
store,
repoId,
localGitOptionsForTerminalSpawn(store, repo),
worktreeId
)
materialized.catch((error: unknown) => {
materializeWorktreePushTargetRemote(
worktreePath,
pushTarget,
store,
repoId,
localGitOptionsForTerminalSpawn(store, repo),
worktreeId
).catch((error: unknown) => {
console.warn(
`[terminal-spawn] failed to materialize push target remote for ${worktreePath}:`,
error
@@ -43,28 +36,6 @@ export function triggerTerminalSpawnPushTargetMaterialization(
})
}
function materializeOverSsh(
connectionId: string,
worktreePath: string,
pushTarget: GitPushTarget,
store: Store | undefined,
worktreeId: string | undefined
): Promise<GitPushTarget> {
const provider = getSshGitProvider(connectionId)
if (!provider) {
// Why: connection dropped -- the next Orca-driven sync action will retry via its own dispatch.
return Promise.resolve(pushTarget)
}
return materializeWorktreePushTargetRemoteSsh(
provider,
worktreePath,
pushTarget,
store,
undefined,
worktreeId
)
}
function localGitOptionsForTerminalSpawn(
store: Store | undefined,
repo: Repo | null | undefined