mirror of
https://github.com/stablyai/orca.git
synced 2026-10-18 08:03:22 +00:00
Stat the resolved local/SSH path in openMobileFile before creating an editor tab so `orca file open` returns ok:false for ENOENT instead of a ghost tab. Fixes #8844.
1806 lines
65 KiB
TypeScript
1806 lines
65 KiB
TypeScript
/* eslint-disable max-lines -- Why: runtime file command tests share mocked fs,
|
|
authorization, and watcher lifecycle fixtures; splitting would duplicate the
|
|
setup that makes cross-command filesystem behavior comparable. */
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
import { EventEmitter } from 'node:events'
|
|
import { link, mkdtemp, readFile, realpath, rm, symlink, writeFile } from 'node:fs/promises'
|
|
import { tmpdir } from 'node:os'
|
|
import { join } from 'node:path'
|
|
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 * as GitRunner from '../git/runner'
|
|
|
|
const {
|
|
lstatMock,
|
|
openMock,
|
|
readdirMock,
|
|
renameMock,
|
|
resolveAuthorizedPathMock,
|
|
statMock,
|
|
watchInWatcherProcessMock,
|
|
closeWatcherInWatcherProcessMock,
|
|
checkRgAvailableMock,
|
|
getLocalGitOptionsForRegisteredWorktreeMock,
|
|
wslAwareSpawnMock,
|
|
watchMock
|
|
} = vi.hoisted(() => ({
|
|
checkRgAvailableMock: vi.fn(),
|
|
getLocalGitOptionsForRegisteredWorktreeMock: vi.fn(),
|
|
lstatMock: vi.fn(),
|
|
openMock: vi.fn(),
|
|
readdirMock: vi.fn(),
|
|
renameMock: vi.fn(),
|
|
resolveAuthorizedPathMock: vi.fn(),
|
|
statMock: vi.fn(),
|
|
watchInWatcherProcessMock: vi.fn(),
|
|
closeWatcherInWatcherProcessMock: vi.fn(),
|
|
wslAwareSpawnMock: vi.fn(),
|
|
watchMock: vi.fn()
|
|
}))
|
|
|
|
vi.mock('fs', async () => {
|
|
const actual = await vi.importActual<typeof Fs>('fs')
|
|
return {
|
|
...actual,
|
|
watch: watchMock
|
|
}
|
|
})
|
|
|
|
vi.mock('fs/promises', async () => {
|
|
const actual = await vi.importActual<typeof FsPromises>('fs/promises')
|
|
return {
|
|
...actual,
|
|
lstat: lstatMock,
|
|
open: (...args: Parameters<typeof actual.open>) => {
|
|
const impl = openMock.getMockImplementation()
|
|
return impl ? openMock(...args) : actual.open(...args)
|
|
},
|
|
readdir: readdirMock,
|
|
rename: renameMock,
|
|
stat: statMock
|
|
}
|
|
})
|
|
|
|
vi.mock('./file-watcher-host', () => ({
|
|
closeFileExplorerWatcherInWatcherProcess: closeWatcherInWatcherProcessMock,
|
|
watchFileExplorerInWatcherProcess: watchInWatcherProcessMock
|
|
}))
|
|
|
|
vi.mock('../ipc/filesystem-auth', async () => {
|
|
const actual = await vi.importActual<typeof FilesystemAuth>('../ipc/filesystem-auth')
|
|
return {
|
|
...actual,
|
|
resolveAuthorizedPath: resolveAuthorizedPathMock
|
|
}
|
|
})
|
|
|
|
vi.mock('../git/runner', async () => {
|
|
const actual = await vi.importActual<typeof GitRunner>('../git/runner')
|
|
return {
|
|
...actual,
|
|
wslAwareSpawn: wslAwareSpawnMock
|
|
}
|
|
})
|
|
|
|
vi.mock('../ipc/rg-availability', () => ({
|
|
checkRgAvailable: checkRgAvailableMock
|
|
}))
|
|
|
|
vi.mock('../ipc/local-worktree-runtime-options', () => ({
|
|
getLocalGitOptionsForRegisteredWorktree: getLocalGitOptionsForRegisteredWorktreeMock
|
|
}))
|
|
|
|
vi.mock('../providers/ssh-filesystem-dispatch', () => ({
|
|
getSshFilesystemProvider: vi.fn(),
|
|
SSH_FILESYSTEM_PROVIDER_UNAVAILABLE_MESSAGE:
|
|
'Remote connection dropped. Click Reconnect on the SSH target before retrying.'
|
|
}))
|
|
|
|
import { awaitRuntimeFileWatcherUnsubscribes, RuntimeFileCommands } from './orca-runtime-files'
|
|
import { getSshFilesystemProvider } from '../providers/ssh-filesystem-dispatch'
|
|
import { SEARCH_TIMEOUT_MS } from '../../shared/text-search'
|
|
|
|
type MockRuntimeSearchChild = EventEmitter & {
|
|
stdout: EventEmitter & { setEncoding: ReturnType<typeof vi.fn> }
|
|
stderr: EventEmitter
|
|
kill: ReturnType<typeof vi.fn>
|
|
}
|
|
|
|
function enoent(): Error {
|
|
return Object.assign(new Error('ENOENT'), { code: 'ENOENT' })
|
|
}
|
|
|
|
function mockStats(dev: number, ino: number) {
|
|
return { dev, ino, isDirectory: () => false }
|
|
}
|
|
|
|
function dirEntry(args: { name: string; directory?: boolean; symlink?: boolean }) {
|
|
return {
|
|
name: args.name,
|
|
isDirectory: () => args.directory ?? false,
|
|
isSymbolicLink: () => args.symlink ?? false
|
|
}
|
|
}
|
|
|
|
function mockLocalPathStats(entries: Record<string, [number, number]>) {
|
|
resolveAuthorizedPathMock.mockImplementation(async (p: string) => p)
|
|
lstatMock.mockImplementation(async (p: string) => {
|
|
const entry = entries[p]
|
|
if (entry) {
|
|
return mockStats(entry[0], entry[1])
|
|
}
|
|
throw enoent()
|
|
})
|
|
}
|
|
|
|
function createRuntimeFileCommands(options?: {
|
|
path?: string
|
|
openFile?: ReturnType<typeof vi.fn>
|
|
openDiff?: ReturnType<typeof vi.fn>
|
|
resolveRuntimeFileTarget?: ReturnType<typeof vi.fn>
|
|
resolveRuntimeGitTarget?: ReturnType<typeof vi.fn>
|
|
resolveTerminalCwd?: ReturnType<typeof vi.fn>
|
|
resolveTerminalContext?: ReturnType<typeof vi.fn>
|
|
resolveTerminalFileUriHostname?: ReturnType<typeof vi.fn>
|
|
hasRecentTerminalOutputPath?: ReturnType<typeof vi.fn>
|
|
}) {
|
|
const store = {
|
|
getRepo: vi.fn((_repoId?: string) => undefined as { connectionId?: string } | undefined)
|
|
}
|
|
const path = options?.path ?? '/repo'
|
|
const worktree = {
|
|
id: 'wt-1',
|
|
repoId: 'repo-1',
|
|
path
|
|
}
|
|
const commands = new RuntimeFileCommands({
|
|
getRuntimeId: () => 'runtime-1',
|
|
requireStore: () => store,
|
|
resolveWorktreeSelector: vi.fn(async () => worktree),
|
|
resolveRuntimeFileTarget:
|
|
options?.resolveRuntimeFileTarget ??
|
|
vi.fn(async () => ({
|
|
worktree,
|
|
connectionId: store.getRepo(worktree.repoId)?.connectionId
|
|
})),
|
|
resolveTerminalCwd: options?.resolveTerminalCwd ?? vi.fn(() => path),
|
|
resolveTerminalContext:
|
|
options?.resolveTerminalContext ??
|
|
vi.fn(() => ({
|
|
worktreeId: worktree.id,
|
|
connectionId: store.getRepo(worktree.repoId)?.connectionId ?? null
|
|
})),
|
|
...(options?.resolveTerminalFileUriHostname
|
|
? { resolveTerminalFileUriHostname: options.resolveTerminalFileUriHostname }
|
|
: {}),
|
|
hasRecentTerminalOutputPath: options?.hasRecentTerminalOutputPath ?? vi.fn(() => true),
|
|
resolveRuntimeGitTarget: options?.resolveRuntimeGitTarget ?? vi.fn(),
|
|
openFile: options?.openFile ?? vi.fn(),
|
|
...(options?.openDiff ? { openDiff: options.openDiff } : {})
|
|
} as never)
|
|
return { commands, store }
|
|
}
|
|
|
|
function createRuntimeSearchChild(): MockRuntimeSearchChild {
|
|
const child = new EventEmitter() as MockRuntimeSearchChild
|
|
child.stdout = new EventEmitter() as MockRuntimeSearchChild['stdout']
|
|
child.stdout.setEncoding = vi.fn()
|
|
child.stderr = new EventEmitter()
|
|
child.kill = vi.fn()
|
|
return child
|
|
}
|
|
|
|
describe('RuntimeFileCommands', () => {
|
|
const originalPlatform = process.platform
|
|
|
|
beforeEach(() => {
|
|
vi.useFakeTimers()
|
|
lstatMock.mockReset()
|
|
openMock.mockReset()
|
|
readdirMock.mockReset()
|
|
renameMock.mockReset()
|
|
resolveAuthorizedPathMock.mockReset()
|
|
statMock.mockReset()
|
|
watchInWatcherProcessMock.mockReset()
|
|
closeWatcherInWatcherProcessMock.mockReset()
|
|
watchMock.mockReset()
|
|
checkRgAvailableMock.mockReset()
|
|
vi.mocked(getSshFilesystemProvider).mockReset()
|
|
getLocalGitOptionsForRegisteredWorktreeMock.mockReset()
|
|
wslAwareSpawnMock.mockReset()
|
|
getLocalGitOptionsForRegisteredWorktreeMock.mockReturnValue({})
|
|
readdirMock.mockResolvedValue([])
|
|
lstatMock.mockRejectedValue(enoent())
|
|
renameMock.mockResolvedValue(undefined)
|
|
Object.defineProperty(process, 'platform', {
|
|
configurable: true,
|
|
value: originalPlatform
|
|
})
|
|
})
|
|
|
|
afterEach(async () => {
|
|
await awaitRuntimeFileWatcherUnsubscribes()
|
|
Object.defineProperty(process, 'platform', {
|
|
configurable: true,
|
|
value: originalPlatform
|
|
})
|
|
vi.useRealTimers()
|
|
})
|
|
|
|
it('opens source control diffs through the renderer host (inheriting active runtime env)', async () => {
|
|
const openDiff = vi.fn()
|
|
const { commands } = createRuntimeFileCommands({ openDiff })
|
|
|
|
const result = await commands.openMobileDiff('id:wt-1', 'docs/readme.md', true)
|
|
|
|
expect(openDiff).toHaveBeenCalledWith(
|
|
'wt-1',
|
|
'/repo/docs/readme.md',
|
|
'docs/readme.md',
|
|
true,
|
|
undefined
|
|
)
|
|
expect(result).toEqual({
|
|
worktree: 'wt-1',
|
|
relativePath: 'docs/readme.md',
|
|
kind: 'markdown',
|
|
opened: true
|
|
})
|
|
})
|
|
|
|
it('opens text files through the renderer host (inheriting active runtime env)', async () => {
|
|
const openFile = vi.fn()
|
|
const { commands } = createRuntimeFileCommands({ openFile })
|
|
resolveAuthorizedPathMock.mockResolvedValue('/repo/docs/readme.md')
|
|
statMock.mockResolvedValue({ isDirectory: () => false })
|
|
|
|
const result = await commands.openMobileFile('id:wt-1', 'docs/readme.md')
|
|
|
|
expect(openFile).toHaveBeenCalledWith(
|
|
'wt-1',
|
|
'/repo/docs/readme.md',
|
|
'docs/readme.md',
|
|
undefined
|
|
)
|
|
expect(result).toEqual({
|
|
worktree: 'wt-1',
|
|
relativePath: 'docs/readme.md',
|
|
kind: 'markdown',
|
|
opened: true
|
|
})
|
|
})
|
|
|
|
it('opens previewable images through the renderer host as an image tab', async () => {
|
|
const openFile = vi.fn()
|
|
const { commands } = createRuntimeFileCommands({ openFile })
|
|
resolveAuthorizedPathMock.mockResolvedValue('/repo/assets/logo.png')
|
|
statMock.mockResolvedValue({ isDirectory: () => false })
|
|
|
|
const result = await commands.openMobileFile('id:wt-1', 'assets/logo.png')
|
|
|
|
expect(openFile).toHaveBeenCalledWith(
|
|
'wt-1',
|
|
'/repo/assets/logo.png',
|
|
'assets/logo.png',
|
|
undefined
|
|
)
|
|
expect(result).toEqual({
|
|
worktree: 'wt-1',
|
|
relativePath: 'assets/logo.png',
|
|
kind: 'image',
|
|
opened: true
|
|
})
|
|
})
|
|
|
|
it('leaves non-previewable binaries unavailable on mobile', async () => {
|
|
const openFile = vi.fn()
|
|
const { commands } = createRuntimeFileCommands({ openFile })
|
|
|
|
const result = await commands.openMobileFile('id:wt-1', 'dist/bundle.zip')
|
|
|
|
expect(openFile).not.toHaveBeenCalled()
|
|
expect(statMock).not.toHaveBeenCalled()
|
|
expect(result).toEqual({
|
|
worktree: 'wt-1',
|
|
relativePath: 'dist/bundle.zip',
|
|
kind: 'binary',
|
|
opened: false
|
|
})
|
|
})
|
|
|
|
it('rejects missing local files without creating an editor tab', async () => {
|
|
const openFile = vi.fn()
|
|
const { commands } = createRuntimeFileCommands({ openFile })
|
|
resolveAuthorizedPathMock.mockResolvedValue('/repo/docs/missing.md')
|
|
statMock.mockRejectedValue(enoent())
|
|
|
|
await expect(commands.openMobileFile('id:wt-1', 'docs/missing.md')).rejects.toThrow(
|
|
"ENOENT: no such file or directory, open '/repo/docs/missing.md'"
|
|
)
|
|
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'
|
|
},
|
|
connectionId: '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('does not follow symlinks when reading runtime-local file explorer dirs', async () => {
|
|
const { commands } = createRuntimeFileCommands()
|
|
resolveAuthorizedPathMock.mockResolvedValue('/repo')
|
|
readdirMock.mockResolvedValue([
|
|
dirEntry({ name: 'README.md' }),
|
|
dirEntry({ name: 'linked-docs', directory: true, symlink: true })
|
|
])
|
|
|
|
const result = await commands.readFileExplorerDir('id:wt-1', '')
|
|
|
|
expect(result).toEqual([
|
|
{ name: 'linked-docs', isDirectory: false, isSymlink: true },
|
|
{ name: 'README.md', isDirectory: false, isSymlink: false }
|
|
])
|
|
expect(statMock).not.toHaveBeenCalledWith('/repo/linked-docs')
|
|
})
|
|
|
|
it('renames a runtime-local file when destination does not exist', async () => {
|
|
const { commands } = createRuntimeFileCommands()
|
|
resolveAuthorizedPathMock.mockImplementation(async (p: string) => p)
|
|
|
|
await commands.renameFileExplorerPath('id:wt-1', 'old.ts', 'new.ts')
|
|
|
|
expect(renameMock).toHaveBeenCalledWith('/repo/old.ts', '/repo/new.ts')
|
|
})
|
|
|
|
it('allows runtime-local case-only rename with IPC parity guard behavior', async () => {
|
|
const { commands } = createRuntimeFileCommands()
|
|
mockLocalPathStats({
|
|
'/repo/README.md': [10, 100],
|
|
'/repo/readme.md': [10, 100]
|
|
})
|
|
|
|
await commands.renameFileExplorerPath('id:wt-1', 'README.md', 'readme.md')
|
|
|
|
expect(renameMock).toHaveBeenCalledWith('/repo/README.md', '/repo/readme.md')
|
|
})
|
|
|
|
it('rejects runtime-local true destination collisions', async () => {
|
|
const { commands } = createRuntimeFileCommands()
|
|
mockLocalPathStats({
|
|
'/repo/old.ts': [11, 110],
|
|
'/repo/new.ts': [11, 111]
|
|
})
|
|
|
|
await expect(commands.renameFileExplorerPath('id:wt-1', 'old.ts', 'new.ts')).rejects.toThrow(
|
|
"A file or folder named 'new.ts' already exists in this location"
|
|
)
|
|
|
|
expect(renameMock).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('rejects runtime-local hard-link alias collisions', async () => {
|
|
const { commands } = createRuntimeFileCommands()
|
|
mockLocalPathStats({
|
|
'/repo/README.md': [12, 120],
|
|
'/repo/README-hardlink.md': [12, 120]
|
|
})
|
|
|
|
await expect(
|
|
commands.renameFileExplorerPath('id:wt-1', 'README.md', 'README-hardlink.md')
|
|
).rejects.toThrow("A file or folder named 'README-hardlink.md' already exists in this location")
|
|
|
|
expect(renameMock).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('rejects runtime-local cross-parent case-only collisions', async () => {
|
|
const { commands } = createRuntimeFileCommands()
|
|
mockLocalPathStats({
|
|
'/repo/src/README.md': [13, 130],
|
|
'/repo/docs/readme.md': [13, 130]
|
|
})
|
|
|
|
await expect(
|
|
commands.renameFileExplorerPath('id:wt-1', 'src/README.md', 'docs/readme.md')
|
|
).rejects.toThrow("A file or folder named 'readme.md' already exists in this location")
|
|
|
|
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)
|
|
const { commands, store } = createRuntimeFileCommands()
|
|
store.getRepo.mockReturnValue({ connectionId: 'ssh-1' })
|
|
|
|
await commands.renameFileExplorerPath('id:wt-1', 'old.ts', 'new.ts')
|
|
|
|
expect(renameNoClobber).toHaveBeenCalledWith('/repo/old.ts', '/repo/new.ts')
|
|
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')).rejects.toThrow(
|
|
'destination exists'
|
|
)
|
|
expect(renameMock).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('uses a conservative Node watcher for Windows runtime file watches', async () => {
|
|
Object.defineProperty(process, 'platform', {
|
|
configurable: true,
|
|
value: 'win32'
|
|
})
|
|
|
|
const watcher = new EventEmitter() as EventEmitter & { close: ReturnType<typeof vi.fn> }
|
|
watcher.close = vi.fn(() => queueMicrotask(() => watcher.emit('close')))
|
|
let listener: (() => void) | null = null
|
|
watchMock.mockImplementation((_rootPath, _options, callback) => {
|
|
listener = callback
|
|
return watcher
|
|
})
|
|
resolveAuthorizedPathMock.mockResolvedValue('C:\\repo')
|
|
statMock.mockResolvedValue({ isDirectory: () => true })
|
|
const { commands } = createRuntimeFileCommands({ path: 'C:\\repo' })
|
|
const onEvents = vi.fn()
|
|
|
|
const unsubscribe = await commands.watchFileExplorer('id:wt-1', onEvents)
|
|
|
|
expect(watchMock).toHaveBeenCalledWith('C:\\repo', { recursive: true }, expect.any(Function))
|
|
const emit = listener as (() => void) | null
|
|
expect(emit).not.toBeNull()
|
|
|
|
emit?.()
|
|
emit?.()
|
|
await vi.advanceTimersByTimeAsync(149)
|
|
expect(onEvents).not.toHaveBeenCalled()
|
|
|
|
await vi.advanceTimersByTimeAsync(1)
|
|
expect(onEvents).toHaveBeenCalledTimes(1)
|
|
expect(onEvents).toHaveBeenCalledWith([{ kind: 'overflow', absolutePath: 'C:\\repo' }])
|
|
|
|
await unsubscribe()
|
|
expect(watcher.close).toHaveBeenCalledTimes(1)
|
|
})
|
|
|
|
it('delegates local recursive watching to the watcher process', async () => {
|
|
Object.defineProperty(process, 'platform', {
|
|
configurable: true,
|
|
value: 'linux'
|
|
})
|
|
resolveAuthorizedPathMock.mockResolvedValue('/repo')
|
|
statMock.mockResolvedValue({ isDirectory: () => true })
|
|
const dispose = vi.fn()
|
|
watchInWatcherProcessMock.mockResolvedValue(dispose)
|
|
const { commands } = createRuntimeFileCommands()
|
|
|
|
const unsubscribe = await commands.watchFileExplorer('id:wt-1', vi.fn())
|
|
expect(watchInWatcherProcessMock).toHaveBeenCalledWith(
|
|
'/repo',
|
|
expect.any(Function),
|
|
expect.any(Function),
|
|
undefined
|
|
)
|
|
|
|
unsubscribe()
|
|
await awaitRuntimeFileWatcherUnsubscribes()
|
|
expect(dispose).toHaveBeenCalledTimes(1)
|
|
})
|
|
|
|
it('keeps SSH runtime watches on the remote filesystem provider', async () => {
|
|
const remoteDispose = vi.fn()
|
|
const providerWatch = vi.fn(() => remoteDispose)
|
|
vi.mocked(getSshFilesystemProvider).mockReturnValue({ watch: providerWatch } as never)
|
|
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', onEvents, {
|
|
signal: undefined,
|
|
onTerminalError: expect.any(Function)
|
|
})
|
|
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()
|
|
vi.mocked(getSshFilesystemProvider).mockImplementation(
|
|
(connectionId) =>
|
|
({
|
|
watch: vi.fn(() => (connectionId === 'ssh-1' ? firstDispose : secondDispose))
|
|
}) as never
|
|
)
|
|
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)
|
|
})
|
|
|
|
it('settles and detaches runtime rg searches when timeout kill is ignored', async () => {
|
|
const resolveRuntimeFileTarget = vi.fn(async () => ({
|
|
worktree: {
|
|
id: 'wt-1',
|
|
repoId: 'repo-1',
|
|
path: '/repo'
|
|
},
|
|
connectionId: null
|
|
}))
|
|
const { commands } = createRuntimeFileCommands({ resolveRuntimeFileTarget })
|
|
const child = createRuntimeSearchChild()
|
|
resolveAuthorizedPathMock.mockResolvedValue('/repo')
|
|
checkRgAvailableMock.mockResolvedValue(true)
|
|
wslAwareSpawnMock.mockReturnValue(child)
|
|
|
|
const resultPromise = commands.searchRuntimeFiles('id:wt-1', {
|
|
query: 'needle',
|
|
maxResults: 10
|
|
})
|
|
await vi.advanceTimersByTimeAsync(SEARCH_TIMEOUT_MS)
|
|
|
|
await expect(resultPromise).resolves.toMatchObject({
|
|
files: [],
|
|
totalMatches: 0,
|
|
truncated: true
|
|
})
|
|
expect(child.kill).toHaveBeenCalledTimes(1)
|
|
expect(child.stdout.listenerCount('data')).toBe(0)
|
|
expect(child.stderr.listenerCount('data')).toBe(0)
|
|
expect(child.listenerCount('error')).toBe(0)
|
|
expect(child.listenerCount('close')).toBe(0)
|
|
})
|
|
|
|
it('routes runtime rg searches through the registered WSL project runtime', async () => {
|
|
const resolveRuntimeFileTarget = vi.fn(async () => ({
|
|
worktree: {
|
|
id: 'wt-1',
|
|
repoId: 'repo-1',
|
|
path: 'C:\\repo'
|
|
},
|
|
connectionId: null
|
|
}))
|
|
const { commands, store } = createRuntimeFileCommands({ resolveRuntimeFileTarget })
|
|
const child = createRuntimeSearchChild()
|
|
resolveAuthorizedPathMock.mockResolvedValue('C:\\repo')
|
|
checkRgAvailableMock.mockResolvedValue(true)
|
|
getLocalGitOptionsForRegisteredWorktreeMock.mockReturnValue({ wslDistro: 'Ubuntu' })
|
|
wslAwareSpawnMock.mockReturnValue(child)
|
|
|
|
const resultPromise = commands.searchRuntimeFiles('id:wt-1', {
|
|
query: 'needle',
|
|
maxResults: 10
|
|
})
|
|
await Promise.resolve()
|
|
await Promise.resolve()
|
|
await Promise.resolve()
|
|
child.emit('close')
|
|
|
|
await expect(resultPromise).resolves.toMatchObject({ files: [] })
|
|
expect(getLocalGitOptionsForRegisteredWorktreeMock).toHaveBeenCalledWith(
|
|
store,
|
|
'C:\\repo',
|
|
'C:\\repo'
|
|
)
|
|
expect(checkRgAvailableMock).toHaveBeenCalledWith('C:\\repo', 'Ubuntu')
|
|
expect(wslAwareSpawnMock).toHaveBeenCalledWith(
|
|
'rg',
|
|
expect.any(Array),
|
|
expect.objectContaining({
|
|
cwd: 'C:\\repo',
|
|
wslDistro: 'Ubuntu'
|
|
})
|
|
)
|
|
})
|
|
|
|
describe('resolveTerminalPath', () => {
|
|
let tempDirs: string[] = []
|
|
|
|
afterEach(async () => {
|
|
await Promise.all(tempDirs.map((dir) => rm(dir, { recursive: true, force: true })))
|
|
tempDirs = []
|
|
})
|
|
|
|
async function tempFile(name: string, content: string): Promise<string> {
|
|
const dir = await mkdtemp(join(tmpdir(), 'orca-terminal-artifact-'))
|
|
tempDirs.push(dir)
|
|
const filePath = join(dir, name)
|
|
await writeFile(filePath, content)
|
|
return filePath
|
|
}
|
|
|
|
function absoluteFileTarget(result: {
|
|
openTarget?: { kind: string; absolutePath?: string; grantId?: string }
|
|
}): { absolutePath: string; grantId: string } {
|
|
if (
|
|
result.openTarget?.kind !== 'absolute-file' ||
|
|
typeof result.openTarget.absolutePath !== 'string' ||
|
|
typeof result.openTarget.grantId !== 'string'
|
|
) {
|
|
throw new Error('Expected an absolute terminal artifact target')
|
|
}
|
|
return result.openTarget as { absolutePath: string; grantId: string }
|
|
}
|
|
|
|
function statAsFile() {
|
|
resolveAuthorizedPathMock.mockImplementation(async (p: string) => p)
|
|
statMock.mockResolvedValue({ isDirectory: () => false, size: 12, dev: 1, ino: 2, mtimeMs: 3 })
|
|
}
|
|
|
|
function resolveTerminalArtifactPath(
|
|
commands: RuntimeFileCommands,
|
|
pathText: string,
|
|
cwd: string | null = null,
|
|
clientId = 'client-a'
|
|
) {
|
|
return commands.resolveTerminalPath('id:wt-1', pathText, cwd, clientId, 'term-1')
|
|
}
|
|
|
|
function createRemoteTerminalArtifactGrantFixture(artifactPath = '/tmp/result.json') {
|
|
const { commands, store } = createRuntimeFileCommands({ path: '/repo' })
|
|
store.getRepo.mockReturnValue({ connectionId: 'ssh-1' })
|
|
let realArtifactPath = artifactPath
|
|
const stat = vi.fn().mockResolvedValue({ type: 'file', size: 11, mtime: 3 })
|
|
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
|
|
}
|
|
}
|
|
}
|
|
|
|
it('resolves an absolute path inside the worktree to a relative path', async () => {
|
|
const { commands } = createRuntimeFileCommands({ path: '/repo' })
|
|
statAsFile()
|
|
|
|
const result = await commands.resolveTerminalPath('id:wt-1', '/repo/src/index.ts')
|
|
|
|
expect(result).toEqual({
|
|
worktree: 'wt-1',
|
|
relativePath: 'src/index.ts',
|
|
absolutePath: '/repo/src/index.ts',
|
|
exists: true,
|
|
isDirectory: false,
|
|
openTarget: {
|
|
kind: 'worktree-file',
|
|
provider: 'local',
|
|
relativePath: 'src/index.ts',
|
|
absolutePath: '/repo/src/index.ts'
|
|
}
|
|
})
|
|
})
|
|
|
|
it('resolves a relative path against the provided cwd', async () => {
|
|
const { commands } = createRuntimeFileCommands({ path: '/repo' })
|
|
statAsFile()
|
|
|
|
const result = await commands.resolveTerminalPath('id:wt-1', 'index.ts', '/repo/src')
|
|
|
|
expect(result).toMatchObject({ relativePath: 'src/index.ts', exists: true })
|
|
})
|
|
|
|
it('prefers the source terminal cwd over stale mobile cached cwd metadata', async () => {
|
|
const resolveTerminalCwd = vi.fn(() => '/repo/current')
|
|
const { commands } = createRuntimeFileCommands({ path: '/repo', resolveTerminalCwd })
|
|
statAsFile()
|
|
|
|
const result = await commands.resolveTerminalPath(
|
|
'id:wt-1',
|
|
'index.ts',
|
|
'/repo/stale',
|
|
'client-a',
|
|
'term-1'
|
|
)
|
|
|
|
expect(resolveTerminalCwd).toHaveBeenCalledWith('term-1')
|
|
expect(result).toMatchObject({ relativePath: 'current/index.ts', exists: true })
|
|
})
|
|
|
|
it('falls back to the source terminal cwd when mobile has not cached cwd metadata', async () => {
|
|
const resolveTerminalCwd = vi.fn(() => '/repo/src')
|
|
const { commands } = createRuntimeFileCommands({ path: '/repo', resolveTerminalCwd })
|
|
statAsFile()
|
|
|
|
const result = await commands.resolveTerminalPath(
|
|
'id:wt-1',
|
|
'index.ts',
|
|
null,
|
|
'client-a',
|
|
'term-1'
|
|
)
|
|
|
|
expect(resolveTerminalCwd).toHaveBeenCalledWith('term-1')
|
|
expect(result).toMatchObject({ relativePath: 'src/index.ts', exists: true })
|
|
})
|
|
|
|
it('awaits async source terminal cwd fallback from the PTY provider', async () => {
|
|
const resolveTerminalCwd = vi.fn(async () => '/repo/packages/app')
|
|
const { commands } = createRuntimeFileCommands({ path: '/repo', resolveTerminalCwd })
|
|
statAsFile()
|
|
|
|
const result = await commands.resolveTerminalPath(
|
|
'id:wt-1',
|
|
'package.json',
|
|
null,
|
|
'client-a',
|
|
'term-1'
|
|
)
|
|
|
|
expect(result).toMatchObject({ relativePath: 'packages/app/package.json', exists: true })
|
|
})
|
|
|
|
it('resolves a relative path against the worktree root when no cwd is given', async () => {
|
|
const { commands } = createRuntimeFileCommands({ path: '/repo' })
|
|
statAsFile()
|
|
|
|
const result = await commands.resolveTerminalPath('id:wt-1', 'docs/readme.md')
|
|
|
|
expect(result).toMatchObject({ relativePath: 'docs/readme.md', exists: true })
|
|
})
|
|
|
|
it('reports a directory', async () => {
|
|
const { commands } = createRuntimeFileCommands({ path: '/repo' })
|
|
resolveAuthorizedPathMock.mockImplementation(async (p: string) => p)
|
|
statMock.mockResolvedValue({ isDirectory: () => true })
|
|
|
|
const result = await commands.resolveTerminalPath('id:wt-1', '/repo/src')
|
|
|
|
expect(result).toMatchObject({ relativePath: 'src', isDirectory: true, exists: true })
|
|
})
|
|
|
|
it('returns an absolute open target for an existing local temp path outside the worktree', async () => {
|
|
const artifactPath = await tempFile('result.json', '{}')
|
|
const { commands } = createRuntimeFileCommands({ path: '/repo' })
|
|
resolveAuthorizedPathMock.mockImplementation(async (p: string) => p)
|
|
statMock.mockResolvedValue({ isDirectory: () => false, size: 12, dev: 1, ino: 2, mtimeMs: 3 })
|
|
const canonicalPath = await realpath(artifactPath)
|
|
|
|
const result = await resolveTerminalArtifactPath(commands, artifactPath)
|
|
|
|
expect(result).toMatchObject({
|
|
worktree: 'wt-1',
|
|
relativePath: null,
|
|
absolutePath: canonicalPath,
|
|
exists: true,
|
|
isDirectory: false,
|
|
openTarget: {
|
|
kind: 'absolute-file',
|
|
provider: 'local',
|
|
absolutePath: canonicalPath
|
|
}
|
|
})
|
|
expect(result.openTarget?.kind === 'absolute-file' ? result.openTarget.grantId : '').toMatch(
|
|
/\S/
|
|
)
|
|
expect(resolveAuthorizedPathMock).not.toHaveBeenCalledWith(canonicalPath, expect.anything())
|
|
expect(statMock).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('does not mint an absolute terminal artifact grant without a source terminal', async () => {
|
|
const artifactPath = await tempFile('result.json', '{}')
|
|
const { commands } = createRuntimeFileCommands({ path: '/repo' })
|
|
|
|
const result = await commands.resolveTerminalPath('id:wt-1', artifactPath, null, 'client-a')
|
|
|
|
expect(result).toMatchObject({
|
|
worktree: 'wt-1',
|
|
relativePath: null,
|
|
exists: false,
|
|
isDirectory: false
|
|
})
|
|
expect(result.openTarget).toBeUndefined()
|
|
})
|
|
|
|
it('does not mint an absolute terminal artifact grant for an unobserved path', async () => {
|
|
const artifactPath = await tempFile('result.json', '{}')
|
|
const hasRecentTerminalOutputPath = vi.fn(
|
|
(_terminalHandle: string, _pathText: string, _absolutePath: string) => false
|
|
)
|
|
const { commands } = createRuntimeFileCommands({
|
|
path: '/repo',
|
|
hasRecentTerminalOutputPath
|
|
})
|
|
|
|
const result = await resolveTerminalArtifactPath(commands, artifactPath)
|
|
|
|
expect(result).toMatchObject({
|
|
worktree: 'wt-1',
|
|
relativePath: null,
|
|
exists: false,
|
|
isDirectory: false
|
|
})
|
|
expect(result.openTarget).toBeUndefined()
|
|
expect(hasRecentTerminalOutputPath).toHaveBeenCalledTimes(1)
|
|
const observed = hasRecentTerminalOutputPath.mock.calls[0]!
|
|
expect(observed[0]).toBe('term-1')
|
|
expect(observed[1]).toBe(artifactPath)
|
|
expect(observed[2]).toContain('result.json')
|
|
expect(observed[1]).toContain('result.json')
|
|
})
|
|
|
|
it('does not mint an absolute artifact grant from a relative path observed under stale cwd', async () => {
|
|
const dir = await mkdtemp(join(tmpdir(), 'orca-terminal-artifact-'))
|
|
tempDirs.push(dir)
|
|
const artifactPath = join(dir, 'result.json')
|
|
await writeFile(artifactPath, '{}')
|
|
const hasRecentTerminalOutputPath = vi.fn(
|
|
(_terminalHandle: string, pathText: string, _absolutePath: string) =>
|
|
pathText === 'result.json'
|
|
)
|
|
const { commands } = createRuntimeFileCommands({
|
|
path: '/repo',
|
|
hasRecentTerminalOutputPath
|
|
})
|
|
resolveAuthorizedPathMock.mockImplementation(async (p: string) => p)
|
|
statMock.mockRejectedValue(enoent())
|
|
|
|
const result = await resolveTerminalArtifactPath(commands, 'result.json', dir)
|
|
|
|
expect(result).toMatchObject({
|
|
relativePath: 'result.json',
|
|
absolutePath: '/repo/result.json',
|
|
exists: false
|
|
})
|
|
expect(result.openTarget).toBeUndefined()
|
|
expect(hasRecentTerminalOutputPath).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('does not mint an artifact grant from a terminal attached to a different worktree', async () => {
|
|
const artifactPath = await tempFile('result.json', '{}')
|
|
const { commands } = createRuntimeFileCommands({
|
|
path: '/repo',
|
|
resolveTerminalContext: vi.fn(() => ({ worktreeId: 'other-wt', connectionId: null }))
|
|
})
|
|
|
|
const result = await resolveTerminalArtifactPath(commands, artifactPath)
|
|
|
|
expect(result).toMatchObject({
|
|
worktree: 'wt-1',
|
|
relativePath: null,
|
|
exists: false,
|
|
isDirectory: false
|
|
})
|
|
expect(result.openTarget).toBeUndefined()
|
|
})
|
|
|
|
it('does not mint a local artifact grant from an SSH terminal handle', async () => {
|
|
const artifactPath = await tempFile('result.json', '{}')
|
|
const { commands } = createRuntimeFileCommands({
|
|
path: '/repo',
|
|
resolveTerminalContext: vi.fn(() => ({ worktreeId: 'wt-1', connectionId: 'ssh-1' }))
|
|
})
|
|
|
|
const result = await resolveTerminalArtifactPath(commands, artifactPath)
|
|
|
|
expect(result).toMatchObject({
|
|
worktree: 'wt-1',
|
|
relativePath: null,
|
|
exists: false,
|
|
isDirectory: false
|
|
})
|
|
expect(result.openTarget).toBeUndefined()
|
|
})
|
|
|
|
it('uses the canonical local temp artifact path for the exact grant', async () => {
|
|
const dir = await mkdtemp(join(tmpdir(), 'orca-terminal-artifact-'))
|
|
tempDirs.push(dir)
|
|
const artifactPath = join(dir, 'result.json')
|
|
const linkPath = join(dir, 'link-result.json')
|
|
await writeFile(artifactPath, '{}')
|
|
await symlink(artifactPath, linkPath)
|
|
const { commands } = createRuntimeFileCommands({ path: '/repo' })
|
|
resolveAuthorizedPathMock.mockImplementation(async (p: string) => p)
|
|
statMock.mockResolvedValue({ isDirectory: () => false, size: 2, dev: 1, ino: 2, mtimeMs: 3 })
|
|
const canonicalPath = await realpath(artifactPath)
|
|
|
|
const result = await resolveTerminalArtifactPath(commands, linkPath)
|
|
|
|
expect(result).toMatchObject({
|
|
relativePath: null,
|
|
absolutePath: canonicalPath,
|
|
exists: true,
|
|
openTarget: {
|
|
kind: 'absolute-file',
|
|
provider: 'local',
|
|
absolutePath: canonicalPath
|
|
}
|
|
})
|
|
expect(resolveAuthorizedPathMock).not.toHaveBeenCalledWith(canonicalPath, expect.anything())
|
|
expect(resolveAuthorizedPathMock).not.toHaveBeenCalledWith(linkPath, expect.anything())
|
|
})
|
|
|
|
it('does not grant hard-linked local temp artifacts', async () => {
|
|
const dir = await mkdtemp(join(tmpdir(), 'orca-terminal-artifact-'))
|
|
tempDirs.push(dir)
|
|
const originalPath = join(dir, 'outside.json')
|
|
const artifactPath = join(dir, 'result.json')
|
|
await writeFile(originalPath, '{"secret":true}')
|
|
await link(originalPath, artifactPath)
|
|
const { commands } = createRuntimeFileCommands({ path: '/repo' })
|
|
|
|
const result = await resolveTerminalArtifactPath(commands, artifactPath)
|
|
|
|
expect(result).toMatchObject({
|
|
relativePath: null,
|
|
absolutePath: artifactPath,
|
|
exists: false
|
|
})
|
|
expect(result.openTarget).toBeUndefined()
|
|
})
|
|
|
|
it('does not grant arbitrary absolute local paths outside temp roots', async () => {
|
|
const { commands } = createRuntimeFileCommands({ path: '/repo' })
|
|
|
|
const result = await commands.resolveTerminalPath('id:wt-1', '/etc/passwd', null, 'client-a')
|
|
|
|
expect(result).toEqual({
|
|
worktree: 'wt-1',
|
|
relativePath: null,
|
|
absolutePath: '/etc/passwd',
|
|
exists: false,
|
|
isDirectory: false
|
|
})
|
|
expect(resolveAuthorizedPathMock).not.toHaveBeenCalled()
|
|
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',
|
|
'/tmp/link-result.json',
|
|
null,
|
|
'client-a'
|
|
)
|
|
|
|
expect(result).toEqual({
|
|
worktree: 'wt-1',
|
|
relativePath: null,
|
|
absolutePath: '/tmp/link-result.json',
|
|
exists: false,
|
|
isDirectory: false
|
|
})
|
|
expect(stat).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('translates WSL absolute in-worktree paths before checking containment', async () => {
|
|
Object.defineProperty(process, 'platform', { configurable: true, value: 'win32' })
|
|
const worktreePath = '\\\\wsl.localhost\\Ubuntu\\home\\me\\repo'
|
|
const expectedPath = '\\\\wsl.localhost\\Ubuntu\\home\\me\\repo\\src\\index.ts'
|
|
const { commands } = createRuntimeFileCommands({ path: worktreePath })
|
|
statAsFile()
|
|
|
|
const result = await commands.resolveTerminalPath('id:wt-1', '/home/me/repo/src/index.ts')
|
|
|
|
expect(result).toMatchObject({
|
|
relativePath: 'src/index.ts',
|
|
absolutePath: expectedPath,
|
|
exists: true,
|
|
openTarget: {
|
|
kind: 'worktree-file',
|
|
provider: 'local',
|
|
relativePath: 'src/index.ts',
|
|
absolutePath: expectedPath
|
|
}
|
|
})
|
|
expect(resolveAuthorizedPathMock).toHaveBeenCalledWith(expectedPath, expect.anything())
|
|
})
|
|
|
|
it('does not translate UNC-style paths as WSL POSIX paths', async () => {
|
|
Object.defineProperty(process, 'platform', { configurable: true, value: 'win32' })
|
|
const worktreePath = '\\\\wsl.localhost\\Ubuntu\\home\\me\\repo'
|
|
const { commands } = createRuntimeFileCommands({ path: worktreePath })
|
|
|
|
const result = await commands.resolveTerminalPath(
|
|
'id:wt-1',
|
|
'//remote-host/tmp/result.json',
|
|
null,
|
|
'client-a'
|
|
)
|
|
|
|
expect(result).toEqual({
|
|
worktree: 'wt-1',
|
|
relativePath: null,
|
|
absolutePath: '//remote-host/tmp/result.json',
|
|
exists: false,
|
|
isDirectory: false
|
|
})
|
|
expect(resolveAuthorizedPathMock).not.toHaveBeenCalled()
|
|
expect(statMock).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('preserves UNC-style paths for local Windows terminal links', async () => {
|
|
Object.defineProperty(process, 'platform', { configurable: true, value: 'win32' })
|
|
const worktreePath = '\\\\server\\share\\repo'
|
|
const expectedPath = '//server/share/repo/src/index.ts'
|
|
const { commands } = createRuntimeFileCommands({ path: worktreePath })
|
|
statAsFile()
|
|
|
|
const result = await commands.resolveTerminalPath('id:wt-1', expectedPath)
|
|
|
|
expect(result).toMatchObject({
|
|
relativePath: 'src/index.ts',
|
|
absolutePath: expectedPath,
|
|
exists: true,
|
|
openTarget: {
|
|
kind: 'worktree-file',
|
|
provider: 'local',
|
|
relativePath: 'src/index.ts',
|
|
absolutePath: expectedPath
|
|
}
|
|
})
|
|
})
|
|
|
|
it('preserves local Windows UNC file URI authority even when OSC7 reported the host', async () => {
|
|
Object.defineProperty(process, 'platform', { configurable: true, value: 'win32' })
|
|
const worktreePath = '\\\\server\\share\\repo'
|
|
const expectedPath = '//server/share/repo/src/index.ts'
|
|
const resolveTerminalFileUriHostname = vi.fn(() => 'server')
|
|
const { commands } = createRuntimeFileCommands({
|
|
path: worktreePath,
|
|
resolveTerminalFileUriHostname
|
|
})
|
|
statAsFile()
|
|
|
|
const result = await commands.resolveTerminalPath(
|
|
'id:wt-1',
|
|
expectedPath,
|
|
null,
|
|
'client-a',
|
|
'term-1'
|
|
)
|
|
|
|
expect(resolveTerminalFileUriHostname).toHaveBeenCalledWith('term-1')
|
|
expect(result).toMatchObject({
|
|
relativePath: 'src/index.ts',
|
|
absolutePath: expectedPath,
|
|
exists: true,
|
|
openTarget: {
|
|
kind: 'worktree-file',
|
|
provider: 'local',
|
|
relativePath: 'src/index.ts',
|
|
absolutePath: expectedPath
|
|
}
|
|
})
|
|
})
|
|
|
|
it('preserves host-qualified local POSIX terminal links from unverified OSC7 host metadata', async () => {
|
|
Object.defineProperty(process, 'platform', { configurable: true, value: 'linux' })
|
|
const artifactPath = await tempFile('result.json', '{}')
|
|
const resolveTerminalFileUriHostname = vi.fn(() => 'laptop.local')
|
|
const { commands } = createRuntimeFileCommands({
|
|
path: '/repo',
|
|
resolveTerminalFileUriHostname
|
|
})
|
|
|
|
const result = await resolveTerminalArtifactPath(commands, `//laptop.local${artifactPath}`)
|
|
|
|
expect(resolveTerminalFileUriHostname).toHaveBeenCalledWith('term-1')
|
|
expect(result).toMatchObject({
|
|
relativePath: null,
|
|
absolutePath: `//laptop.local${artifactPath}`,
|
|
exists: false
|
|
})
|
|
expect(result.openTarget).toBeUndefined()
|
|
})
|
|
|
|
it('opens IPv4 loopback local POSIX terminal links as local paths', async () => {
|
|
Object.defineProperty(process, 'platform', { configurable: true, value: 'linux' })
|
|
const artifactPath = await tempFile('result.json', '{}')
|
|
const { commands } = createRuntimeFileCommands({ path: '/repo' })
|
|
const canonicalPath = await realpath(artifactPath)
|
|
|
|
const result = await resolveTerminalArtifactPath(commands, `//127.0.0.1${artifactPath}`)
|
|
|
|
expect(result).toMatchObject({
|
|
relativePath: null,
|
|
absolutePath: canonicalPath,
|
|
exists: true,
|
|
openTarget: {
|
|
kind: 'absolute-file',
|
|
provider: 'local',
|
|
absolutePath: canonicalPath
|
|
}
|
|
})
|
|
})
|
|
|
|
it('opens host-qualified remote POSIX terminal links when the source terminal verified the host', async () => {
|
|
const resolveTerminalFileUriHostname = vi.fn(() => 'remote-host')
|
|
const hasRecentTerminalOutputPath = vi.fn(() => true)
|
|
const { commands, store } = createRuntimeFileCommands({
|
|
path: '/home/me/repo',
|
|
resolveTerminalFileUriHostname,
|
|
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'
|
|
}
|
|
})
|
|
})
|
|
|
|
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',
|
|
'//remote-host/tmp/result.json',
|
|
null,
|
|
'client-a'
|
|
)
|
|
|
|
expect(stat).not.toHaveBeenCalled()
|
|
expect(result).toEqual({
|
|
worktree: 'wt-1',
|
|
relativePath: null,
|
|
absolutePath: '//remote-host/tmp/result.json',
|
|
exists: false,
|
|
isDirectory: false
|
|
})
|
|
})
|
|
|
|
it('translates WSL temp artifacts before granting the exact path', async () => {
|
|
Object.defineProperty(process, 'platform', { configurable: true, value: 'win32' })
|
|
const worktreePath = '\\\\wsl.localhost\\Ubuntu\\home\\me\\repo'
|
|
const artifactPath = '\\\\wsl.localhost\\Ubuntu\\tmp\\result.json'
|
|
const { commands } = createRuntimeFileCommands({ path: worktreePath })
|
|
resolveAuthorizedPathMock.mockImplementation(async (p: string) => p)
|
|
statMock.mockResolvedValue({ isDirectory: () => false, size: 2, dev: 1, ino: 2, mtimeMs: 3 })
|
|
openMock.mockResolvedValue({
|
|
stat: vi.fn(async () => ({
|
|
isDirectory: () => false,
|
|
size: 2,
|
|
dev: 1,
|
|
ino: 2,
|
|
mtimeMs: 3
|
|
})),
|
|
close: vi.fn(async () => undefined)
|
|
})
|
|
|
|
const result = await resolveTerminalArtifactPath(commands, '/tmp/result.json')
|
|
|
|
expect(result).toMatchObject({
|
|
relativePath: null,
|
|
absolutePath: artifactPath,
|
|
exists: true,
|
|
openTarget: {
|
|
kind: 'absolute-file',
|
|
provider: 'local',
|
|
absolutePath: artifactPath
|
|
}
|
|
})
|
|
expect(resolveAuthorizedPathMock).not.toHaveBeenCalledWith(artifactPath, expect.anything())
|
|
})
|
|
|
|
it('reads an absolute terminal artifact only for the client that received the grant', async () => {
|
|
const artifactPath = await tempFile('result.json', '{"ok":true}')
|
|
const { commands } = createRuntimeFileCommands({ path: '/repo' })
|
|
resolveAuthorizedPathMock.mockImplementation(async (p: string) => p)
|
|
statMock.mockResolvedValue({ isDirectory: () => false, size: 11, dev: 1, ino: 2, mtimeMs: 3 })
|
|
|
|
const result = await resolveTerminalArtifactPath(commands, artifactPath)
|
|
const target = absoluteFileTarget(result)
|
|
|
|
await expect(
|
|
commands.readTerminalArtifactFile(
|
|
'id:wt-1',
|
|
target.grantId,
|
|
target.absolutePath,
|
|
'client-a'
|
|
)
|
|
).resolves.toMatchObject({
|
|
relativePath: target.absolutePath,
|
|
content: '{"ok":true}',
|
|
truncated: false
|
|
})
|
|
await expect(
|
|
commands.readTerminalArtifactFile(
|
|
'id:wt-1',
|
|
target.grantId,
|
|
target.absolutePath,
|
|
'client-b'
|
|
)
|
|
).rejects.toThrow('terminal_file_grant_mismatch')
|
|
})
|
|
|
|
it('revokes absolute terminal artifact grants when the owning client disconnects', async () => {
|
|
const artifactPath = await tempFile('result.json', '{}')
|
|
const { commands } = createRuntimeFileCommands({ path: '/repo' })
|
|
resolveAuthorizedPathMock.mockImplementation(async (p: string) => p)
|
|
statMock.mockResolvedValue({ isDirectory: () => false, size: 2, dev: 1, ino: 2, mtimeMs: 3 })
|
|
|
|
const result = await resolveTerminalArtifactPath(commands, artifactPath)
|
|
const target = absoluteFileTarget(result)
|
|
|
|
commands.revokeTerminalFileGrantsForClient('client-a')
|
|
|
|
await expect(
|
|
commands.readTerminalArtifactFile(
|
|
'id:wt-1',
|
|
target.grantId,
|
|
target.absolutePath,
|
|
'client-a'
|
|
)
|
|
).rejects.toThrow('terminal_file_grant_expired')
|
|
})
|
|
|
|
it('expires absolute terminal artifact grants without waiting for another tap', async () => {
|
|
const artifactPath = await tempFile('result.json', '{}')
|
|
const { commands } = createRuntimeFileCommands({ path: '/repo' })
|
|
resolveAuthorizedPathMock.mockImplementation(async (p: string) => p)
|
|
statMock.mockResolvedValue({ isDirectory: () => false, size: 2, dev: 1, ino: 2, mtimeMs: 3 })
|
|
|
|
const result = await resolveTerminalArtifactPath(commands, artifactPath)
|
|
const target = absoluteFileTarget(result)
|
|
|
|
await vi.advanceTimersByTimeAsync(10 * 60 * 1000 + 1)
|
|
|
|
await expect(
|
|
commands.readTerminalArtifactFile(
|
|
'id:wt-1',
|
|
target.grantId,
|
|
target.absolutePath,
|
|
'client-a'
|
|
)
|
|
).rejects.toThrow('terminal_file_grant_expired')
|
|
expect(statMock).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('rejects stale absolute terminal artifact writes before changing the file', async () => {
|
|
const artifactPath = await tempFile('result.json', '{"ok":true}')
|
|
const { commands } = createRuntimeFileCommands({ path: '/repo' })
|
|
resolveAuthorizedPathMock.mockImplementation(async (p: string) => p)
|
|
|
|
const result = await resolveTerminalArtifactPath(commands, artifactPath)
|
|
const target = absoluteFileTarget(result)
|
|
await rm(artifactPath)
|
|
await writeFile(artifactPath, '{"ok":"ext"}')
|
|
|
|
await expect(
|
|
commands.writeTerminalArtifactFile(
|
|
'id:wt-1',
|
|
target.grantId,
|
|
target.absolutePath,
|
|
'{"ok":false}',
|
|
'client-a'
|
|
)
|
|
).rejects.toThrow('terminal_file_grant_stale')
|
|
await expect(readFile(artifactPath, 'utf8')).resolves.toBe('{"ok":"ext"}')
|
|
})
|
|
|
|
it('keeps the original terminal artifact when atomic commit fails', async () => {
|
|
const artifactPath = await tempFile('result.json', '{"ok":true}')
|
|
const { commands } = createRuntimeFileCommands({ path: '/repo' })
|
|
resolveAuthorizedPathMock.mockImplementation(async (p: string) => p)
|
|
renameMock.mockRejectedValueOnce(new Error('ENOSPC'))
|
|
|
|
const result = await resolveTerminalArtifactPath(commands, artifactPath)
|
|
const target = absoluteFileTarget(result)
|
|
|
|
await expect(
|
|
commands.writeTerminalArtifactFile(
|
|
'id:wt-1',
|
|
target.grantId,
|
|
target.absolutePath,
|
|
'{"ok":false}',
|
|
'client-a'
|
|
)
|
|
).rejects.toThrow('ENOSPC')
|
|
await expect(readFile(artifactPath, 'utf8')).resolves.toBe('{"ok":true}')
|
|
})
|
|
|
|
it('rejects hard-linked terminal artifact writes after a grant is created', async () => {
|
|
const artifactPath = await tempFile('result.json', '{"ok":true}')
|
|
const hardLinkPath = join(artifactPath, '..', 'linked-result.json')
|
|
const { commands } = createRuntimeFileCommands({ path: '/repo' })
|
|
resolveAuthorizedPathMock.mockImplementation(async (p: string) => p)
|
|
|
|
const result = await resolveTerminalArtifactPath(commands, artifactPath)
|
|
const target = absoluteFileTarget(result)
|
|
await link(artifactPath, hardLinkPath)
|
|
|
|
await expect(
|
|
commands.writeTerminalArtifactFile(
|
|
'id:wt-1',
|
|
target.grantId,
|
|
target.absolutePath,
|
|
'{"ok":false}',
|
|
'client-a'
|
|
)
|
|
).rejects.toThrow('terminal_file_grant_stale')
|
|
await expect(readFile(artifactPath, 'utf8')).resolves.toBe('{"ok":true}')
|
|
})
|
|
|
|
it('rejects stale absolute terminal artifact reads before returning changed content', async () => {
|
|
const artifactPath = await tempFile('result.json', '{"ok":true}')
|
|
const { commands } = createRuntimeFileCommands({ path: '/repo' })
|
|
resolveAuthorizedPathMock.mockImplementation(async (p: string) => p)
|
|
|
|
const result = await resolveTerminalArtifactPath(commands, artifactPath)
|
|
const target = absoluteFileTarget(result)
|
|
|
|
await rm(artifactPath)
|
|
await writeFile(artifactPath, '{"ok":false}')
|
|
|
|
await expect(
|
|
commands.readTerminalArtifactFile(
|
|
'id:wt-1',
|
|
target.grantId,
|
|
target.absolutePath,
|
|
'client-a'
|
|
)
|
|
).rejects.toThrow('terminal_file_grant_stale')
|
|
})
|
|
|
|
it('rejects retargeted symlink terminal artifact reads before returning outside content', async () => {
|
|
const artifactPath = await tempFile('result.json', '{"ok":true}')
|
|
const outsidePath = join(artifactPath, '..', 'outside.json')
|
|
await writeFile(outsidePath, '{"secret":true}')
|
|
const { commands } = createRuntimeFileCommands({ path: '/repo' })
|
|
resolveAuthorizedPathMock.mockImplementation(async (p: string) => p)
|
|
|
|
const result = await resolveTerminalArtifactPath(commands, artifactPath)
|
|
const target = absoluteFileTarget(result)
|
|
await rm(artifactPath)
|
|
await symlink(outsidePath, artifactPath)
|
|
|
|
await expect(
|
|
commands.readTerminalArtifactFile(
|
|
'id:wt-1',
|
|
target.grantId,
|
|
target.absolutePath,
|
|
'client-a'
|
|
)
|
|
).rejects.toThrow('terminal_file_grant_stale')
|
|
})
|
|
|
|
it('rejects retargeted symlink terminal artifact writes before changing outside content', async () => {
|
|
const artifactPath = await tempFile('result.json', '{"ok":true}')
|
|
const outsidePath = join(artifactPath, '..', 'outside.json')
|
|
await writeFile(outsidePath, '{"secret":true}')
|
|
const { commands } = createRuntimeFileCommands({ path: '/repo' })
|
|
resolveAuthorizedPathMock.mockImplementation(async (p: string) => p)
|
|
|
|
const result = await resolveTerminalArtifactPath(commands, artifactPath)
|
|
const target = absoluteFileTarget(result)
|
|
await rm(artifactPath)
|
|
await symlink(outsidePath, artifactPath)
|
|
|
|
await expect(
|
|
commands.writeTerminalArtifactFile(
|
|
'id:wt-1',
|
|
target.grantId,
|
|
target.absolutePath,
|
|
'{"ok":false}',
|
|
'client-a'
|
|
)
|
|
).rejects.toThrow('terminal_file_grant_stale')
|
|
await expect(readFile(outsidePath, 'utf8')).resolves.toBe('{"secret":true}')
|
|
})
|
|
|
|
it('does not renew stale terminal artifact grants', async () => {
|
|
const artifactPath = await tempFile('result.json', '{"ok":true}')
|
|
const { commands } = createRuntimeFileCommands({ path: '/repo' })
|
|
resolveAuthorizedPathMock.mockImplementation(async (p: string) => p)
|
|
|
|
const result = await resolveTerminalArtifactPath(commands, artifactPath)
|
|
const target = absoluteFileTarget(result)
|
|
await rm(artifactPath)
|
|
await writeFile(artifactPath, '{"ok":false}')
|
|
|
|
await expect(
|
|
commands.readTerminalArtifactFile(
|
|
'id:wt-1',
|
|
target.grantId,
|
|
target.absolutePath,
|
|
'client-a'
|
|
)
|
|
).rejects.toThrow('terminal_file_grant_stale')
|
|
|
|
await vi.advanceTimersByTimeAsync(10 * 60 * 1000 + 1)
|
|
|
|
await expect(
|
|
commands.readTerminalArtifactFile(
|
|
'id:wt-1',
|
|
target.grantId,
|
|
target.absolutePath,
|
|
'client-a'
|
|
)
|
|
).rejects.toThrow('terminal_file_grant_expired')
|
|
})
|
|
|
|
it('rejects stale absolute terminal artifact previews before returning changed content', async () => {
|
|
const artifactPath = await tempFile('result.png', 'fake-png')
|
|
const { commands } = createRuntimeFileCommands({ path: '/repo' })
|
|
resolveAuthorizedPathMock.mockImplementation(async (p: string) => p)
|
|
|
|
const result = await resolveTerminalArtifactPath(commands, artifactPath)
|
|
const target = absoluteFileTarget(result)
|
|
|
|
await rm(artifactPath)
|
|
await writeFile(artifactPath, 'changed!')
|
|
|
|
await expect(
|
|
commands.readTerminalArtifactPreview(
|
|
'id:wt-1',
|
|
target.grantId,
|
|
target.absolutePath,
|
|
'client-a'
|
|
)
|
|
).rejects.toThrow('terminal_file_grant_stale')
|
|
})
|
|
|
|
it('rejects binary-extension terminal artifacts from the editable text path', async () => {
|
|
const artifactPath = await tempFile('report.pdf', '%PDF text-looking bytes')
|
|
const { commands } = createRuntimeFileCommands({ path: '/repo' })
|
|
resolveAuthorizedPathMock.mockImplementation(async (p: string) => p)
|
|
statMock.mockResolvedValue({
|
|
isDirectory: () => false,
|
|
size: 23,
|
|
dev: 1,
|
|
ino: 2,
|
|
mtimeMs: 3
|
|
})
|
|
|
|
const result = await resolveTerminalArtifactPath(commands, artifactPath)
|
|
const target = absoluteFileTarget(result)
|
|
|
|
await expect(
|
|
commands.readTerminalArtifactFile(
|
|
'id:wt-1',
|
|
target.grantId,
|
|
target.absolutePath,
|
|
'client-a'
|
|
)
|
|
).rejects.toThrow('binary_file')
|
|
await expect(
|
|
commands.writeTerminalArtifactFile(
|
|
'id:wt-1',
|
|
target.grantId,
|
|
target.absolutePath,
|
|
'not a pdf',
|
|
'client-a'
|
|
)
|
|
).rejects.toThrow('binary_file')
|
|
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('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 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()
|
|
})
|
|
|
|
it('reports a nonexistent in-worktree path as not existing', async () => {
|
|
const { commands } = createRuntimeFileCommands({ path: '/repo' })
|
|
resolveAuthorizedPathMock.mockImplementation(async (p: string) => p)
|
|
statMock.mockRejectedValue(Object.assign(new Error('ENOENT'), { code: 'ENOENT' }))
|
|
|
|
const result = await commands.resolveTerminalPath('id:wt-1', 'src/missing.ts')
|
|
|
|
expect(result).toMatchObject({ relativePath: 'src/missing.ts', exists: false })
|
|
})
|
|
|
|
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()
|
|
})
|
|
|
|
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'
|
|
)
|
|
})
|
|
})
|
|
})
|