cc-haha/src/server/__tests__/filesystem.test.ts
程序员阿江(Relakkes) 0f7494006f Prevent misleading empty @-file menus when filesystem access fails
Desktop @-mentions were collapsing filesystem browse failures into an empty
state, which made /private/tmp aliases on macOS look like real empty
directories. The server now treats /private/tmp as the same temp root as
/tmp on Darwin, and the menu surfaces explicit access/load errors instead of
pretending the directory has no files.

Constraint: Desktop browse/file APIs must stay restricted to approved roots
Constraint: Native macOS pickers and realpath() may canonicalize /tmp to /private/tmp
Rejected: Expanding filesystem access to arbitrary system paths | too broad for this targeted fix
Rejected: Keeping silent empty-state fallback | hides permission and path bugs from users
Confidence: high
Scope-risk: narrow
Reversibility: clean
Directive: Keep filesystem error states visible in the composer; do not collapse API failures back into empty-directory messaging
Tested: bun test src/server/__tests__/filesystem.test.ts
Tested: cd desktop && bun run test -- FileSearchMenu.test.tsx
Tested: cd desktop && bun run lint
Not-tested: Manual desktop packaging or end-to-end Tauri app run after this patch
2026-04-20 23:07:15 +08:00

76 lines
2.6 KiB
TypeScript

import { afterEach, describe, expect, it } from 'bun:test'
import * as fs from 'fs'
import * as fsp from 'fs/promises'
import * as path from 'path'
import { handleFilesystemRoute } from '../api/filesystem.js'
const cleanupDirs = new Set<string>()
function makeUrl(route: string, params: Record<string, string>): URL {
const url = new URL(`http://localhost${route}`)
for (const [key, value] of Object.entries(params)) {
url.searchParams.set(key, value)
}
return url
}
afterEach(async () => {
for (const dir of cleanupDirs) {
await fsp.rm(dir, { recursive: true, force: true })
}
cleanupDirs.clear()
})
describe('filesystem API', () => {
it('allows browsing a directory under the user home directory', async () => {
const homeFixtureDir = await fsp.mkdtemp(path.join(process.env.HOME || path.sep, 'claude-filesystem-test-'))
cleanupDirs.add(homeFixtureDir)
await fsp.writeFile(path.join(homeFixtureDir, 'note.txt'), 'hello')
const res = await handleFilesystemRoute(
'/api/filesystem/browse',
makeUrl('/api/filesystem/browse', {
path: homeFixtureDir,
includeFiles: 'true',
}),
)
expect(res.status).toBe(200)
const body = await res.json() as { entries: Array<{ name: string }> }
expect(body.entries.some((entry) => entry.name === 'note.txt')).toBe(true)
})
it('accepts /private/tmp aliases on macOS for browsing and file serving', async () => {
if (process.platform !== 'darwin') return
const tmpFixtureDir = await fsp.mkdtemp('/tmp/claude-filesystem-test-')
cleanupDirs.add(tmpFixtureDir)
const canonicalTmpDir = fs.realpathSync(tmpFixtureDir)
const imagePath = path.join(canonicalTmpDir, 'preview.png')
await fsp.writeFile(
imagePath,
Buffer.from('89504e470d0a1a0a0000000d4948445200000001000000010802000000907753de0000000c49444154789c63606060000000040001f61738550000000049454e44ae426082', 'hex'),
)
const browseRes = await handleFilesystemRoute(
'/api/filesystem/browse',
makeUrl('/api/filesystem/browse', {
path: canonicalTmpDir,
includeFiles: 'true',
}),
)
expect(browseRes.status).toBe(200)
const browseBody = await browseRes.json() as { entries: Array<{ name: string }> }
expect(browseBody.entries.some((entry) => entry.name === 'preview.png')).toBe(true)
const fileRes = await handleFilesystemRoute(
'/api/filesystem/file',
makeUrl('/api/filesystem/file', {
path: imagePath,
}),
)
expect(fileRes.status).toBe(200)
expect(fileRes.headers.get('Content-Type')).toBe('image/png')
})
})