import fs from 'node:fs' import path from 'node:path' import { describe, expect, it } from 'vitest' import { config } from '../src/config' import { execFileSync } from 'node:child_process' import { convertDocument, recalcWorkbook, sandboxArgs } from '../src/services/office' // 真实调用 LibreOffice(约数秒),验证独立沙箱中的转换可用 describe('convertDocument', () => { const ws = path.join(config.dataDir, 'workspaces/ses_office') fs.mkdirSync(ws, { recursive: true }) fs.writeFileSync(path.join(ws, '中文 报告.txt'), '你好,LibreOffice') it('把工作区内的文件转换为 PDF(中文文件名)', async () => { const out = await convertDocument({ workspaceDir: ws, input: '中文 报告.txt', format: 'pdf' }) expect(out).toBe(path.join(fs.realpathSync(ws), 'outputs/中文 报告.pdf')) expect(fs.readFileSync(out).subarray(0, 4).toString()).toBe('%PDF') }, 120_000) it('拒绝工作区以外的输入和输出', async () => { await expect(convertDocument({ workspaceDir: ws, input: '/etc/hostname', format: 'pdf' })).rejects.toThrow(/工作区内/) await expect(convertDocument({ workspaceDir: ws, input: '中文 报告.txt', format: 'pdf', outDir: '/tmp' })).rejects.toThrow(/工作区内/) }) }) describe('转换沙箱隔离', () => { const ws = path.join(config.dataDir, 'workspaces/ses_iso') fs.mkdirSync(ws, { recursive: true }) const run = (cmd: string) => execFileSync('bwrap', [...sandboxArgs(fs.realpathSync(ws)), 'sh', '-c', cmd], { encoding: 'utf8' }).trim() it('看不到 home、/var、/srv、/run,也连不上宿主 socket', () => { expect(run('for d in /home /srv /run /mnt /root; do [ -e $d ] && echo $d; done; true')).toBe('') // /var 中只有字体缓存 expect(run('find /var -mindepth 1 -maxdepth 2 | sort')).toBe('/var/cache\n/var/cache/fontconfig') expect(run('[ -S /var/run/docker.sock ] || [ -S /var/snap/lxd/common/lxd/unix.socket ] && echo found || echo none')).toBe('none') }) it('/usr 与 /etc 中的 socket 被屏蔽', () => { // find 按目录项类型报告,需要 stat 实际 inode:被 /dev/null 覆盖后应为字符设备 expect(run('find /usr/share /usr/local /etc -type s 2>/dev/null | while read f; do LC_ALL=C stat -c %F "$f"; done | grep -c socket || true')).toBe('0') }) it('断网', () => { expect(run('cat /sys/class/net/*/operstate 2>/dev/null | grep -c up || true')).toBe('0') }) }) describe('recalcWorkbook', () => { const ws = path.join(config.dataDir, 'workspaces/ses_recalc') fs.mkdirSync(path.join(ws, '.agent'), { recursive: true }) const py = '/srv/agent-server/runtime/py' it('重算公式并报告错误单元格', async () => { // 用 openpyxl 写入公式(无缓存值),包含一个除零错误 execFileSync(path.join(py, 'bin/python'), ['-c', ` import openpyxl wb = openpyxl.Workbook(); s = wb.active; s.title = '数据' s['A1'] = 10; s['A2'] = 32; s['A3'] = '=SUM(A1:A2)'; s['B1'] = '=A1/0' wb.save('${ws}/book.xlsx')`]) const r = await recalcWorkbook({ workspaceDir: ws, input: 'book.xlsx', pythonRuntime: py }) expect(r.formulas).toBe(2) expect(r.errors).toEqual([{ sheet: '数据', cell: 'B1', error: '#DIV/0!' }]) const value = execFileSync(path.join(py, 'bin/python'), ['-c', `import openpyxl; print(openpyxl.load_workbook('${ws}/book.xlsx', data_only=True).active['A3'].value)`], { encoding: 'utf8' }) expect(value.trim()).toBe('42') }, 120_000) })