plugin-pnpm.spec.ts 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119
  1. import { createHash } from 'node:crypto'
  2. import { execFileSync } from 'node:child_process'
  3. import { createServer } from 'node:http'
  4. import { existsSync, mkdtempSync, readFileSync, realpathSync, rmSync, writeFileSync } from 'node:fs'
  5. import { tmpdir } from 'node:os'
  6. import { join } from 'node:path'
  7. import { pathToFileURL } from 'node:url'
  8. import { c } from 'tar'
  9. import { expect, it } from 'vitest'
  10. import { healIsolatedProfileModuleFallback, loadProfileDirectory } from '@deepseek-ai/dsh-app-boot'
  11. import { DesktopProjectManager, type DesktopProjectHooks } from '../src/project-manager.ts'
  12. import { resolveDesktopPaths } from '../src/paths.ts'
  13. import { runtimeFixture, writePackage } from './runtime-fixture.ts'
  14. it('installs a real pnpm graph and executes scripts approved by user configuration', async () => {
  15. // pnpm resolves its cwd natively; use the same spelling for Windows 8.3 temp paths.
  16. const root = realpathSync.native(mkdtempSync(join(tmpdir(), 'desktop-real-pnpm-')))
  17. const server = createServer()
  18. const archives = new Map<string, Buffer>()
  19. try {
  20. for (const name of ['fixture-plugin', 'fixture-script-dependency']) {
  21. const path = writePackage(join(root, 'packages'), name, name === 'fixture-plugin'
  22. ? { dependencies: { 'fixture-script-dependency': '1.0.0' }, peerDependencies: { '@deepseek-ai/cordis': '^1.0.0' }, dsh: { bundle: { patch: 'bundle.yml' } } }
  23. : { scripts: { install: 'node install.cjs' } }, 'export {identity} from "@deepseek-ai/cordis"')
  24. writeFileSync(join(path, 'bundle.yml'), '[]\n')
  25. writeFileSync(join(path, 'install.cjs'), 'require("node:fs").writeFileSync("built.json", JSON.stringify({node:process.execPath}))')
  26. const tarball = join(root, `${name}.tgz`)
  27. await c({ file: tarball, cwd: join(path, '..'), gzip: true }, [name])
  28. archives.set(name, readFileSync(tarball))
  29. }
  30. await new Promise<void>((resolve, reject) => { server.once('error', reject); server.listen(0, '127.0.0.1', resolve) })
  31. const address = server.address()
  32. if (address === null || typeof address === 'string') throw new Error('fixture registry has no TCP address')
  33. const origin = `http://127.0.0.1:${address.port}`
  34. server.on('request', (request, response) => {
  35. const name = request.url?.slice(1).replace(/\.tgz$/u, '') ?? ''
  36. const archive = archives.get(name)
  37. if (archive === undefined) { response.writeHead(404); response.end(); return }
  38. if (request.url?.endsWith('.tgz')) { response.end(archive); return }
  39. response.setHeader('content-type', 'application/json')
  40. response.end(JSON.stringify({ name, 'dist-tags': { latest: '1.0.0' }, versions: { '1.0.0': {
  41. name, version: '1.0.0', dist: { tarball: `${origin}/${name}.tgz`, integrity: `sha512-${createHash('sha512').update(archive).digest('base64')}` },
  42. ...(name === 'fixture-plugin' ? { dependencies: { 'fixture-script-dependency': '1.0.0' }, peerDependencies: { '@deepseek-ai/cordis': '^1.0.0' } } : { scripts: { install: 'node install.cjs' } }),
  43. } }, time: { '1.0.0': '2020-01-01T00:00:00.000Z' } }))
  44. })
  45. const dsh = join(root, 'dsh')
  46. runtimeFixture(dsh)
  47. const pnpm = join(import.meta.dirname, '../node_modules/pnpm/bin/pnpm.mjs')
  48. const manager = new DesktopProjectManager(resolveDesktopPaths(join(root, '.dsh')), { node: process.execPath, pnpm, dsh })
  49. const installAnchor = join(dsh, 'node_modules', '@deepseek-ai', 'dsh', 'package.json')
  50. const hooks: DesktopProjectHooks = {
  51. beforeChange: async () => {},
  52. afterChange: async () => {
  53. const profile = loadProfileDirectory('desktop test', manager.paths.profile, installAnchor)
  54. healIsolatedProfileModuleFallback({ installAnchor, profile })
  55. },
  56. }
  57. await manager.applyRelease()
  58. writeFileSync(join(manager.paths.profile, '.npmrc'), `registry=${origin}\n`)
  59. writeFileSync(join(manager.paths.profile, 'pnpm-workspace.yaml'), `packages:\n - .\nnodeLinker: hoisted\nautoInstallPeers: false\nstoreDir: ${JSON.stringify(join(root, 'store'))}\nallowBuilds:\n fixture-script-dependency: true\n`)
  60. await manager.mutate({ type: 'plugin-add', spec: 'fixture-plugin@1.0.0' }, hooks)
  61. expect(manager.listPlugins()).toEqual([{ name: 'fixture-plugin', version: '1.0.0', enabled: true }])
  62. const builtPath = join(manager.paths.profile, 'node_modules/fixture-script-dependency/built.json')
  63. if (!existsSync(builtPath)) {
  64. const config = (name: string): string => execFileSync(process.execPath, [pnpm, 'config', 'get', name], {
  65. cwd: manager.paths.profile, encoding: 'utf8',
  66. }).trim()
  67. throw new Error(`pnpm install script produced no output: ignoreScripts=${config('ignoreScripts')}, allowBuilds=${config('allowBuilds')}`)
  68. }
  69. const built = JSON.parse(readFileSync(builtPath, 'utf8')) as { node: string }
  70. expect(realpathSync(built.node)).toBe(realpathSync(process.execPath))
  71. const entry = join(dsh, 'identity.mjs')
  72. writeFileSync(entry, `import {identity} from '@deepseek-ai/cordis'; import {identity as plugin} from ${JSON.stringify(pathToFileURL(join(manager.paths.profile, 'node_modules/fixture-plugin/index.js')).href)}; console.log(identity === plugin)`)
  73. expect(execFileSync(process.execPath, [entry], { encoding: 'utf8' }).trim()).toBe('true')
  74. await manager.mutate({ type: 'plugin-update', name: 'fixture-plugin', version: '^1.0.0' }, hooks)
  75. expect(manager.listPlugins()[0]?.version).toBe('1.0.0')
  76. await manager.mutate({ type: 'plugin-remove', name: 'fixture-plugin' }, hooks)
  77. expect(manager.listPlugins()).toEqual([])
  78. } finally {
  79. server.closeAllConnections()
  80. if (server.listening) await new Promise<void>((resolve, reject) => {
  81. server.close((error) => {
  82. if (error !== undefined) reject(error)
  83. else resolve()
  84. })
  85. })
  86. rmSync(root, { recursive: true, force: true })
  87. }
  88. }, 30_000)
  89. it.each(['directory', 'file', 'tarball', 'plain'] as const)('installs a %s source through pnpm', async (source) => {
  90. const root = realpathSync.native(mkdtempSync(join(tmpdir(), 'desktop-local-pnpm-')))
  91. try {
  92. const dsh = join(root, 'dsh')
  93. runtimeFixture(dsh)
  94. const packageDir = writePackage(join(root, 'local packages'), 'local-plugin', {
  95. ...(source === 'plain' ? {} : { dsh: { bundle: { patch: 'missing.yml' } } }),
  96. peerDependencies: { '@deepseek-ai/cordis': '^999.0.0' },
  97. })
  98. const tarball = join(root, 'local-plugin.tgz')
  99. await c({ file: tarball, cwd: join(packageDir, '..'), gzip: true }, ['local-plugin'])
  100. const spec = source === 'directory' ? packageDir : source === 'file' ? `file:${packageDir}` : tarball
  101. const manager = new DesktopProjectManager(resolveDesktopPaths(join(root, '.dsh')), {
  102. node: process.execPath, pnpm: join(import.meta.dirname, '../node_modules/pnpm/bin/pnpm.mjs'), dsh,
  103. })
  104. const hooks: DesktopProjectHooks = { beforeChange: async () => {}, afterChange: async () => {} }
  105. await manager.applyRelease()
  106. await manager.mutate({ type: 'plugin-add', spec }, hooks)
  107. expect(manager.listPlugins()).toEqual([{ name: 'local-plugin', version: '1.0.0', enabled: source !== 'plain' }])
  108. expect(existsSync(join(manager.paths.profile, 'node_modules/local-plugin/missing.yml'))).toBe(false)
  109. await manager.disableAllPlugins()
  110. expect(manager.listPlugins()[0]?.enabled).toBe(false)
  111. await manager.mutate({ type: 'plugin-remove', name: 'local-plugin' }, hooks)
  112. expect(manager.listPlugins()).toEqual([])
  113. expect(existsSync(join(packageDir, 'package.json'))).toBe(true)
  114. } finally {
  115. rmSync(root, { recursive: true, force: true })
  116. }
  117. }, 30_000)