feat: harden local shell, SFTP, and desktop session reliability
Finder-launched terminals now inherit a login PATH so tools like ollama resolve, destructive SFTP/editor actions require confirmation, and terminal search plus window dragging work again. SSH forwards and RDP sessions keep listeners and survive reconnect without FastPath crashes. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1 parent
188f3556ed
commit
08696896f9
223 files changed
+13493
-3423
No files matched your search
@@ -70,8 +70,8 @@ describe('SFTP basic transfer acceptance', () => {
|
||||
})
|
||||
|
||||
render(<SFTPView tabId="sftp-tab" sessionId={session.id} />)
|
||||
await screen.findByRole('button', { name: '上传' })
|
||||
await user.click(screen.getByRole('button', { name: '上传' }))
|
||||
await screen.findByRole('button', { name: '上传到远程' })
|
||||
await user.click(screen.getByRole('button', { name: '上传到远程' }))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockDesktopApi.sftpUpload).toHaveBeenCalledWith(
|
||||
@@ -86,9 +86,9 @@ describe('SFTP basic transfer acceptance', () => {
|
||||
const user = userEvent.setup()
|
||||
render(<SFTPView tabId="sftp-tab" sessionId={session.id} />)
|
||||
|
||||
await screen.findByRole('button', { name: '上传' })
|
||||
await screen.findByRole('button', { name: '上传到远程' })
|
||||
await user.click(await screen.findByText('remote.txt'))
|
||||
await user.click(screen.getByRole('button', { name: '下载' }))
|
||||
await user.click(screen.getByRole('button', { name: '下载到本地' }))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockDesktopApi.sftpDownload).toHaveBeenCalledWith(
|
||||
@@ -115,23 +115,31 @@ describe('SFTP basic transfer acceptance', () => {
|
||||
})
|
||||
|
||||
render(<SFTPView tabId="sftp-tab" sessionId={session.id} />)
|
||||
await screen.findByRole('button', { name: '上传' })
|
||||
await user.click(screen.getByRole('button', { name: '上传' }))
|
||||
await screen.findByRole('button', { name: '上传到远程' })
|
||||
await user.click(screen.getByRole('button', { name: '上传到远程' }))
|
||||
|
||||
expect(await screen.findByText(/权限不足|Permission denied/)).toBeVisible()
|
||||
expect(await screen.findByText(/权限|Permission denied|失败/)).toBeVisible()
|
||||
})
|
||||
|
||||
it('uploads by dragging a local row onto the remote panel', async () => {
|
||||
it('uploads by pointer-dragging a local row onto the remote panel', async () => {
|
||||
vi.mocked(mockDesktopApi.localReadDir).mockResolvedValue({
|
||||
success: true,
|
||||
files: [{ name: 'local.txt', path: '/Users/test/local.txt', type: 'file', size: 8, modified: 1_700_000_000 }],
|
||||
})
|
||||
render(<SFTPView tabId="sftp-tab" sessionId={session.id} />)
|
||||
await screen.findByRole('button', { name: '上传' })
|
||||
await screen.findByRole('button', { name: '上传到远程' })
|
||||
|
||||
const transfer = createDataTransfer()
|
||||
fireEvent.dragStart(await screen.findByText('local.txt'), { dataTransfer: transfer })
|
||||
fireEvent.drop(screen.getByLabelText('远程文件面板'), { dataTransfer: transfer })
|
||||
const localRow = await screen.findByText('local.txt')
|
||||
const remotePanel = screen.getByLabelText('远程文件面板')
|
||||
const originalElementFromPoint = document.elementFromPoint
|
||||
Object.defineProperty(document, 'elementFromPoint', {
|
||||
configurable: true,
|
||||
value: vi.fn(() => remotePanel),
|
||||
})
|
||||
|
||||
fireEvent.pointerDown(localRow, { button: 0, pointerId: 11, clientX: 10, clientY: 10 })
|
||||
fireEvent.pointerMove(localRow, { pointerId: 11, clientX: 30, clientY: 30 })
|
||||
fireEvent.pointerUp(localRow, { button: 0, pointerId: 11, clientX: 30, clientY: 30 })
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockDesktopApi.sftpUpload).toHaveBeenCalledWith(
|
||||
@@ -140,15 +148,28 @@ describe('SFTP basic transfer acceptance', () => {
|
||||
'/root/local.txt'
|
||||
)
|
||||
})
|
||||
|
||||
Object.defineProperty(document, 'elementFromPoint', {
|
||||
configurable: true,
|
||||
value: originalElementFromPoint,
|
||||
})
|
||||
})
|
||||
|
||||
it('downloads by dragging a remote row onto the local panel', async () => {
|
||||
it('downloads by pointer-dragging a remote row onto the local panel', async () => {
|
||||
render(<SFTPView tabId="sftp-tab" sessionId={session.id} />)
|
||||
await screen.findByRole('button', { name: '上传' })
|
||||
await screen.findByRole('button', { name: '上传到远程' })
|
||||
|
||||
const transfer = createDataTransfer()
|
||||
fireEvent.dragStart(await screen.findByText('remote.txt'), { dataTransfer: transfer })
|
||||
fireEvent.drop(screen.getByLabelText('本地文件面板'), { dataTransfer: transfer })
|
||||
const remoteRow = await screen.findByText('remote.txt')
|
||||
const localPanel = screen.getByLabelText('本地文件面板')
|
||||
const originalElementFromPoint = document.elementFromPoint
|
||||
Object.defineProperty(document, 'elementFromPoint', {
|
||||
configurable: true,
|
||||
value: vi.fn(() => localPanel),
|
||||
})
|
||||
|
||||
fireEvent.pointerDown(remoteRow, { button: 0, pointerId: 12, clientX: 50, clientY: 50 })
|
||||
fireEvent.pointerMove(remoteRow, { pointerId: 12, clientX: 70, clientY: 70 })
|
||||
fireEvent.pointerUp(remoteRow, { button: 0, pointerId: 12, clientX: 70, clientY: 70 })
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockDesktopApi.sftpDownload).toHaveBeenCalledWith(
|
||||
@@ -157,5 +178,132 @@ describe('SFTP basic transfer acceptance', () => {
|
||||
'/Users/test/remote.txt'
|
||||
)
|
||||
})
|
||||
|
||||
Object.defineProperty(document, 'elementFromPoint', {
|
||||
configurable: true,
|
||||
value: originalElementFromPoint,
|
||||
})
|
||||
})
|
||||
|
||||
it('still accepts OS file HTML5 drops onto the remote panel', async () => {
|
||||
render(<SFTPView tabId="sftp-tab" sessionId={session.id} />)
|
||||
await screen.findByRole('button', { name: '上传到远程' })
|
||||
|
||||
const file = new File(['hello'], 'from-os.txt', { type: 'text/plain' })
|
||||
Object.defineProperty(file, 'path', { value: '/Users/test/from-os.txt' })
|
||||
const transfer = createDataTransfer()
|
||||
Object.defineProperty(transfer, 'types', { value: ['Files'] })
|
||||
Object.defineProperty(transfer, 'files', {
|
||||
value: {
|
||||
length: 1,
|
||||
0: file,
|
||||
item: (index: number) => (index === 0 ? file : null),
|
||||
[Symbol.iterator]: function* () {
|
||||
yield file
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
fireEvent.dragEnter(screen.getByLabelText('远程文件面板'), { dataTransfer: transfer })
|
||||
fireEvent.drop(screen.getByLabelText('远程文件面板'), { dataTransfer: transfer })
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockDesktopApi.sftpUpload).toHaveBeenCalledWith(
|
||||
'sftp-native',
|
||||
'/Users/test/from-os.txt',
|
||||
'/root/from-os.txt'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
it('transfers both ways through pointer dragging when the WebView does not deliver HTML5 drop', async () => {
|
||||
vi.mocked(mockDesktopApi.localReadDir).mockResolvedValue({
|
||||
success: true,
|
||||
files: [{ name: 'local.txt', path: '/Users/test/local.txt', type: 'file', size: 8, modified: 1_700_000_000 }],
|
||||
})
|
||||
render(<SFTPView tabId="sftp-tab" sessionId={session.id} />)
|
||||
const localRow = await screen.findByText('local.txt')
|
||||
const remoteRow = await screen.findByText('remote.txt')
|
||||
const localPanel = screen.getByLabelText('本地文件面板')
|
||||
const remotePanel = screen.getByLabelText('远程文件面板')
|
||||
const hitTest = vi.fn<() => Element | null>(() => remotePanel)
|
||||
const originalElementFromPoint = document.elementFromPoint
|
||||
Object.defineProperty(document, 'elementFromPoint', {
|
||||
configurable: true,
|
||||
value: hitTest,
|
||||
})
|
||||
|
||||
fireEvent.pointerDown(localRow, { button: 0, pointerId: 1, clientX: 10, clientY: 10 })
|
||||
fireEvent.pointerMove(localRow, { pointerId: 1, clientX: 30, clientY: 30 })
|
||||
fireEvent.pointerUp(localRow, { button: 0, pointerId: 1, clientX: 30, clientY: 30 })
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockDesktopApi.sftpUpload).toHaveBeenCalledWith(
|
||||
'sftp-native',
|
||||
'/Users/test/local.txt',
|
||||
'/root/local.txt'
|
||||
)
|
||||
})
|
||||
|
||||
hitTest.mockReturnValue(localPanel)
|
||||
fireEvent.pointerDown(remoteRow, { button: 0, pointerId: 2, clientX: 50, clientY: 50 })
|
||||
fireEvent.pointerMove(remoteRow, { pointerId: 2, clientX: 70, clientY: 70 })
|
||||
fireEvent.pointerUp(remoteRow, { button: 0, pointerId: 2, clientX: 70, clientY: 70 })
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockDesktopApi.sftpDownload).toHaveBeenCalledWith(
|
||||
'sftp-native',
|
||||
'/root/remote.txt',
|
||||
'/Users/test/remote.txt'
|
||||
)
|
||||
})
|
||||
|
||||
Object.defineProperty(document, 'elementFromPoint', {
|
||||
configurable: true,
|
||||
value: originalElementFromPoint,
|
||||
})
|
||||
})
|
||||
|
||||
it('uses the directory under the pointer as the transfer destination', async () => {
|
||||
vi.mocked(mockDesktopApi.localReadDir).mockResolvedValue({
|
||||
success: true,
|
||||
files: [{ name: 'local.txt', path: '/Users/test/local.txt', type: 'file', size: 8, modified: 1_700_000_000 }],
|
||||
})
|
||||
vi.mocked(mockDesktopApi.sftpListDirectory).mockResolvedValue({
|
||||
success: true,
|
||||
files: [
|
||||
remoteFile,
|
||||
{
|
||||
filename: 'archive',
|
||||
longname: 'drwxr-xr-x 1 root root 0 Jul 30 12:00 archive',
|
||||
attrs: { size: 0, mtime: 1_700_000_000, mode: 0o040755 },
|
||||
},
|
||||
],
|
||||
})
|
||||
render(<SFTPView tabId="sftp-tab" sessionId={session.id} />)
|
||||
const localRow = await screen.findByText('local.txt')
|
||||
const archiveRow = await screen.findByText('archive')
|
||||
const originalElementFromPoint = document.elementFromPoint
|
||||
Object.defineProperty(document, 'elementFromPoint', {
|
||||
configurable: true,
|
||||
value: vi.fn(() => archiveRow),
|
||||
})
|
||||
|
||||
fireEvent.pointerDown(localRow, { button: 0, pointerId: 3, clientX: 10, clientY: 10 })
|
||||
fireEvent.pointerMove(localRow, { pointerId: 3, clientX: 40, clientY: 40 })
|
||||
fireEvent.pointerUp(localRow, { button: 0, pointerId: 3, clientX: 40, clientY: 40 })
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockDesktopApi.sftpUpload).toHaveBeenCalledWith(
|
||||
'sftp-native',
|
||||
'/Users/test/local.txt',
|
||||
'/root/archive/local.txt'
|
||||
)
|
||||
})
|
||||
|
||||
Object.defineProperty(document, 'elementFromPoint', {
|
||||
configurable: true,
|
||||
value: originalElementFromPoint,
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in new issue
Block a user