Files
orca/src/renderer/src/components/editor/useEditorPanelRemoteSiblingContentState.test.tsx
T
Neil 359fec1a09 fix(terminal): a file link opens in the workspace it was clicked in (#32320)
* fix(terminal): a file link opens in the workspace it was clicked in

A terminal or chat link to a file another workspace on the same host holds used to switch to that workspace. It now opens as a plain read-only file in the workspace it was clicked in (local and WSL read on this computer; paired and managed servers read through the owning workspace's scope), with a toast naming the owner and an Open there action, and a Read-only badge in the editor header. HTML on server hosts, server workspaces the client does not know, directories, shift-click download and direct-SSH workspaces keep the switch. The view is watched with its owner through one derived scope (lib/open-file-owner-scope.ts); phones skip plain file tabs they cannot read by workspace-relative path.

Fixes #32158

* fix(terminal): a fresh id per other-workspace hint, and owner lookups cached per route inputs

* fix(terminal): only this computer's files open in place; SSH and server links keep opening in their workspace

Servers read files only by workspace-relative path, so a read-only view of another workspace's server file needed owner rerouting in every read path. Paired and managed servers keep today's switch; the loader moves any restored read-only server tab of another workspace's file there as before.
2026-10-23 18:14:13 -07:00

251 lines
8.2 KiB
TypeScript

// @vitest-environment happy-dom
import { act } from 'react'
import { createRoot, type Root } from 'react-dom/client'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import type { OpenFile } from '@/store/slices/editor'
import type { FileContent } from './editor-panel-content-types'
const mocks = vi.hoisted(() => ({
readRuntimeFileContent: vi.fn(),
findWorkspaceFileRoute: vi.fn(),
migrateRestoredEditorFileOwner: vi.fn(),
getState: vi.fn()
}))
vi.mock('@/runtime/runtime-file-routing', () => ({
getRuntimeFileReadScope: vi.fn(
(
settings: { activeRuntimeEnvironmentId?: string | null } | null | undefined,
connectionId?: string
) => connectionId ?? settings?.activeRuntimeEnvironmentId ?? null
)
}))
vi.mock('@/runtime/runtime-file-read-client', () => ({
readRuntimeFileContent: mocks.readRuntimeFileContent
}))
vi.mock('@/runtime/runtime-file-watch-client', () => ({ subscribeRuntimeFileChanges: vi.fn() }))
vi.mock('@/runtime/runtime-git-diff-client', () => ({
getRuntimeGitBranchDiff: vi.fn(),
getRuntimeGitCommitDiff: vi.fn(),
getRuntimeGitDiff: vi.fn()
}))
vi.mock('@/runtime/runtime-git-client-context', () => ({ getRuntimeGitScope: vi.fn(() => null) }))
vi.mock('@/lib/connection-context', () => ({
getConnectionId: vi.fn(),
getConnectionIdForFile: vi.fn(),
isWorktreeConnectionResolved: vi.fn(() => true)
}))
vi.mock('@/lib/worktree-host-connection-phase', () => import('./local-host-test-fixture'))
vi.mock('@/lib/runtime-workspace-file-route', () => ({
findWorkspaceFileRoute: mocks.findWorkspaceFileRoute
}))
vi.mock('./migrate-restored-editor-file-owner', () => ({
migrateRestoredEditorFileOwner: mocks.migrateRestoredEditorFileOwner
}))
vi.mock('@/lib/worktree-runtime-owner', () => ({
getExecutionHostIdForWorktree: vi.fn(() => 'local')
}))
// These fixtures' workspaces live on runtime-1, so an unstamped tab resolves there.
vi.mock('@/lib/file-owner-runtime-target', () => {
const getRuntimeTargetForFileOwner = (
_state: unknown,
_worktreeId: string | null | undefined,
runtimeEnvironmentId: string | null | undefined
) =>
runtimeEnvironmentId === null
? { kind: 'local' as const }
: { kind: 'environment' as const, environmentId: runtimeEnvironmentId ?? 'runtime-1' }
return {
FILE_OWNER_UNRESOLVED_MESSAGE: 'unresolved',
getRuntimeTargetForFileOwner,
requireRuntimeTargetForFileOwner: getRuntimeTargetForFileOwner,
getRuntimeTargetForWorktreeOwner: (state: unknown, worktreeId: string | null | undefined) =>
getRuntimeTargetForFileOwner(state, worktreeId, undefined)
}
})
vi.mock('@/store', () => ({
useAppStore: Object.assign(
(select: (state: ReturnType<typeof mocks.getState>) => unknown) => select(mocks.getState()),
{ getState: mocks.getState }
)
}))
vi.mock('./useEditorPanelExternalContentEvents', () => ({
useEditorPanelExternalContentEvents: vi.fn(),
usePruneClosedEditorContent: vi.fn()
}))
vi.mock('./useEditorPanelFileLoadRetry', () => ({ useEditorPanelFileLoadRetry: vi.fn() }))
vi.mock('./useLocalLogTail', () => ({ useLocalLogTail: vi.fn() }))
import { useEditorPanelContentState } from './useEditorPanelContentState'
let latestFileContents: Record<string, FileContent> = {}
function createOpenFile(overrides: Partial<OpenFile>): OpenFile {
return {
id: '/repo/file.ts',
filePath: '/repo/file.ts',
relativePath: 'file.ts',
worktreeId: 'wt-1',
language: 'typescript',
isDirty: false,
mode: 'edit',
...overrides
}
}
function HookProbe({ activeFile }: { activeFile: OpenFile }): null {
latestFileContents = useEditorPanelContentState({
activeFile,
isChangesMode: false,
openFiles: [activeFile],
gitStatusEntries: undefined,
editorViewMode: {}
}).fileContents
return null
}
describe('remote sibling editor content routing', () => {
let container: HTMLDivElement | null = null
let root: Root | null = null
beforeEach(() => {
latestFileContents = {}
vi.stubGlobal('api', { fs: {} })
mocks.readRuntimeFileContent.mockReset()
mocks.findWorkspaceFileRoute.mockReset()
mocks.findWorkspaceFileRoute.mockReturnValue(null)
mocks.migrateRestoredEditorFileOwner.mockReset()
mocks.migrateRestoredEditorFileOwner.mockResolvedValue({ ok: true, fileId: 'owned-file' })
mocks.getState.mockReset()
mocks.getState.mockReturnValue({
settings: { activeRuntimeEnvironmentId: 'runtime-1' },
openFiles: [],
setLastKnownDiskSignature: vi.fn()
})
container = document.body.appendChild(document.createElement('div'))
root = createRoot(container)
})
afterEach(() => {
act(() => root?.unmount())
container?.remove()
})
it('keeps a client-local live-tail log on the client', async () => {
const logPath = '/Users/me/.codex/sessions/session.jsonl'
const activeFile = createOpenFile({
id: logPath,
filePath: logPath,
relativePath: logPath,
worktreeId: 'repo-runtime::/work/demo-project',
readOnly: true,
liveTail: true
})
mocks.readRuntimeFileContent.mockResolvedValue({ content: 'log line', isBinary: false })
await act(async () => root?.render(<HookProbe activeFile={activeFile} />))
await vi.waitFor(() => expect(latestFileContents[activeFile.id]?.content).toBe('log line'))
expect(mocks.findWorkspaceFileRoute).not.toHaveBeenCalled()
expect(mocks.readRuntimeFileContent).toHaveBeenCalledWith(
expect.objectContaining({
connectionId: undefined,
includeLocalLogMetadata: true,
access: { kind: 'user-file' }
})
)
})
it('atomically reparents an unstamped sibling file before reading', async () => {
const activeFile = createOpenFile({
id: '/work/repo-b/docs/readme.md',
filePath: '/work/repo-b/docs/readme.md',
relativePath: '/work/repo-b/docs/readme.md',
worktreeId: 'repo-a::/work/repo-a'
})
const route = {
worktreeId: 'repo-b::/work/repo-b',
relativePath: 'docs/readme.md',
executionHostId: 'runtime:runtime-1'
} as const
mocks.findWorkspaceFileRoute.mockReturnValue(route)
await act(async () => root?.render(<HookProbe activeFile={activeFile} />))
await vi.waitFor(() =>
expect(mocks.migrateRestoredEditorFileOwner).toHaveBeenCalledWith(
activeFile.id,
route,
'runtime-1'
)
)
expect(mocks.readRuntimeFileContent).not.toHaveBeenCalled()
})
it('moves a restored read-only server tab of another workspace file to that workspace', async () => {
const activeFile = createOpenFile({
id: '/work/repo-b/docs/readme.md',
filePath: '/work/repo-b/docs/readme.md',
relativePath: '/work/repo-b/docs/readme.md',
worktreeId: 'repo-a::/work/repo-a',
runtimeEnvironmentId: 'runtime-1',
readOnly: true
})
const route = {
worktreeId: 'repo-b::/work/repo-b',
relativePath: 'docs/readme.md',
executionHostId: 'runtime:runtime-1'
} as const
mocks.findWorkspaceFileRoute.mockReturnValue(route)
await act(async () => root?.render(<HookProbe activeFile={activeFile} />))
// Why: a server reads only by workspace-relative path, so no view of it stays in another workspace.
await vi.waitFor(() =>
expect(mocks.migrateRestoredEditorFileOwner).toHaveBeenCalledWith(
activeFile.id,
route,
'runtime-1'
)
)
expect(mocks.readRuntimeFileContent).not.toHaveBeenCalled()
})
it('reports a sibling-owner collision instead of remaining in loading state', async () => {
const activeFile = createOpenFile({
id: '/work/repo-b/docs/readme.md',
filePath: '/work/repo-b/docs/readme.md',
relativePath: '/work/repo-b/docs/readme.md',
worktreeId: 'repo-a::/work/repo-a'
})
mocks.findWorkspaceFileRoute.mockReturnValue({
worktreeId: 'repo-b::/work/repo-b',
relativePath: 'docs/readme.md',
executionHostId: 'runtime:runtime-1'
})
mocks.migrateRestoredEditorFileOwner.mockResolvedValue({
ok: false,
reason: 'collision'
})
await act(async () => root?.render(<HookProbe activeFile={activeFile} />))
await vi.waitFor(() =>
expect(latestFileContents[activeFile.id]?.loadError).toBe(
'The sibling file is already open; close one tab before restoring it.'
)
)
})
})