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
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { guessLanguageFromPath } from '../../src/utils/fileLanguage'
|
||||
import { guessLanguageFromPath, isEditableRemoteTextFile } from '../../src/utils/fileLanguage'
|
||||
|
||||
describe('guessLanguageFromPath', () => {
|
||||
it('detects common file extensions for the SFTP editor', () => {
|
||||
@@ -7,5 +7,44 @@ describe('guessLanguageFromPath', () => {
|
||||
expect(guessLanguageFromPath('Dockerfile')).toBe('dockerfile')
|
||||
expect(guessLanguageFromPath('app.py')).toBe('python')
|
||||
expect(guessLanguageFromPath('readme.md')).toBe('markdown')
|
||||
expect(guessLanguageFromPath('index.html.bak')).toBe('html')
|
||||
})
|
||||
})
|
||||
|
||||
describe('isEditableRemoteTextFile (denylist)', () => {
|
||||
it('allows text, code, config, backups, and unknown extensions', () => {
|
||||
expect(isEditableRemoteTextFile('notes.txt')).toBe(true)
|
||||
expect(isEditableRemoteTextFile('app.yaml')).toBe(true)
|
||||
expect(isEditableRemoteTextFile('index.html')).toBe(true)
|
||||
expect(isEditableRemoteTextFile('app.css')).toBe(true)
|
||||
expect(isEditableRemoteTextFile('main.go')).toBe(true)
|
||||
expect(isEditableRemoteTextFile('weird.xyz')).toBe(true)
|
||||
expect(isEditableRemoteTextFile('Makefile')).toBe(true)
|
||||
expect(isEditableRemoteTextFile('.env')).toBe(true)
|
||||
expect(isEditableRemoteTextFile('nginx.conf.bak')).toBe(true)
|
||||
expect(isEditableRemoteTextFile('app.py.old')).toBe(true)
|
||||
expect(isEditableRemoteTextFile('mystery.bak')).toBe(true)
|
||||
expect(isEditableRemoteTextFile('noext')).toBe(true)
|
||||
})
|
||||
|
||||
it('rejects archives, binaries, media, and office files', () => {
|
||||
expect(isEditableRemoteTextFile('pkg.tar')).toBe(false)
|
||||
expect(isEditableRemoteTextFile('pkg.tar.gz')).toBe(false)
|
||||
expect(isEditableRemoteTextFile('data.gz')).toBe(false)
|
||||
expect(isEditableRemoteTextFile('archive.zip')).toBe(false)
|
||||
expect(isEditableRemoteTextFile('setup.exe')).toBe(false)
|
||||
expect(isEditableRemoteTextFile('lib.so')).toBe(false)
|
||||
expect(isEditableRemoteTextFile('image.png')).toBe(false)
|
||||
expect(isEditableRemoteTextFile('photo.jpg')).toBe(false)
|
||||
expect(isEditableRemoteTextFile('video.mp4')).toBe(false)
|
||||
expect(isEditableRemoteTextFile('font.woff2')).toBe(false)
|
||||
expect(isEditableRemoteTextFile('blob.bin')).toBe(false)
|
||||
expect(isEditableRemoteTextFile('doc.pdf')).toBe(false)
|
||||
expect(isEditableRemoteTextFile('sheet.xlsx')).toBe(false)
|
||||
expect(isEditableRemoteTextFile('archive.zip.bak')).toBe(false)
|
||||
})
|
||||
|
||||
it('rejects directories', () => {
|
||||
expect(isEditableRemoteTextFile('/home/u/src', 'directory')).toBe(false)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,21 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import {
|
||||
isWithinRemoteTextEditLimit,
|
||||
MAX_REMOTE_TEXT_EDIT_BYTES,
|
||||
} from '../../src/utils/remoteTextLimits'
|
||||
|
||||
describe('remoteTextLimits', () => {
|
||||
it('caps at 2 MiB', () => {
|
||||
expect(MAX_REMOTE_TEXT_EDIT_BYTES).toBe(2 * 1024 * 1024)
|
||||
})
|
||||
|
||||
it('allows missing or zero size', () => {
|
||||
expect(isWithinRemoteTextEditLimit(undefined)).toBe(true)
|
||||
expect(isWithinRemoteTextEditLimit(0)).toBe(true)
|
||||
})
|
||||
|
||||
it('rejects over the soft cap', () => {
|
||||
expect(isWithinRemoteTextEditLimit(MAX_REMOTE_TEXT_EDIT_BYTES)).toBe(true)
|
||||
expect(isWithinRemoteTextEditLimit(MAX_REMOTE_TEXT_EDIT_BYTES + 1)).toBe(false)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,89 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import type { FitAddon } from '@xterm/addon-fit'
|
||||
import type { Terminal } from '@xterm/xterm'
|
||||
import {
|
||||
isTerminalDisposed,
|
||||
markTerminalDisposed,
|
||||
safeTerminalDispose,
|
||||
safeTerminalFit,
|
||||
safeTerminalWrite,
|
||||
} from '../../src/utils/safeTerminalFit'
|
||||
|
||||
describe('safeTerminalFit', () => {
|
||||
it('returns false without an addon', () => {
|
||||
expect(safeTerminalFit(null)).toBe(false)
|
||||
})
|
||||
|
||||
it('skips fit when terminal element is detached', () => {
|
||||
const fit = vi.fn()
|
||||
const term = {
|
||||
element: { isConnected: false, parentElement: { clientWidth: 100, clientHeight: 40 } },
|
||||
} as unknown as Terminal
|
||||
expect(safeTerminalFit({ fit } as unknown as FitAddon, term)).toBe(false)
|
||||
expect(fit).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('skips fit when host has zero size', () => {
|
||||
const fit = vi.fn()
|
||||
const term = {
|
||||
element: { isConnected: true, parentElement: { clientWidth: 0, clientHeight: 0 } },
|
||||
} as unknown as Terminal
|
||||
expect(safeTerminalFit({ fit } as unknown as FitAddon, term)).toBe(false)
|
||||
expect(fit).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('swallows renderer dimension races', () => {
|
||||
const fit = vi.fn(() => {
|
||||
throw new TypeError("undefined is not an object (evaluating 'this._renderer.value.dimensions')")
|
||||
})
|
||||
const term = {
|
||||
element: { isConnected: true, parentElement: { clientWidth: 80, clientHeight: 40 } },
|
||||
} as unknown as Terminal
|
||||
expect(safeTerminalFit({ fit } as unknown as FitAddon, term)).toBe(false)
|
||||
expect(fit).toHaveBeenCalledOnce()
|
||||
})
|
||||
|
||||
it('returns true on successful fit', () => {
|
||||
const fit = vi.fn()
|
||||
const term = {
|
||||
element: { isConnected: true, parentElement: { clientWidth: 80, clientHeight: 40 } },
|
||||
} as unknown as Terminal
|
||||
expect(safeTerminalFit({ fit } as unknown as FitAddon, term)).toBe(true)
|
||||
})
|
||||
|
||||
it('blocks write after mark disposed', () => {
|
||||
const write = vi.fn()
|
||||
const term = { write } as unknown as Terminal
|
||||
markTerminalDisposed(term)
|
||||
expect(isTerminalDisposed(term)).toBe(true)
|
||||
safeTerminalWrite(term, 'x')
|
||||
expect(write).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('marks disposed and removes element before dispose', () => {
|
||||
const raf: FrameRequestCallback[] = []
|
||||
const original = globalThis.requestAnimationFrame
|
||||
globalThis.requestAnimationFrame = ((cb: FrameRequestCallback) => {
|
||||
raf.push(cb)
|
||||
return raf.length
|
||||
}) as typeof requestAnimationFrame
|
||||
|
||||
const dispose = vi.fn()
|
||||
const remove = vi.fn()
|
||||
const term = {
|
||||
dispose,
|
||||
element: { remove },
|
||||
} as unknown as Terminal
|
||||
safeTerminalDispose(term)
|
||||
expect(isTerminalDisposed(term)).toBe(true)
|
||||
expect(remove).toHaveBeenCalledOnce()
|
||||
expect(dispose).not.toHaveBeenCalled()
|
||||
// Drain double-rAF queue used by safeTerminalDispose.
|
||||
while (raf.length) {
|
||||
const next = raf.shift()!
|
||||
next(0)
|
||||
}
|
||||
expect(dispose).toHaveBeenCalledOnce()
|
||||
globalThis.requestAnimationFrame = original
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,38 @@
|
||||
import { beforeAll, describe, expect, it } from 'vitest'
|
||||
import i18n from '../../src/i18n'
|
||||
import {
|
||||
filterSettingsTabsByQuery,
|
||||
settingsTabMatchesQuery,
|
||||
} from '../../src/utils/settingsSearch'
|
||||
|
||||
describe('settingsSearch multilingual', () => {
|
||||
beforeAll(async () => {
|
||||
await i18n.changeLanguage('zh-CN')
|
||||
})
|
||||
|
||||
it('finds general via Language / lang while UI is Chinese', () => {
|
||||
expect(settingsTabMatchesQuery('general', 'Language')).toBe(true)
|
||||
expect(settingsTabMatchesQuery('general', 'lang')).toBe(true)
|
||||
expect(settingsTabMatchesQuery('general', '语言')).toBe(true)
|
||||
expect(settingsTabMatchesQuery('general', '言語')).toBe(true)
|
||||
})
|
||||
|
||||
it('finds workspace via English aliases while UI is Chinese', () => {
|
||||
expect(settingsTabMatchesQuery('general', 'workspace')).toBe(true)
|
||||
expect(settingsTabMatchesQuery('general', '通用')).toBe(true)
|
||||
})
|
||||
|
||||
it('finds account via license / 账户 while UI is Chinese', () => {
|
||||
expect(settingsTabMatchesQuery('account', 'license')).toBe(true)
|
||||
expect(settingsTabMatchesQuery('account', '账户')).toBe(true)
|
||||
expect(settingsTabMatchesQuery('account', 'email')).toBe(true)
|
||||
})
|
||||
|
||||
it('filters tab list for language query', () => {
|
||||
const matched = filterSettingsTabsByQuery(
|
||||
['general', 'account', 'terminal', 'ai', 'shortcuts', 'ssh', 'security', 'about'],
|
||||
'lang'
|
||||
)
|
||||
expect(matched).toContain('general')
|
||||
})
|
||||
})
|
||||
@@ -55,7 +55,7 @@ describe('applyKeywordHighlights', () => {
|
||||
const osc = '\x1b]0;ssh root@165.154.155.48\x07'
|
||||
const out = applyKeywordHighlights(`${osc}Welcome to Ubuntu`, rules)
|
||||
expect(out).toContain(osc)
|
||||
expect(out).not.toMatch(/\x1b\]0;.*\x1b\[38/)
|
||||
expect(out).not.toMatch(/\x1b\]0;.*\x1b\[38/) // eslint-disable-line no-control-regex -- OSC must stay intact
|
||||
expect(out).toContain('Welcome to Ubuntu')
|
||||
})
|
||||
|
||||
@@ -102,7 +102,7 @@ describe('StreamingKeywordHighlight', () => {
|
||||
const done = stream.push('\x07Welcome', ipv4Rules)
|
||||
expect(done).toContain('\x1b]0;ssh root@1.2.3.4\x07')
|
||||
expect(done).toContain('Welcome')
|
||||
expect(done).not.toMatch(/\x1b\]0;.*\x1b\[38/)
|
||||
expect(done).not.toMatch(/\x1b\]0;.*\x1b\[38/) // eslint-disable-line no-control-regex -- OSC must stay intact
|
||||
})
|
||||
|
||||
it('holds incomplete CSI across chunks', () => {
|
||||
|
||||
Reference in new issue
Block a user