build-exe-for-python-sdk-office.spec.ts 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137
  1. import { chmod, mkdir, mkdtemp, readFile, rm, stat, writeFile } from 'node:fs/promises'
  2. import { tmpdir } from 'node:os'
  3. import { dirname, join } from 'node:path'
  4. import { afterEach, expect, it } from 'vitest'
  5. import { copyOfficeSidecar } from './build-exe-for-python-sdk-office.ts'
  6. const temporaryDirectories: string[] = []
  7. afterEach(async () => {
  8. await Promise.all(temporaryDirectories.splice(0).map(directory => rm(directory, { recursive: true, force: true })))
  9. })
  10. async function fixture() {
  11. const root = await mkdtemp(join(tmpdir(), 'dsh-python-office-'))
  12. temporaryDirectories.push(root)
  13. const staging = join(root, 'staging')
  14. const destination = join(root, 'runtime-office')
  15. async function packageAt(name: string, fields: Record<string, unknown> = {}, parent = staging) {
  16. const directory = join(parent, 'node_modules', name)
  17. await mkdir(directory, { recursive: true })
  18. await writeFile(join(directory, 'package.json'), JSON.stringify({ name, version: '1.0.0', ...fields }))
  19. return directory
  20. }
  21. return { root, staging, destination, packageAt }
  22. }
  23. it('copies package-owned data, licenses, helper permissions, and nested dependencies', async () => {
  24. const { staging, destination, packageAt } = await fixture()
  25. const entry = await packageAt('@deepseek-ai/libreoffice-kit', { optionalDependencies: { '@deepseek-ai/libreoffice-kit-wasm': '0.0.1' }, dependencies: { decoder: '1' } })
  26. const engine = await packageAt('@deepseek-ai/libreoffice-kit-wasm')
  27. const decoder = await packageAt('decoder', { dependencies: { codec: '1' } }, entry)
  28. await packageAt('codec', {}, decoder)
  29. await packageAt('codec', { version: '2.0.0' })
  30. const assets = ['assets/soffice.data', 'prebuilds.json', 'licenses/LICENSE', 'bin/helper']
  31. for (const name of assets) {
  32. const path = join(engine, name)
  33. await mkdir(dirname(path), { recursive: true })
  34. await writeFile(path, name)
  35. }
  36. await chmod(join(engine, 'bin/helper'), 0o755)
  37. const packages = await copyOfficeSidecar(staging, destination, { platform: 'linux', arch: 'x64' })
  38. expect(packages).toHaveLength(4)
  39. expect(await readFile(join(destination, 'node_modules/@deepseek-ai/libreoffice-kit-wasm/assets/soffice.data'), 'utf8')).toBe('assets/soffice.data')
  40. expect(await readFile(join(destination, 'node_modules/@deepseek-ai/libreoffice-kit-wasm/licenses/LICENSE'), 'utf8')).toBe('licenses/LICENSE')
  41. expect(await readFile(join(destination, 'node_modules/@deepseek-ai/libreoffice-kit/node_modules/decoder/node_modules/codec/package.json'), 'utf8')).toContain('codec')
  42. await expect(stat(join(destination, 'node_modules/codec'))).rejects.toMatchObject({ code: 'ENOENT' })
  43. if (process.platform !== 'win32') expect((await stat(join(destination, 'node_modules/@deepseek-ai/libreoffice-kit-wasm/bin/helper'))).mode & 0o111).toBe(0o111)
  44. })
  45. it('copies installed target optionals and leaves other platforms and absent optionals out', async () => {
  46. const { staging, destination, packageAt } = await fixture()
  47. await packageAt('@deepseek-ai/libreoffice-kit', {
  48. optionalDependencies: { native: '1', foreign: '1', absent: '1' },
  49. })
  50. await packageAt('@deepseek-ai/libreoffice-kit-wasm')
  51. await packageAt('native', { os: ['linux'], cpu: ['x64'] })
  52. await packageAt('foreign', { os: ['darwin'], cpu: ['arm64'] })
  53. const packages = await copyOfficeSidecar(staging, destination, { platform: 'linux', arch: 'x64' })
  54. expect(packages.map(path => path.replaceAll('\\', '/'))).toEqual([
  55. 'node_modules/@deepseek-ai/libreoffice-kit',
  56. 'node_modules/@deepseek-ai/libreoffice-kit-wasm',
  57. 'node_modules/native',
  58. ])
  59. })
  60. it('rejects a missing required dependency before producing a sidecar', async () => {
  61. const { staging, destination, packageAt } = await fixture()
  62. await packageAt('@deepseek-ai/libreoffice-kit', { dependencies: { 'dsh-missing-office-fixture': '1' } })
  63. await expect(copyOfficeSidecar(staging, destination, { platform: 'linux', arch: 'x64' }))
  64. .rejects.toThrow('dsh-missing-office-fixture required by @deepseek-ai/libreoffice-kit is missing')
  65. await expect(stat(destination)).rejects.toMatchObject({ code: 'ENOENT' })
  66. })
  67. it('rejects an incomplete installed optional instead of omitting it', async () => {
  68. const { staging, destination, packageAt } = await fixture()
  69. await packageAt('@deepseek-ai/libreoffice-kit', { optionalDependencies: { broken: '1' } })
  70. await mkdir(join(staging, 'node_modules/broken'), { recursive: true })
  71. await expect(copyOfficeSidecar(staging, destination, { platform: 'linux', arch: 'x64' }))
  72. .rejects.toMatchObject({ code: 'ENOENT' })
  73. })
  74. it('rejects an ancestor dependency outside the deployed closure', async () => {
  75. const { root, staging, destination, packageAt } = await fixture()
  76. await packageAt('@deepseek-ai/libreoffice-kit', { dependencies: { 'ancestor-office-fixture': '1' } })
  77. await packageAt('ancestor-office-fixture', {}, root)
  78. await expect(copyOfficeSidecar(staging, destination, { platform: 'linux', arch: 'x64' }))
  79. .rejects.toThrow('outside the deployed closure')
  80. })
  81. it('rejects an optional WASM engine resolved outside the deployed closure', async () => {
  82. const { root, staging, destination, packageAt } = await fixture()
  83. await packageAt('@deepseek-ai/libreoffice-kit', { optionalDependencies: { '@deepseek-ai/libreoffice-kit-wasm': '0.0.1' } })
  84. const ambient = await packageAt('@deepseek-ai/libreoffice-kit-wasm', {}, root)
  85. await expect(copyOfficeSidecar(staging, destination, { platform: 'linux', arch: 'x64' }))
  86. .rejects.toThrow(`Python Office dependency is outside the deployed closure: ${ambient}`)
  87. await expect(stat(destination)).rejects.toMatchObject({ code: 'ENOENT' })
  88. })
  89. it.each([
  90. ['darwin', 'arm64'], ['darwin', 'x64'], ['win32', 'arm64'], ['win32', 'x64'], ['linux', 'x64'],
  91. ])('copies only the %s/%s engine even when other engines are staged', async (platform, arch) => {
  92. const { staging, destination, packageAt } = await fixture()
  93. const targets = ['darwin-arm64', 'darwin-x64', 'win32-arm64', 'win32-x64', 'wasm']
  94. const names = targets.map(target => `@deepseek-ai/libreoffice-kit-${target}`)
  95. await packageAt('@deepseek-ai/libreoffice-kit', { optionalDependencies: Object.fromEntries(names.map(name => [name, '0.0.1'])) })
  96. for (const name of names) await packageAt(name)
  97. const expected = `@deepseek-ai/libreoffice-kit-${platform === 'linux' ? 'wasm' : `${platform}-${arch}`}`
  98. const packages = await copyOfficeSidecar(staging, destination, { platform, arch })
  99. expect(packages.map(path => path.replaceAll('\\', '/'))).toEqual([
  100. 'node_modules/@deepseek-ai/libreoffice-kit', `node_modules/${expected}`,
  101. ])
  102. })
  103. it.each(['darwin', 'win32'])('%s requires its native package even when WASM is staged', async (platform) => {
  104. const { staging, destination, packageAt } = await fixture()
  105. await packageAt('@deepseek-ai/libreoffice-kit', { optionalDependencies: { '@deepseek-ai/libreoffice-kit-wasm': '0.0.1' } })
  106. await packageAt('@deepseek-ai/libreoffice-kit-wasm')
  107. await expect(copyOfficeSidecar(staging, destination, { platform, arch: 'arm64' }))
  108. .rejects.toThrow(`Python Office engine @deepseek-ai/libreoffice-kit-${platform}-arm64 required for ${platform}/arm64 is missing.`)
  109. await expect(stat(destination)).rejects.toMatchObject({ code: 'ENOENT' })
  110. })
  111. it('requires a staged Linux WASM engine even when an ancestor has one', async () => {
  112. const { root, staging, destination, packageAt } = await fixture()
  113. await packageAt('@deepseek-ai/libreoffice-kit')
  114. await packageAt('@deepseek-ai/libreoffice-kit-wasm', {}, root)
  115. await expect(copyOfficeSidecar(staging, destination, { platform: 'linux', arch: 'x64' }))
  116. .rejects.toThrow('Python Office engine @deepseek-ai/libreoffice-kit-wasm required for linux/x64 is missing.')
  117. await expect(stat(destination)).rejects.toMatchObject({ code: 'ENOENT' })
  118. })