| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101 |
- import fs from 'node:fs'
- import os from 'node:os'
- import path from 'node:path'
- import pino from 'pino'
- import { unzipSync } from 'fflate'
- import { beforeAll, describe, expect, it } from 'vitest'
- import { createApp, type App } from '../src/app'
- import { config } from '../src/config'
- let app: App
- let sid: string
- let ws: string
- let kbDir: string
- beforeAll(async () => {
- app = createApp(pino({ level: 'silent' }), { providers: {} })
- const builtin = app.profiles.getBuiltin().id
- const key = app.apiKeys.create('fm', { allowedProfileIds: [builtin], defaultProfileId: builtin })
- const s = await app.sessions.create(key.apiKey.id, {})
- sid = s.id
- ws = s.workspaceDir
- fs.writeFileSync(path.join(ws, 'uploads', 'data.csv'), 'a,b\n1,2\n')
- kbDir = fs.mkdtempSync(path.join(os.tmpdir(), 'kb-'))
- fs.writeFileSync(path.join(kbDir, 'note.md'), '# 账单\n')
- fs.symlinkSync('/etc', path.join(kbDir, 'etc-link'))
- fs.mkdirSync(path.join(kbDir, '.ssh-like'))
- app.knowledge.create({ name: '账单', path: kbDir, description: '家庭账单', access: 'read', enabled: true })
- })
- const kbRoot = () => app.fileManager.roots().find(r => r.kind === 'knowledge')!.id
- describe('后台文件管理', () => {
- it('位置:会话工作区与资料库可写,整个服务目录只读;常用目录包含资料库与服务目录的常用子目录,不含会话', () => {
- const roots = app.fileManager.roots()
- expect(roots.map(r => [r.id.split(':')[0], r.writable])).toEqual([['workspaces', true], ['kb', true], ['server', false]])
- const sc = app.fileManager.shortcuts()
- expect(sc.some(s => s.rootId === 'workspaces')).toBe(false)
- expect(sc).toEqual(expect.arrayContaining([expect.objectContaining({ label: '服务数据', rootId: 'server', path: 'data' })]))
- expect(sc.some(s => s.label === '账单')).toBe(true)
- })
- it('浏览、编辑(带冲突检查)、上传、新建目录、移动、删除;会话的文件记录随之更新', async () => {
- expect(app.fileManager.list('workspaces', sid).entries.map(e => [e.name, e.type])).toEqual(expect.arrayContaining([['uploads', 'dir'], ['outputs', 'dir']]))
- const t = app.fileManager.readText('workspaces', `${sid}/uploads/data.csv`)
- expect(t.content).toBe('a,b\n1,2\n')
- const saved = app.fileManager.writeText('workspaces', `${sid}/uploads/data.csv`, 'a,b\n3,4\n', t.mtime)
- expect(fs.readFileSync(path.join(ws, 'uploads/data.csv'), 'utf8')).toBe('a,b\n3,4\n')
- // 打开后文件被修改:拒绝保存
- fs.writeFileSync(path.join(ws, 'uploads/data.csv'), 'changed')
- fs.utimesSync(path.join(ws, 'uploads/data.csv'), new Date(), new Date(Date.now() + 5000))
- expect(() => app.fileManager.writeText('workspaces', `${sid}/uploads/data.csv`, 'x', saved.mtime)).toThrow(/修改过/)
- app.fileManager.mkdir('workspaces', `${sid}/outputs/drafts`)
- app.fileManager.upload('workspaces', `${sid}/outputs/drafts`, 'report.md', Buffer.from('# 报告'))
- await new Promise(r => setTimeout(r, 50))
- expect(app.files.list(sid).map(f => f.path)).toContain('outputs/drafts/report.md')
- app.fileManager.move('workspaces', `${sid}/outputs/drafts/report.md`, `${sid}/outputs/final.md`)
- app.fileManager.delete('workspaces', `${sid}/outputs/drafts`)
- await new Promise(r => setTimeout(r, 50))
- const paths = app.files.list(sid).map(f => f.path)
- expect(paths).toContain('outputs/final.md')
- expect(paths).not.toContain('outputs/drafts/report.md')
- })
- it('保护:只读位置不能写;路径不能越出位置;外部符号链接不跟随;会话目录不能在这里删除', () => {
- expect(() => app.fileManager.writeText('server', 'x.md', 'x', null)).toThrow(/只读/)
- expect(() => app.fileManager.list('workspaces', '../')).toThrow(/路径不正确/)
- expect(() => app.fileManager.list('workspaces', `${sid}/../..`)).toThrow(/路径不正确/)
- const kb = kbRoot()
- expect(app.fileManager.list(kb, '').entries.find(e => e.name === 'etc-link')!.type).toBe('link')
- expect(() => app.fileManager.readText(kb, 'etc-link/hostname')).toThrow(/位置之外/)
- expect(() => app.fileManager.delete('workspaces', sid)).toThrow(/会话页面/)
- expect(() => app.fileManager.delete('workspaces', '')).toThrow(/根目录/)
- })
- it('服务目录整体只读可见:登录凭证不显示,数据库文件看得到但不能读取或下载', () => {
- fs.mkdirSync(path.join(config.dataDir, 'home', '.codex'), { recursive: true })
- fs.writeFileSync(path.join(config.dataDir, 'home', '.codex', 'auth.json'), '{}')
- fs.writeFileSync(path.join(config.dataDir, 'home', '.codex', 'config.toml'), '')
- fs.mkdirSync(path.join(config.dataDir, 'home', '.claude'), { recursive: true })
- fs.writeFileSync(path.join(config.dataDir, 'home', '.claude', '.credentials.json'), '{}')
- expect(app.fileManager.list('server', '').entries.map(e => e.name)).toEqual(expect.arrayContaining(['data', 'home', 'workspaces']))
- expect(app.fileManager.list('server', 'home/.codex').entries.map(e => e.name)).toEqual(['config.toml'])
- expect(app.fileManager.list('server', 'home/.claude').entries.map(e => e.name)).not.toContain('.credentials.json')
- expect(() => app.fileManager.readText('server', 'home/.codex/auth.json')).toThrow(/不允许访问/)
- expect(app.fileManager.list('server', 'data').entries.map(e => e.name)).toContain('agent.db')
- expect(() => app.fileManager.openFile('server', 'data/agent.db')).toThrow(/数据库文件/)
- expect(() => app.fileManager.writeText('server', 'workspaces/x.md', 'x', null)).toThrow(/只读/)
- expect(() => app.fileManager.zipDir('server', 'data')).not.toThrow()
- // 包含服务数据目录的路径不能登记为资料库
- expect(() => app.knowledge.create({ name: '数据目录上层', path: config.dataDir, description: '', access: 'read', enabled: true })).toThrow(/全局禁止项/)
- })
- it('二进制文件不能在线编辑;目录可以打包下载', () => {
- fs.writeFileSync(path.join(ws, 'outputs', 'img.bin'), Buffer.from([0, 1, 2, 0, 5]))
- expect(() => app.fileManager.readText('workspaces', `${sid}/outputs/img.bin`)).toThrow(/二进制/)
- const z = app.fileManager.zipDir('workspaces', `${sid}/outputs`)
- expect(z.name).toBe('outputs.zip')
- expect(Object.keys(unzipSync(new Uint8Array(z.data))).sort()).toEqual(['final.md', 'img.bin'])
- })
- })
|