mirror of
https://github.com/stablyai/orca.git
synced 2026-09-23 00:02:29 +00:00
278 lines
8.1 KiB
TypeScript
278 lines
8.1 KiB
TypeScript
import { describe, expect, it } from 'vitest'
|
|
import {
|
|
NATIVE_FILE_DROP_MAX_PATH_BYTES,
|
|
NATIVE_FILE_DROP_MAX_PATHS,
|
|
NATIVE_FILE_DROP_TARGET,
|
|
ORCA_INTERNAL_FILE_DRAG_TYPE,
|
|
createNativeFileDropPayload,
|
|
createRejectedNativeFileDropPayload,
|
|
hasNativeFileDragTypes,
|
|
isNativeFileDropPayload,
|
|
validateNativeFileDropPaths,
|
|
resolveNativeFileDropPath
|
|
} from './native-file-drop'
|
|
|
|
describe('hasNativeFileDragTypes', () => {
|
|
it('accepts native OS file drags', () => {
|
|
expect(hasNativeFileDragTypes(['Files'])).toBe(true)
|
|
})
|
|
|
|
it('rejects internal Orca file moves and URL/text drags', () => {
|
|
expect(hasNativeFileDragTypes(['Files', ORCA_INTERNAL_FILE_DRAG_TYPE])).toBe(false)
|
|
expect(hasNativeFileDragTypes(['text/uri-list'])).toBe(false)
|
|
expect(hasNativeFileDragTypes(['text/plain'])).toBe(false)
|
|
})
|
|
})
|
|
|
|
describe('resolveNativeFileDropPath', () => {
|
|
it('routes drops on the left sidebar to the add-project surface', () => {
|
|
expect(
|
|
resolveNativeFileDropPath([{ nativeFileDropTarget: NATIVE_FILE_DROP_TARGET.projectSidebar }])
|
|
).toEqual({ target: NATIVE_FILE_DROP_TARGET.projectSidebar })
|
|
})
|
|
|
|
it('preserves terminal tab and pane routing for native file drops', () => {
|
|
expect(
|
|
resolveNativeFileDropPath([
|
|
{
|
|
terminalPaneLeafId: 'leaf-1'
|
|
},
|
|
{
|
|
nativeFileDropTarget: NATIVE_FILE_DROP_TARGET.terminal,
|
|
terminalTabId: 'tab-1'
|
|
}
|
|
])
|
|
).toEqual({
|
|
target: NATIVE_FILE_DROP_TARGET.terminal,
|
|
tabId: 'tab-1',
|
|
paneLeafId: 'leaf-1'
|
|
})
|
|
})
|
|
|
|
it('uses the nearest file-explorer destination and fails closed without one', () => {
|
|
expect(
|
|
resolveNativeFileDropPath([
|
|
{ nativeFileDropDir: '/repo/src' },
|
|
{
|
|
nativeFileDropTarget: NATIVE_FILE_DROP_TARGET.fileExplorer,
|
|
nativeFileDropDir: '/repo'
|
|
}
|
|
])
|
|
).toEqual({
|
|
target: NATIVE_FILE_DROP_TARGET.fileExplorer,
|
|
destinationDir: '/repo/src'
|
|
})
|
|
|
|
expect(
|
|
resolveNativeFileDropPath([{ nativeFileDropTarget: NATIVE_FILE_DROP_TARGET.fileExplorer }])
|
|
).toEqual({ target: 'rejected' })
|
|
})
|
|
})
|
|
|
|
describe('validateNativeFileDropPaths', () => {
|
|
it('rejects native drops by file count before path byte accounting is needed', () => {
|
|
const paths = Array.from({ length: NATIVE_FILE_DROP_MAX_PATHS + 1 }, (_value, index) =>
|
|
['/tmp/file-', String(index)].join('')
|
|
)
|
|
|
|
expect(validateNativeFileDropPaths(paths)).toEqual({
|
|
byteLength: 0,
|
|
pathCount: NATIVE_FILE_DROP_MAX_PATHS + 1,
|
|
reason: 'too-many-paths',
|
|
status: 'rejected'
|
|
})
|
|
})
|
|
|
|
it('rejects native drops whose path list is too large without exposing paths', () => {
|
|
const validation = validateNativeFileDropPaths(['C:\\Users\\alice\\secret-token.txt'], {
|
|
maxPathBytes: 4
|
|
})
|
|
|
|
expect(validation).toEqual({
|
|
byteLength: 5,
|
|
pathCount: 1,
|
|
reason: 'paths-too-large',
|
|
status: 'rejected'
|
|
})
|
|
if (validation.status === 'rejected') {
|
|
const payload = createRejectedNativeFileDropPayload(validation)
|
|
expect(JSON.stringify(payload)).not.toContain('secret')
|
|
expect(JSON.stringify(payload)).not.toContain('alice')
|
|
}
|
|
})
|
|
|
|
it('accepts path payloads within the configured limits', () => {
|
|
expect(validateNativeFileDropPaths(['/tmp/a', '/tmp/b'])).toEqual({
|
|
byteLength: 12,
|
|
pathCount: 2,
|
|
status: 'accepted'
|
|
})
|
|
})
|
|
|
|
it('rejects multibyte native path lists with bounded byte accounting', () => {
|
|
expect(validateNativeFileDropPaths(['😀'.repeat(3)], { maxPathBytes: 5 })).toEqual({
|
|
byteLength: 8,
|
|
pathCount: 1,
|
|
reason: 'paths-too-large',
|
|
status: 'rejected'
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('createNativeFileDropPayload', () => {
|
|
it('preserves terminal tab and pane routing in accepted payloads', () => {
|
|
expect(
|
|
createNativeFileDropPayload(
|
|
{ target: NATIVE_FILE_DROP_TARGET.terminal, tabId: 'tab-1', paneLeafId: 'leaf-1' },
|
|
['/tmp/a']
|
|
)
|
|
).toEqual({
|
|
paneLeafId: 'leaf-1',
|
|
paths: ['/tmp/a'],
|
|
tabId: 'tab-1',
|
|
target: NATIVE_FILE_DROP_TARGET.terminal
|
|
})
|
|
})
|
|
|
|
it('preserves file explorer destination routing in accepted payloads', () => {
|
|
expect(
|
|
createNativeFileDropPayload(
|
|
{ target: NATIVE_FILE_DROP_TARGET.fileExplorer, destinationDir: '/repo/src' },
|
|
['/tmp/a']
|
|
)
|
|
).toEqual({
|
|
destinationDir: '/repo/src',
|
|
paths: ['/tmp/a'],
|
|
target: NATIVE_FILE_DROP_TARGET.fileExplorer
|
|
})
|
|
})
|
|
|
|
it('falls back to editor for unmarked drops and fails closed for rejected targets', () => {
|
|
expect(createNativeFileDropPayload(null, ['/tmp/a'])).toEqual({
|
|
paths: ['/tmp/a'],
|
|
target: NATIVE_FILE_DROP_TARGET.editor
|
|
})
|
|
expect(createNativeFileDropPayload({ target: 'rejected' }, ['/tmp/a'])).toBeNull()
|
|
})
|
|
|
|
it('returns metadata-only rejected payloads for oversized path lists', () => {
|
|
const payload = createNativeFileDropPayload(null, [
|
|
'C:\\Users\\alice\\',
|
|
'a'.repeat(NATIVE_FILE_DROP_MAX_PATH_BYTES)
|
|
])
|
|
|
|
expect(payload).toEqual({
|
|
byteLength: NATIVE_FILE_DROP_MAX_PATH_BYTES + 1,
|
|
pathCount: 2,
|
|
reason: 'paths-too-large',
|
|
target: 'rejected'
|
|
})
|
|
expect(JSON.stringify(payload)).not.toContain('alice')
|
|
})
|
|
})
|
|
|
|
describe('isNativeFileDropPayload', () => {
|
|
it('accepts bounded native file-drop payload shapes', () => {
|
|
expect(
|
|
isNativeFileDropPayload({
|
|
paths: ['/tmp/a'],
|
|
target: NATIVE_FILE_DROP_TARGET.editor
|
|
})
|
|
).toBe(true)
|
|
expect(
|
|
isNativeFileDropPayload({
|
|
destinationDir: '/repo/src',
|
|
paths: ['/tmp/a'],
|
|
target: NATIVE_FILE_DROP_TARGET.fileExplorer
|
|
})
|
|
).toBe(true)
|
|
expect(
|
|
isNativeFileDropPayload({
|
|
paneLeafId: 'leaf-1',
|
|
paths: ['/tmp/a'],
|
|
tabId: 'tab-1',
|
|
target: NATIVE_FILE_DROP_TARGET.terminal
|
|
})
|
|
).toBe(true)
|
|
expect(
|
|
isNativeFileDropPayload({
|
|
byteLength: 0,
|
|
pathCount: NATIVE_FILE_DROP_MAX_PATHS + 1,
|
|
reason: 'too-many-paths',
|
|
target: 'rejected'
|
|
})
|
|
).toBe(true)
|
|
})
|
|
|
|
it('rejects malformed or unbounded native file-drop payloads', () => {
|
|
expect(isNativeFileDropPayload(null)).toBe(false)
|
|
expect(isNativeFileDropPayload({ paths: ['/tmp/a'], target: 'browser' })).toBe(false)
|
|
expect(
|
|
isNativeFileDropPayload({
|
|
paths: ['/tmp/a'],
|
|
target: NATIVE_FILE_DROP_TARGET.fileExplorer
|
|
})
|
|
).toBe(false)
|
|
expect(
|
|
isNativeFileDropPayload({
|
|
paths: ['/tmp/a'],
|
|
tabId: 42,
|
|
target: NATIVE_FILE_DROP_TARGET.terminal
|
|
})
|
|
).toBe(false)
|
|
expect(
|
|
isNativeFileDropPayload({
|
|
paths: Array.from({ length: NATIVE_FILE_DROP_MAX_PATHS + 1 }, () => '/tmp/a'),
|
|
target: NATIVE_FILE_DROP_TARGET.editor
|
|
})
|
|
).toBe(false)
|
|
expect(
|
|
isNativeFileDropPayload({
|
|
paths: ['a'.repeat(NATIVE_FILE_DROP_MAX_PATH_BYTES + 1)],
|
|
target: NATIVE_FILE_DROP_TARGET.editor
|
|
})
|
|
).toBe(false)
|
|
expect(
|
|
isNativeFileDropPayload({
|
|
byteLength: 0,
|
|
pathCount: 1,
|
|
reason: 'contains-secret-path',
|
|
target: 'rejected'
|
|
})
|
|
).toBe(false)
|
|
})
|
|
|
|
it('enforces native file-drop count and byte limits at their boundaries', () => {
|
|
expect(
|
|
isNativeFileDropPayload({
|
|
paths: Array.from(
|
|
{ length: NATIVE_FILE_DROP_MAX_PATHS },
|
|
(_value, index) => `/tmp/${index}`
|
|
),
|
|
target: NATIVE_FILE_DROP_TARGET.editor
|
|
})
|
|
).toBe(true)
|
|
expect(
|
|
isNativeFileDropPayload({
|
|
paths: Array.from(
|
|
{ length: NATIVE_FILE_DROP_MAX_PATHS + 1 },
|
|
(_value, index) => `/tmp/${index}`
|
|
),
|
|
target: NATIVE_FILE_DROP_TARGET.editor
|
|
})
|
|
).toBe(false)
|
|
expect(
|
|
isNativeFileDropPayload({
|
|
paths: ['a'.repeat(NATIVE_FILE_DROP_MAX_PATH_BYTES)],
|
|
target: NATIVE_FILE_DROP_TARGET.editor
|
|
})
|
|
).toBe(true)
|
|
expect(
|
|
isNativeFileDropPayload({
|
|
paths: ['a'.repeat(NATIVE_FILE_DROP_MAX_PATH_BYTES + 1)],
|
|
target: NATIVE_FILE_DROP_TARGET.editor
|
|
})
|
|
).toBe(false)
|
|
})
|
|
})
|