profile-packages.spec.ts 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778
  1. import { execFileSync } from 'node:child_process'
  2. import { mkdtempSync, rmSync, symlinkSync, unlinkSync, writeFileSync } from 'node:fs'
  3. import { tmpdir } from 'node:os'
  4. import { join } from 'node:path'
  5. import { pathToFileURL } from 'node:url'
  6. import { afterEach, expect, it } from 'vitest'
  7. import { createPluginProfile } from '../src/project-manager.ts'
  8. import { linkDesktopHostPackages, unlinkDesktopHostPackages, validateDesktopPluginGraph } from '../src/profile-packages.ts'
  9. import { runtimeFixture, writePackage } from './runtime-fixture.ts'
  10. const roots: string[] = []
  11. function fixture() {
  12. const root = mkdtempSync(join(tmpdir(), 'desktop-profile-'))
  13. roots.push(root)
  14. const dsh = join(root, 'dsh')
  15. const runtime = runtimeFixture(dsh)
  16. const profile = join(root, 'profile')
  17. createPluginProfile(profile)
  18. linkDesktopHostPackages(profile, dsh, runtime)
  19. return { root, dsh, runtime, profile }
  20. }
  21. afterEach(() => { for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true }) })
  22. it('loads one shared ESM instance from both host and external plugin while keeping ordinary dependencies private', () => {
  23. const { dsh, runtime, profile } = fixture()
  24. writePackage(join(dsh, 'node_modules'), 'ordinary', {}, 'export default "host"')
  25. writePackage(join(profile, 'node_modules'), 'ordinary', {}, 'export default "plugin"')
  26. const plugin = writePackage(join(profile, 'node_modules'), 'plugin', {
  27. peerDependencies: { '@deepseek-ai/cordis': '^1.0.0' }, dependencies: { ordinary: '1.0.0' },
  28. }, 'export { identity } from "@deepseek-ai/cordis"; export { default as ordinary } from "ordinary"')
  29. validateDesktopPluginGraph(profile, dsh, runtime, ['plugin'])
  30. const entry = join(dsh, 'check.mjs')
  31. writeFileSync(entry, `import {identity} from '@deepseek-ai/cordis'; import ordinary from 'ordinary'; import * as plugin from ${JSON.stringify(pathToFileURL(join(plugin, 'index.js')).href)}; console.log(JSON.stringify({same:identity===plugin.identity, host:ordinary, plugin:plugin.ordinary}))`)
  32. const output = execFileSync(process.execPath, [entry], { encoding: 'utf8', env: { ...process.env, NODE_OPTIONS: '', NODE_PATH: '' } })
  33. expect(JSON.parse(output)).toEqual({ same: true, host: 'host', plugin: 'plugin' })
  34. })
  35. it.each(['nested', 'alias'])('rejects a %s second copy of a host package', (placement) => {
  36. const { dsh, runtime, profile } = fixture()
  37. const plugin = writePackage(join(profile, 'node_modules'), 'plugin')
  38. if (placement === 'nested') writePackage(join(plugin, 'node_modules'), '@deepseek-ai/cordis')
  39. else writePackage(join(profile, 'node_modules'), 'alias', { name: '@deepseek-ai/cordis' })
  40. expect(() =>{ validateDesktopPluginGraph(profile, dsh, runtime, ['plugin']) }).toThrow(/duplicate or aliased/u)
  41. })
  42. it('rejects a host package declared as an ordinary dependency', () => {
  43. const { dsh, runtime, profile } = fixture()
  44. writePackage(join(profile, 'node_modules'), 'plugin', { dependencies: { '@deepseek-ai/cordis': '^1.0.0' } })
  45. expect(() =>{ validateDesktopPluginGraph(profile, dsh, runtime, ['plugin']) }).toThrow(/peer dependency/u)
  46. })
  47. it('rejects incompatible peers only when the plugin is enabled', () => {
  48. const { dsh, runtime, profile } = fixture()
  49. writePackage(join(profile, 'node_modules'), 'plugin', { peerDependencies: { '@deepseek-ai/cordis': '^2.0.0' } })
  50. expect(() =>{ validateDesktopPluginGraph(profile, dsh, runtime, ['plugin']) }).toThrow(/found 1.0.0/u)
  51. expect(() =>{ validateDesktopPluginGraph(profile, dsh, runtime, []) }).not.toThrow()
  52. })
  53. it('refuses to satisfy a plugin dependency from an ancestor CLI project', () => {
  54. const { root, dsh, runtime, profile } = fixture()
  55. writePackage(join(root, 'node_modules'), 'ambient')
  56. writePackage(join(profile, 'node_modules'), 'plugin', { dependencies: { ambient: '1.0.0' } })
  57. expect(() =>{ validateDesktopPluginGraph(profile, dsh, runtime, ['plugin']) }).toThrow(/outside its owned packages/u)
  58. })
  59. it('removes broken owned links without following them', () => {
  60. const { root, profile } = fixture()
  61. writePackage(join(profile, 'node_modules'), 'plugin')
  62. rmSync(join(root, 'dsh'), { recursive: true })
  63. expect(() =>{ unlinkDesktopHostPackages(profile) }).not.toThrow()
  64. })
  65. it('refuses to replace an unowned package at a managed name', () => {
  66. const { profile } = fixture()
  67. unlinkSync(join(profile, 'node_modules/@deepseek-ai/cordis'))
  68. writePackage(join(profile, 'node_modules'), '@deepseek-ai/cordis')
  69. expect(() =>{ unlinkDesktopHostPackages(profile) }).toThrow(/unowned package/u)
  70. })
  71. it('rejects private package links instead of following cycles or old transaction paths', () => {
  72. const { dsh, runtime, profile } = fixture()
  73. const plugin = writePackage(join(profile, 'node_modules'), 'plugin')
  74. symlinkSync(plugin, join(profile, 'node_modules/alias'), process.platform === 'win32' ? 'junction' : 'dir')
  75. expect(() =>{ validateDesktopPluginGraph(profile, dsh, runtime, ['plugin']) }).toThrow(/linked private package/u)
  76. })