office.test.ts 3.4 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667
  1. import fs from 'node:fs'
  2. import path from 'node:path'
  3. import { describe, expect, it } from 'vitest'
  4. import { config } from '../src/config'
  5. import { execFileSync } from 'node:child_process'
  6. import { convertDocument, recalcWorkbook, sandboxArgs } from '../src/services/office'
  7. // 真实调用 LibreOffice(约数秒),验证独立沙箱中的转换可用
  8. describe('convertDocument', () => {
  9. const ws = path.join(config.dataDir, 'workspaces/ses_office')
  10. fs.mkdirSync(ws, { recursive: true })
  11. fs.writeFileSync(path.join(ws, '中文 报告.txt'), '你好,LibreOffice')
  12. it('把工作区内的文件转换为 PDF(中文文件名)', async () => {
  13. const out = await convertDocument({ workspaceDir: ws, input: '中文 报告.txt', format: 'pdf' })
  14. expect(out).toBe(path.join(fs.realpathSync(ws), 'outputs/中文 报告.pdf'))
  15. expect(fs.readFileSync(out).subarray(0, 4).toString()).toBe('%PDF')
  16. }, 120_000)
  17. it('拒绝工作区以外的输入和输出', async () => {
  18. await expect(convertDocument({ workspaceDir: ws, input: '/etc/hostname', format: 'pdf' })).rejects.toThrow(/工作区内/)
  19. await expect(convertDocument({ workspaceDir: ws, input: '中文 报告.txt', format: 'pdf', outDir: '/tmp' })).rejects.toThrow(/工作区内/)
  20. })
  21. })
  22. describe('转换沙箱隔离', () => {
  23. const ws = path.join(config.dataDir, 'workspaces/ses_iso')
  24. fs.mkdirSync(ws, { recursive: true })
  25. const run = (cmd: string) =>
  26. execFileSync('bwrap', [...sandboxArgs(fs.realpathSync(ws)), 'sh', '-c', cmd], { encoding: 'utf8' }).trim()
  27. it('看不到 home、/var、/srv、/run,也连不上宿主 socket', () => {
  28. expect(run('for d in /home /srv /run /mnt /root; do [ -e $d ] && echo $d; done; true')).toBe('')
  29. // /var 中只有字体缓存
  30. expect(run('find /var -mindepth 1 -maxdepth 2 | sort')).toBe('/var/cache\n/var/cache/fontconfig')
  31. expect(run('[ -S /var/run/docker.sock ] || [ -S /var/snap/lxd/common/lxd/unix.socket ] && echo found || echo none')).toBe('none')
  32. })
  33. it('/usr 与 /etc 中的 socket 被屏蔽', () => {
  34. // find 按目录项类型报告,需要 stat 实际 inode:被 /dev/null 覆盖后应为字符设备
  35. 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')
  36. })
  37. it('断网', () => {
  38. expect(run('cat /sys/class/net/*/operstate 2>/dev/null | grep -c up || true')).toBe('0')
  39. })
  40. })
  41. describe('recalcWorkbook', () => {
  42. const ws = path.join(config.dataDir, 'workspaces/ses_recalc')
  43. fs.mkdirSync(path.join(ws, '.agent'), { recursive: true })
  44. const py = '/srv/agent-server/runtime/py'
  45. it('重算公式并报告错误单元格', async () => {
  46. // 用 openpyxl 写入公式(无缓存值),包含一个除零错误
  47. execFileSync(path.join(py, 'bin/python'), ['-c', `
  48. import openpyxl
  49. wb = openpyxl.Workbook(); s = wb.active; s.title = '数据'
  50. s['A1'] = 10; s['A2'] = 32; s['A3'] = '=SUM(A1:A2)'; s['B1'] = '=A1/0'
  51. wb.save('${ws}/book.xlsx')`])
  52. const r = await recalcWorkbook({ workspaceDir: ws, input: 'book.xlsx', pythonRuntime: py })
  53. expect(r.formulas).toBe(2)
  54. expect(r.errors).toEqual([{ sheet: '数据', cell: 'B1', error: '#DIV/0!' }])
  55. 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' })
  56. expect(value.trim()).toBe('42')
  57. }, 120_000)
  58. })