mirror of
https://github.com/EKKOLearnAI/hermes-web-ui.git
synced 2026-07-10 02:30:15 +00:00
08b7813882
* fix: safely show workspace symlink folders * docs: add chat-chain fragment for workspace picker
79 lines
3.7 KiB
TypeScript
79 lines
3.7 KiB
TypeScript
import { describe, expect, it } from 'vitest'
|
|
import { join, resolve } from 'path'
|
|
import { tmpdir } from 'os'
|
|
import { mkdir, mkdtemp, rm, symlink } from 'fs/promises'
|
|
import { normalizePlatformPath, validatePath } from '../../packages/server/src/services/hermes/file-provider'
|
|
import { isNearestExistingRealPathWithin, isPathWithin, isRealPathWithin, relativePathFromBase } from '../../packages/server/src/services/hermes/hermes-path'
|
|
|
|
describe('file provider platform path normalization', () => {
|
|
it('converts MSYS drive paths to Windows absolute paths on Windows', () => {
|
|
expect(normalizePlatformPath('/c/Users/Administrator/Desktop/screenshot.png', 'win32'))
|
|
.toBe('C:\\Users\\Administrator\\Desktop\\screenshot.png')
|
|
expect(normalizePlatformPath('/d/tmp/report.txt', 'win32'))
|
|
.toBe('D:\\tmp\\report.txt')
|
|
})
|
|
|
|
it('leaves MSYS-style paths unchanged on non-Windows platforms', () => {
|
|
expect(normalizePlatformPath('/c/Users/Administrator/Desktop/screenshot.png', 'darwin'))
|
|
.toBe('/c/Users/Administrator/Desktop/screenshot.png')
|
|
expect(normalizePlatformPath('/c/Users/Administrator/Desktop/screenshot.png', 'linux'))
|
|
.toBe('/c/Users/Administrator/Desktop/screenshot.png')
|
|
})
|
|
|
|
it('leaves normal Windows paths unchanged', () => {
|
|
expect(normalizePlatformPath('C:\\Users\\Administrator\\Desktop\\screenshot.png', 'win32'))
|
|
.toBe('C:\\Users\\Administrator\\Desktop\\screenshot.png')
|
|
})
|
|
|
|
it('allows literal double dots inside safe absolute path segments', () => {
|
|
const filePath = join(tmpdir(), 'foo..bar.txt')
|
|
|
|
expect(validatePath(filePath)).toBe(resolve(filePath))
|
|
})
|
|
|
|
it('rejects parent-directory traversal segments', () => {
|
|
const filePath = `${join(tmpdir(), 'safe')}/../evil.txt`
|
|
|
|
expect(() => validatePath(filePath)).toThrow('Invalid file path')
|
|
})
|
|
})
|
|
|
|
describe('Hermes path containment helpers', () => {
|
|
it('does not treat sibling paths with the same prefix as inside the base', () => {
|
|
expect(isPathWithin('/tmp/hermes-profile2/state.db', '/tmp/hermes-profile')).toBe(false)
|
|
expect(isPathWithin('/tmp/hermes-profile/state.db', '/tmp/hermes-profile')).toBe(true)
|
|
})
|
|
|
|
it('returns normalized relative paths only for children', () => {
|
|
expect(relativePathFromBase('/tmp/hermes-profile/logs/run.txt', '/tmp/hermes-profile'))
|
|
.toBe('logs/run.txt')
|
|
expect(relativePathFromBase('/tmp/hermes-profile2/logs/run.txt', '/tmp/hermes-profile'))
|
|
.toBeNull()
|
|
})
|
|
|
|
it('realpath-vets symlinked ancestors before allowing nested workspace operations', async () => {
|
|
const workspaceBase = await mkdtemp(join(tmpdir(), 'hermes-workspace-base-'))
|
|
const outsideRoot = await mkdtemp(join(tmpdir(), 'hermes-workspace-outside-'))
|
|
|
|
try {
|
|
const safeTarget = join(workspaceBase, 'projects')
|
|
const safeLink = join(workspaceBase, 'linked-projects')
|
|
const outsideTarget = join(outsideRoot, 'external-projects')
|
|
const outsideLink = join(workspaceBase, 'linked-external')
|
|
|
|
await mkdir(safeTarget, { recursive: true })
|
|
await mkdir(outsideTarget, { recursive: true })
|
|
await symlink(safeTarget, safeLink)
|
|
await symlink(outsideTarget, outsideLink)
|
|
|
|
await expect(isRealPathWithin(safeLink, workspaceBase)).resolves.toBe(true)
|
|
await expect(isRealPathWithin(outsideLink, workspaceBase)).resolves.toBe(false)
|
|
await expect(isNearestExistingRealPathWithin(join(safeLink, 'child'), workspaceBase)).resolves.toBe(true)
|
|
await expect(isNearestExistingRealPathWithin(join(outsideLink, 'child'), workspaceBase)).resolves.toBe(false)
|
|
} finally {
|
|
await rm(workspaceBase, { recursive: true, force: true })
|
|
await rm(outsideRoot, { recursive: true, force: true })
|
|
}
|
|
})
|
|
})
|