primary-runtime.spec.ts 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193
  1. import { mkdir, mkdtemp, readFile, rename, rm, symlink, writeFile } from 'node:fs/promises'
  2. import { tmpdir } from 'node:os'
  3. import { dirname, join } from 'node:path'
  4. import { pathToFileURL } from 'node:url'
  5. import { afterEach, expect, it } from 'vitest'
  6. import { Context } from '@deepseek-ai/cordis'
  7. import Loader from '@deepseek-ai/cordis-plugin-loader'
  8. import Include from '@deepseek-ai/cordis-plugin-include'
  9. import AgentRegistry from '@deepseek-ai/dsh-agent'
  10. import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
  11. import ToolRuntime from '@deepseek-ai/dsh-tools'
  12. import { ToolCallId } from '@deepseek-ai/dsh-llm'
  13. import { installPrimaryRuntime, readPrimaryRuntime, workspaceDependencyPaths, type PrimaryRuntimeManifest } from '../src/primary-runtime.ts'
  14. import * as workspaceDependencies from '../src/workspace-dependencies.ts'
  15. const roots: string[] = []
  16. afterEach(async () => { await Promise.all(roots.splice(0).map(root => rm(root, { recursive: true, force: true }))) })
  17. async function fixture() {
  18. const directory = await mkdtemp(join(tmpdir(), 'dsh-primary-runtime-'))
  19. roots.push(directory)
  20. const source = join(directory, 'resources')
  21. const root = join(directory, 'home', 'dsh-runtimes', 'dsh-primary-runtime')
  22. const manifest: PrimaryRuntimeManifest = {
  23. desktopVersion: '1.0.0', platform: process.platform === 'win32' ? 'win32' : 'darwin', arch: process.arch,
  24. components: { python: '3.12.14', node: '24.21.0', pnpm: '11.7.0', numpy: '2.3.5', pandas: '3.0.1' },
  25. pythonPackages: { 'python-docx': '1.2.0', 'python-pptx': '1.0.2', openpyxl: '3.1.5' },
  26. }
  27. const paths = workspaceDependencyPaths(source, manifest)
  28. for (const path of [paths.python, paths.node, paths.pnpm]) {
  29. await mkdir(dirname(path), { recursive: true })
  30. await writeFile(path, 'interpreter')
  31. }
  32. await mkdir(paths.pythonPackages, { recursive: true })
  33. await mkdir(paths.nodePackages, { recursive: true })
  34. await writeFile(join(source, 'runtime.json'), JSON.stringify(manifest))
  35. return { source, root, manifest, directory }
  36. }
  37. it.each(['win32', 'darwin'])('returns %s interpreter and package paths', (platform) => {
  38. const manifest: PrimaryRuntimeManifest = { desktopVersion: '1', platform, arch: 'x64', components: { python: '3.12.14', node: '24.21.0', pnpm: '11.7.0', numpy: '2.3.5', pandas: '3.0.1' } }
  39. const paths = workspaceDependencyPaths('/runtime', manifest)
  40. expect(paths.pythonDistributions).toEqual({})
  41. expect(paths.python).toBe(join('/runtime', 'dependencies', 'python', ...(platform === 'win32' ? ['python.exe'] : ['bin', 'python3'])))
  42. expect(paths.pythonPackages).toBe(join('/runtime', 'dependencies', 'python', ...(platform === 'win32' ? ['Lib'] : ['lib', 'python3.12']), 'site-packages'))
  43. })
  44. it.skipIf(process.platform === 'linux')('installs offline, reuses the same release, and leaves environment and user packages unchanged', async () => {
  45. const { source, root, manifest } = await fixture()
  46. const environment = { ...process.env }
  47. const installed = await installPrimaryRuntime(source, root)
  48. await writeFile(join(installed.pythonPackages, 'user-package.py'), 'user content')
  49. expect(await installPrimaryRuntime(source, root)).toEqual(installed)
  50. expect(installed.pythonDistributions).toEqual(manifest.pythonPackages)
  51. expect(await readFile(join(installed.pythonPackages, 'user-package.py'), 'utf8')).toBe('user content')
  52. expect(process.env).toEqual(environment)
  53. })
  54. it.skipIf(process.platform === 'linux')('replaces release components and recovers an interrupted directory swap', async () => {
  55. const { source, root, manifest } = await fixture()
  56. await installPrimaryRuntime(source, root)
  57. await rename(root, `${root}.previous`)
  58. await writeFile(join(source, 'runtime.json'), JSON.stringify({ ...manifest, desktopVersion: '2.0.0' }))
  59. await installPrimaryRuntime(source, root)
  60. expect((await readPrimaryRuntime(root)).desktopVersion).toBe('2.0.0')
  61. })
  62. it.skipIf(process.platform === 'linux')('replaces dependencies when the locked payload changes without a Desktop version change', async () => {
  63. const { source, root, manifest } = await fixture()
  64. const first = { ...manifest, payloadDigest: 'a'.repeat(64), pythonPackages: { 'python-docx': '1.1.2' } }
  65. await writeFile(join(source, 'runtime.json'), JSON.stringify(first))
  66. const installed = await installPrimaryRuntime(source, root)
  67. await writeFile(join(installed.pythonPackages, 'old-package.py'), 'old dependency')
  68. const next = { ...first, payloadDigest: 'b'.repeat(64), pythonPackages: { 'python-docx': '1.2.0' } }
  69. await writeFile(join(source, 'runtime.json'), JSON.stringify(next))
  70. await writeFile(join(workspaceDependencyPaths(source, next).pythonPackages, 'new-package.py'), 'new dependency')
  71. await installPrimaryRuntime(source, root)
  72. expect(await readPrimaryRuntime(root)).toEqual(next)
  73. expect(await readFile(join(installed.pythonPackages, 'new-package.py'), 'utf8')).toBe('new dependency')
  74. await expect(readFile(join(installed.pythonPackages, 'old-package.py'))).rejects.toMatchObject({ code: 'ENOENT' })
  75. })
  76. it.skipIf(process.platform === 'linux')('upgrades a release manifest without a payload digest', async () => {
  77. const { source, root, manifest } = await fixture()
  78. await installPrimaryRuntime(source, root)
  79. await writeFile(join(source, 'runtime.json'), JSON.stringify({ ...manifest, payloadDigest: 'a'.repeat(64), pythonPackages: { 'python-docx': '1.2.0' } }))
  80. await installPrimaryRuntime(source, root)
  81. expect((await readPrimaryRuntime(root)).payloadDigest).toBe('a'.repeat(64))
  82. })
  83. it.skipIf(process.platform === 'linux')('replaces changed payload bytes when only the digest changes', async () => {
  84. const { source, root, manifest } = await fixture()
  85. const first = { ...manifest, payloadDigest: 'a'.repeat(64), pythonPackages: { 'python-docx': '1.2.0' } }
  86. const sourceFile = join(workspaceDependencyPaths(source, first).pythonPackages, 'library.py')
  87. await writeFile(join(source, 'runtime.json'), JSON.stringify(first))
  88. await writeFile(sourceFile, 'first wheel bytes')
  89. const installed = await installPrimaryRuntime(source, root)
  90. await writeFile(join(source, 'runtime.json'), JSON.stringify({ ...first, payloadDigest: 'b'.repeat(64) }))
  91. await writeFile(sourceFile, 'repacked wheel bytes')
  92. await installPrimaryRuntime(source, root)
  93. expect(await readFile(join(installed.pythonPackages, 'library.py'), 'utf8')).toBe('repacked wheel bytes')
  94. expect((await readPrimaryRuntime(root)).pythonPackages).toEqual(first.pythonPackages)
  95. })
  96. it.skipIf(process.platform === 'linux')('keeps the installed release when the replacement payload is incomplete', async () => {
  97. const { source, root, manifest } = await fixture()
  98. await installPrimaryRuntime(source, root)
  99. await writeFile(join(source, 'runtime.json'), JSON.stringify({ ...manifest, desktopVersion: '2.0.0' }))
  100. await rm(workspaceDependencyPaths(source, manifest).python)
  101. await expect(installPrimaryRuntime(source, root)).rejects.toThrow()
  102. expect((await readPrimaryRuntime(root)).desktopVersion).toBe('1.0.0')
  103. })
  104. it.skipIf(process.platform === 'linux')('refuses linked installation directories without modifying their targets', async () => {
  105. const { source, root, directory } = await fixture()
  106. const outside = join(directory, 'outside')
  107. await mkdir(outside)
  108. await writeFile(join(outside, 'keep'), 'untouched')
  109. await mkdir(dirname(root), { recursive: true })
  110. await symlink(outside, root, process.platform === 'win32' ? 'junction' : 'dir')
  111. await expect(installPrimaryRuntime(source, root)).rejects.toThrow('filesystem link')
  112. expect(await readFile(join(outside, 'keep'), 'utf8')).toBe('untouched')
  113. })
  114. it('rejects malformed metadata and incompatible targets', async () => {
  115. const { source, root, manifest } = await fixture()
  116. await writeFile(join(source, 'runtime.json'), JSON.stringify({ ...manifest, arch: process.arch === 'x64' ? 'arm64' : 'x64' }))
  117. await expect(installPrimaryRuntime(source, root)).rejects.toThrow('incompatible')
  118. await writeFile(join(source, 'runtime.json'), JSON.stringify({ ...manifest, components: { ...manifest.components, python: '../escape' } }))
  119. await expect(readPrimaryRuntime(source)).rejects.toThrow('invalid metadata')
  120. })
  121. it.each([['numpy', 'numpy'], ['pandas', 'pandas'], ['Numpy', 'numpy'], ['PANDAS', 'pandas']] as const)('rejects conflicting %s component and distribution versions', async (distribution, name) => {
  122. const { source, manifest } = await fixture()
  123. await writeFile(join(source, 'runtime.json'), JSON.stringify({ ...manifest, pythonPackages: { [distribution]: '0.0.1' } }))
  124. await expect(readPrimaryRuntime(source)).rejects.toThrow(`conflicting ${name} distribution version`)
  125. const consistent = { ...manifest, pythonPackages: { [distribution]: manifest.components[name] } }
  126. await writeFile(join(source, 'runtime.json'), JSON.stringify(consistent))
  127. expect(await readPrimaryRuntime(source)).toEqual(consistent)
  128. })
  129. it.each([
  130. { payloadDigest: 'invalid' },
  131. { pythonPackages: ['python-docx'] },
  132. { pythonPackages: { 'python-docx': '../escape' } },
  133. { pythonPackages: { '../escape': '1.2.0' } },
  134. { pythonPackages: { numpy: '2.3.5', Numpy: '2.3.5' } },
  135. { pythonPackages: { Pillow: '12.3.0', pillow: '12.3.0' } },
  136. { pythonPackages: { typing_extensions: '4.16.0', 'typing.extensions': '4.16.0' } },
  137. ])('rejects invalid locked payload metadata: %j', async (invalid) => {
  138. const { source, manifest } = await fixture()
  139. await writeFile(join(source, 'runtime.json'), JSON.stringify({ ...manifest, ...invalid }))
  140. await expect(readPrimaryRuntime(source)).rejects.toThrow('invalid metadata')
  141. })
  142. it.skipIf(process.platform === 'linux')('loads the real tool through Cordis, exposes installed paths, and unregisters on disposal', async () => {
  143. const { source, root, manifest, directory } = await fixture()
  144. const ctx = new Context()
  145. try {
  146. ctx.baseUrl = pathToFileURL(directory).href + '/'
  147. await ctx.plugin(Loader)
  148. ctx.loader.builtins.include = Include
  149. const modules = new Map<string, unknown>([
  150. ['agents', AgentRegistry], ['systemPrompt', SystemPrompt], ['tools', ToolRuntime], ['dependencies', workspaceDependencies],
  151. ])
  152. ctx.loader.internal = {
  153. version: 'v2',
  154. async import(specifier: string) {
  155. if (!modules.has(specifier)) throw new Error(`unexpected plugin ${specifier}`)
  156. return modules.get(specifier)
  157. },
  158. } as unknown as NonNullable<typeof ctx.loader.internal>
  159. const config = join(directory, 'cordis.yml')
  160. await writeFile(config, `- name: agents\n- name: systemPrompt\n- name: tools\n- name: dependencies\n config:\n source: ${JSON.stringify(source)}\n root: ${JSON.stringify(root)}\n`)
  161. await ctx.loader.create({ name: 'cordis:include', config: { path: pathToFileURL(config).href } })
  162. await ctx.loader.await()
  163. for (const entry of ctx.loader.entries()) await entry.fiber?.await()
  164. const environment = { ...process.env }
  165. const request = { signal: new AbortController().signal, name: 'load_workspace_dependencies', arguments: {} }
  166. const results = await Promise.all(['first', 'second'].map(id => ctx.tools.execute({ ...request, callId: ToolCallId(id) })))
  167. for (const result of results) {
  168. expect(result.isError).toBe(false)
  169. expect(result.content).toEqual([{ type: 'text', text: JSON.stringify(workspaceDependencyPaths(root, manifest), undefined, 2) }])
  170. }
  171. expect(process.env).toEqual(environment)
  172. const entry = [...ctx.loader.entries()].find(entry => entry.options.name === 'dependencies')
  173. expect(entry).toBeDefined()
  174. await entry?.fiber?.dispose()
  175. expect(ctx.tools.schemas().some(tool => tool.name === 'load_workspace_dependencies')).toBe(false)
  176. } finally {
  177. await ctx.fiber.dispose()
  178. }
  179. })