Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions src/main/runtime/orca-runtime-files.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,7 @@ describe('RuntimeFileCommands', () => {
it('opens text files through the renderer host (inheriting active runtime env)', async () => {
const openFile = vi.fn()
const { commands } = createRuntimeFileCommands({ openFile })
statMock.mockResolvedValue({ isDirectory: () => false })

const result = await commands.openMobileFile('id:wt-1', 'docs/readme.md')

Expand All @@ -268,6 +269,7 @@ describe('RuntimeFileCommands', () => {
it('opens previewable images through the renderer host as an image tab', async () => {
const openFile = vi.fn()
const { commands } = createRuntimeFileCommands({ openFile })
statMock.mockResolvedValue({ isDirectory: () => false })

const result = await commands.openMobileFile('id:wt-1', 'assets/logo.png')

Expand All @@ -285,6 +287,43 @@ describe('RuntimeFileCommands', () => {
})
})

it('fails the open when the resolved local file does not exist', async () => {
const openFile = vi.fn()
const { commands } = createRuntimeFileCommands({ openFile })
statMock.mockRejectedValue(enoent())

await expect(commands.openMobileFile('id:wt-1', 'spec/does-not-exist.md')).rejects.toThrow(
'file_not_found'
)
expect(openFile).not.toHaveBeenCalled()
})

it('fails the open when the resolved path is a directory', async () => {
const openFile = vi.fn()
const { commands } = createRuntimeFileCommands({ openFile })
statMock.mockResolvedValue({ isDirectory: () => true })

await expect(commands.openMobileFile('id:wt-1', 'docs')).rejects.toThrow('file_not_found')
expect(openFile).not.toHaveBeenCalled()
})

it('fails the open when the remote SSH file does not exist', async () => {
const openFile = vi.fn()
const resolveRuntimeFileTarget = vi.fn(async () => ({
worktree: { id: 'wt-1', repoId: 'repo-1', path: '/repo' },
connectionId: 'ssh-1'
}))
const { commands } = createRuntimeFileCommands({ openFile, resolveRuntimeFileTarget })
vi.mocked(getSshFilesystemProvider).mockReturnValue({
stat: vi.fn().mockRejectedValue(new Error('No such file or directory'))
} as never)

await expect(commands.openMobileFile('id:wt-1', 'spec/does-not-exist.md')).rejects.toThrow(
'file_not_found'
)
expect(openFile).not.toHaveBeenCalled()
})

it('leaves non-previewable binaries unavailable on mobile', async () => {
const openFile = vi.fn()
const { commands } = createRuntimeFileCommands({ openFile })
Expand Down
31 changes: 30 additions & 1 deletion src/main/runtime/orca-runtime-files.ts
Original file line number Diff line number Diff line change
Expand Up @@ -248,7 +248,7 @@ export class RuntimeFileCommands {
worktreeSelector: string,
relativePath: string
): Promise<RuntimeFileOpenResult> {
const { worktree } = await this.host.resolveRuntimeFileTarget(worktreeSelector)
const { worktree, connectionId } = await this.host.resolveRuntimeFileTarget(worktreeSelector)
if (!isSafeMobileRelativePath(relativePath)) {
throw new Error('invalid_relative_path')
}
Expand All @@ -265,6 +265,12 @@ export class RuntimeFileCommands {
return { worktree: worktree.id, relativePath, kind, opened: false }
}
const filePath = joinWorktreeRelativePath(worktree.path, relativePath)
// Why: a missing (or directory) path used to report opened:true / exit 0
// and create a ghost tab that only failed at render time, misleading
// CLI-driving agents into trusting the open succeeded (#8844).
if (!(await this.resolvedOpenTargetIsFile(filePath, connectionId))) {
throw new Error('file_not_found')
}
// Why: the service's internal runtimeId is not a registered runtime env selector
// (those live in orca-environments.json). Passing it caused Unknown environment
// errors on content load for CLI-initiated opens (via files.open from orca cli
Expand Down Expand Up @@ -482,6 +488,29 @@ export class RuntimeFileCommands {
return /\bENOENT\b|no such file|not found|does not exist/i.test(message)
}

// Existence probe for files.open across local and SSH worktrees. Genuine
// not-found (or a directory) → false; transport/permission failures still
// throw so a dropped remote session doesn't read as "file missing".
private async resolvedOpenTargetIsFile(
filePath: string,
connectionId: string | undefined
): Promise<boolean> {
try {
const stats = connectionId
? await this.statRemoteTerminalPath(filePath, connectionId)
: await stat(filePath)
return !stats.isDirectory()
} catch (error) {
if (
isENOENT(error) ||
(connectionId !== undefined && RuntimeFileCommands.isRemoteNotFoundErrorMessage(error))
) {
return false
}
throw error
}
}

private async statRemoteTerminalPath(
absolutePath: string,
connectionId: string
Expand Down
1 change: 1 addition & 0 deletions src/main/runtime/rpc/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ const RUNTIME_PASSTHROUGH_CODES: ReadonlySet<string> = new Set([
'terminal_gone',
'no_active_terminal',
'repo_not_found',
'file_not_found',
'timeout',
'invalid_limit'
])
Expand Down
Loading