mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-18 13:23:33 +08:00
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
76 lines
2.6 KiB
TypeScript
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')
|
|
})
|
|
})
|