github-repository-plugin.built.e2e.ts 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123
  1. import { existsSync, mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync } from 'node:fs'
  2. import { createRequire } from 'node:module'
  3. import { tmpdir } from 'node:os'
  4. import { join } from 'node:path'
  5. import { fileURLToPath } from 'node:url'
  6. import { startMockLlmServer } from '@deepseek-ai/dsh-llm-mock-server'
  7. import { execa } from 'execa'
  8. import { describe, expect, it } from 'vitest'
  9. const repoRoot = fileURLToPath(new URL('../../../', import.meta.url))
  10. const dshBin = join(repoRoot, 'apps/cli/lib/bin.js')
  11. const source = process.env.DSH_GITHUB_REPOSITORY_PLUGIN_SOURCE
  12. const required = process.env.DSH_REQUIRE_GITHUB_REPOSITORY_PLUGIN_E2E === '1'
  13. const enabled = required || source !== undefined
  14. describe.skipIf(!enabled)('dsh run GitHub repository Plugin installation', () => {
  15. it('installs, builds, and runs skill, MCP, and TypeScript Plugin contributions from a private exact GitHub source', async () => {
  16. expect(existsSync(dshBin), 'the repository Plugin acceptance must run the built dsh entry').toBe(true)
  17. expect(source, 'DSH_GITHUB_REPOSITORY_PLUGIN_SOURCE is required by this CI lane').toMatch(
  18. /^github:[^/\s#&]+\/[^/\s#&]+#[0-9a-f]{40}&path:\/.*\/\.dsh-plugin$/u,
  19. )
  20. const apiKey = 'github-repository-plugin-e2e-key'
  21. const server = await startMockLlmServer({
  22. sequence: ['tool_call_success', 'success'],
  23. apiKey,
  24. toolName: 'mcp__github_repository__proof',
  25. toolArguments: '{}',
  26. successText: 'trusted GitHub repository package reached dsh run',
  27. })
  28. const home = mkdtempSync(join(tmpdir(), 'dsh-github-repository-plugin-'))
  29. const patch = join(home, 'github-repository-plugin.cordis.patch.yml')
  30. writeFileSync(patch, [
  31. '- id: repository-plugins',
  32. ' config:',
  33. ' repositories:',
  34. ` - ${JSON.stringify(source)}`,
  35. '- id: session-title-llm',
  36. ' disabled: true',
  37. '',
  38. ].join('\n'))
  39. try {
  40. const result = await execa(process.execPath, [
  41. dshBin,
  42. 'run',
  43. '--patch',
  44. patch,
  45. 'prove the private GitHub repository Plugin is active',
  46. ], {
  47. cwd: repoRoot,
  48. input: '',
  49. timeout: 180_000,
  50. killSignal: 'SIGKILL',
  51. reject: false,
  52. env: {
  53. ...process.env,
  54. DSH_HOME: home,
  55. DSH_TELEMETRY_DISABLED: '1',
  56. DEEPSEEK_API_KEY: apiKey,
  57. DEEPSEEK_BASE_URL: server.baseURL,
  58. },
  59. })
  60. if (result.timedOut) {
  61. throw new Error(`dsh GitHub repository Plugin run did not exit within 180s. stdout:\n${result.stdout}\nstderr:\n${result.stderr}`)
  62. }
  63. expect(result.exitCode, `${result.stderr}\nstdout:\n${result.stdout}`).toBe(0)
  64. expect(result.stdout).toBe('trusted GitHub repository package reached dsh run')
  65. expect(server.requests).toHaveLength(2)
  66. const runtimeDiagnostic = `${result.stderr}\nstdout:\n${result.stdout}`
  67. const firstRequest = JSON.stringify(server.requests[0]!.body)
  68. const secondRequest = JSON.stringify(server.requests[1]!.body)
  69. expect(firstRequest, runtimeDiagnostic).toContain(
  70. 'Proves that dsh installed a private repository Plugin from an exact GitHub source.',
  71. )
  72. expect(firstRequest, runtimeDiagnostic).toContain('mcp__github_repository__proof')
  73. expect(firstRequest, runtimeDiagnostic).toContain('Proves that an MCP server compiled from the exact GitHub repository package is active.')
  74. expect(secondRequest, runtimeDiagnostic).toContain('MCP_FROM_GITHUB_REPOSITORY')
  75. expect(secondRequest, runtimeDiagnostic).toContain('TS_PLUGIN_FROM_GITHUB_REPOSITORY')
  76. const cacheRoot = join(home, 'cache', 'repository-plugins')
  77. const generations = readdirSync(cacheRoot, { withFileTypes: true }).filter(entry => entry.isDirectory())
  78. expect(generations).toHaveLength(1)
  79. const installed = join(cacheRoot, generations[0]!.name, 'node_modules', 'repository')
  80. const manifest = JSON.parse(readFileSync(join(installed, 'package.json'), 'utf8')) as Record<string, unknown>
  81. expect(manifest).toMatchObject({
  82. name: 'dsh-github-repository-plugin-e2e-fixture',
  83. private: true,
  84. scripts: {
  85. prepack: 'tsc --noEmit && tsdown src/plugin.ts src/mcp-server.ts --no-config --tsconfig tsconfig.json --out-dir lib --platform node --target es2024 --clean && dsh-plugin-prepare',
  86. },
  87. dsh: {
  88. skills: ['../skills'],
  89. mcpServers: './.mcp.json',
  90. entry: './lib/plugin.mjs',
  91. },
  92. dependencies: {
  93. '@modelcontextprotocol/sdk': '1.29.0',
  94. },
  95. devDependencies: {
  96. cordis: '4.0.0-rc.7',
  97. tsdown: '0.22.2',
  98. typescript: '6.0.3',
  99. },
  100. })
  101. expect(readFileSync(join(installed, 'dsh-plugin-assets/skills/0/github-source-proof/SKILL.md'), 'utf8'))
  102. .toContain('This skill exists only in the GitHub repository source fixture.')
  103. expect(readFileSync(join(installed, 'dsh-plugin-assets/.mcp.json'), 'utf8')).toContain('lib/mcp-server.mjs')
  104. expect(readFileSync(join(installed, 'lib/plugin.mjs'), 'utf8')).toContain('TS_PLUGIN_FROM_GITHUB_REPOSITORY')
  105. expect(readFileSync(join(installed, 'lib/mcp-server.mjs'), 'utf8')).toContain('MCP_FROM_GITHUB_REPOSITORY')
  106. expect(existsSync(join(installed, 'src'))).toBe(false)
  107. const installedRequire = createRequire(join(installed, 'lib/mcp-server.mjs'))
  108. expect(existsSync(installedRequire.resolve('@modelcontextprotocol/sdk/server/mcp.js'))).toBe(true)
  109. const wrapper = readFileSync(join(installed, 'dsh-plugin.mjs'), 'utf8')
  110. expect(wrapper).toContain('dsh-repository-plugin')
  111. expect(wrapper).toContain('await import(manifest.entry)')
  112. expect(wrapper).toContain('"entry":"./lib/plugin.mjs"')
  113. } finally {
  114. await server.close()
  115. rmSync(home, { recursive: true, force: true })
  116. }
  117. }, 190_000)
  118. })