link-workspace.e2e.ts 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112
  1. import { execFile } from 'node:child_process'
  2. import { existsSync } from 'node:fs'
  3. import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'
  4. import { tmpdir } from 'node:os'
  5. import { join } from 'node:path'
  6. import { fileURLToPath } from 'node:url'
  7. import { promisify } from 'node:util'
  8. import { afterEach, describe, expect, it } from 'vitest'
  9. import {
  10. LocalPluginBlueprint,
  11. featureId,
  12. createPackageManager,
  13. type PackageManagerName,
  14. } from '@deepseek-ai/dsh-helper'
  15. import { scrubEnvironment } from '../../helper/src/package-managers/package-manager.ts'
  16. import { scaffoldProject } from '../src/project-scaffolder.ts'
  17. const execFileAsync = promisify(execFile)
  18. const repoRoot = fileURLToPath(new URL('../../../../', import.meta.url))
  19. const builtScripts = join(repoRoot, 'packages/sdk/scripts/lib/bin.js')
  20. const temporary: string[] = []
  21. afterEach(async () => {
  22. await Promise.all(temporary.splice(0).map(path => rm(path, { recursive: true, force: true })))
  23. })
  24. async function managerVersion(name: PackageManagerName): Promise<string | undefined> {
  25. try {
  26. return (await execFileAsync(name, ['--version'], { encoding: 'utf8' })).stdout.trim()
  27. } catch {
  28. // An unavailable optional manager skips only its own live-link case.
  29. return undefined
  30. }
  31. }
  32. const managers: PackageManagerName[] = ['npm', 'pnpm', 'yarn']
  33. describe.skipIf(!existsSync(builtScripts))('live-linked generated projects', () => {
  34. for (const name of managers) {
  35. it(`${name}: installs the local closure and resolves plugin TypeScript in dev`, async (context) => {
  36. const version = await managerVersion(name)
  37. if (!version) {
  38. context.skip()
  39. return
  40. }
  41. const parent = await mkdtemp(join(tmpdir(), `dsh-link-${name}-`))
  42. const root = join(parent, 'project')
  43. temporary.push(parent)
  44. const manager = createPackageManager(name, version)
  45. await scaffoldProject(root, {
  46. name: `linked-${name}`,
  47. description: 'link e2e',
  48. runtime: { model: 'deepseek-v4-flash' },
  49. packageManager: manager,
  50. releaseVersion: '0.0.1',
  51. linkWorkspaceRoot: repoRoot,
  52. features: [
  53. { id: featureId('provider'), options: ['deepseek'], secrets: { apiKey: 'test-key' } },
  54. { id: featureId('bash'), options: ['local'] },
  55. { id: featureId('app'), options: ['embed'] },
  56. { id: featureId('persistence'), options: ['jsonl'] },
  57. ],
  58. localPlugins: [new LocalPluginBlueprint('probe', 'plugin')],
  59. })
  60. await writeFile(join(root, 'plugins/probe/src/index.ts'), `
  61. import { writeFileSync } from 'node:fs'
  62. import type { Context } from 'cordis'
  63. export const name = 'probe'
  64. export function apply(_ctx: Context): void {
  65. writeFileSync(new URL('../../../plugin-loaded', import.meta.url), 'loaded\\n')
  66. }
  67. `)
  68. const cacheRoot = join(tmpdir(), 'dsh-sdk-link-cache', name)
  69. const commandEnvironment = {
  70. ...scrubEnvironment(),
  71. COREPACK_HOME: join(cacheRoot, 'corepack'),
  72. XDG_CACHE_HOME: join(cacheRoot, 'cache'),
  73. XDG_DATA_HOME: join(cacheRoot, 'data'),
  74. npm_config_cache: join(cacheRoot, 'npm'),
  75. pnpm_config_store_dir: join(cacheRoot, 'pnpm-store'),
  76. }
  77. await execFileAsync(name, manager.installCommand(), {
  78. cwd: root,
  79. env: commandEnvironment,
  80. encoding: 'utf8',
  81. timeout: 120_000,
  82. })
  83. await execFileAsync(name, manager.buildCommand(), {
  84. cwd: root,
  85. env: commandEnvironment,
  86. encoding: 'utf8',
  87. timeout: 120_000,
  88. })
  89. expect(existsSync(join(root, 'index.js'))).toBe(true)
  90. expect(existsSync(join(root, 'plugins/probe/lib/index.js'))).toBe(true)
  91. const dshSdk = join(root, 'node_modules/@deepseek-ai/dsh-scripts/lib/bin.js')
  92. const run = await execFileAsync(process.execPath, [dshSdk, 'dev', 'index.ts'], {
  93. cwd: root,
  94. env: { ...commandEnvironment, DEEPSEEK_API_KEY: 'test-key' },
  95. encoding: 'utf8',
  96. timeout: 30_000,
  97. })
  98. expect(run.stderr).not.toContain('without inject')
  99. expect(await readFile(join(root, 'plugin-loaded'), 'utf8')).toBe('loaded\n')
  100. const manifest = JSON.parse(await readFile(join(root, 'package.json'), 'utf8')) as {
  101. dependencies: Record<string, string>
  102. }
  103. expect(manifest.dependencies.cordis).toMatch(name === 'npm' ? /^file:/ : name === 'pnpm' ? /^link:/ : /^portal:/)
  104. expect(manifest.dependencies).not.toHaveProperty('node-addon-require-builtin')
  105. }, 180_000)
  106. }
  107. })