real-deepseek.e2e.ts 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160
  1. import { execFile } from 'node:child_process'
  2. import { randomUUID } from 'node:crypto'
  3. import {
  4. mkdirSync,
  5. mkdtempSync,
  6. readFileSync,
  7. rmSync,
  8. } from 'node:fs'
  9. import { tmpdir } from 'node:os'
  10. import { dirname, join, resolve } from 'node:path'
  11. import { fileURLToPath } from 'node:url'
  12. import { promisify } from 'node:util'
  13. import { Context } from '@deepseek-ai/cordis'
  14. import { afterEach, describe, expect, it, vi } from 'vitest'
  15. import type { Agent } from '@deepseek-ai/dsh-agent'
  16. import SubagentRuntime from '@deepseek-ai/dsh-subagent'
  17. import type { SubprocessHandle } from '@deepseek-ai/dsh-subprocess'
  18. import LocalSubprocessRuntime from '@deepseek-ai/dsh-subprocess-local'
  19. import * as claudeCode from '../src/index.ts'
  20. const execFileAsync = promisify(execFile)
  21. const OFFICIAL_DEEPSEEK_BASE_URL = 'https://api.deepseek.com'
  22. const sdkRoot = dirname(fileURLToPath(
  23. import.meta.resolve('@anthropic-ai/claude-agent-sdk'),
  24. ))
  25. const sdkPackage = JSON.parse(readFileSync(
  26. join(sdkRoot, 'package.json'),
  27. 'utf8',
  28. )) as {
  29. version: string
  30. claudeCodeVersion: string
  31. optionalDependencies: Record<string, string>
  32. }
  33. const platformPackage = `@anthropic-ai/claude-agent-sdk-${process.platform}-${process.arch}`
  34. const platformRoot = resolve(sdkRoot, '..', platformPackage.split('/')[1]!)
  35. const claudeBin = join(
  36. platformRoot,
  37. process.platform === 'win32' ? 'claude.exe' : 'claude',
  38. )
  39. const roots: string[] = []
  40. const contexts: Context[] = []
  41. afterEach(async () => {
  42. await Promise.all(contexts.splice(0).map(ctx => ctx.fiber.dispose()))
  43. for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true })
  44. })
  45. function deepSeekBaseUrl(): string {
  46. const configured = (process.env.DEEPSEEK_BASE_URL ?? OFFICIAL_DEEPSEEK_BASE_URL)
  47. .replace(/\/+$/, '')
  48. if (configured !== OFFICIAL_DEEPSEEK_BASE_URL) {
  49. throw new Error('Claude Code DeepSeek e2e requires the official DeepSeek base URL')
  50. }
  51. return configured
  52. }
  53. async function expectQuiescent(handles: readonly SubprocessHandle[]): Promise<void> {
  54. expect(handles.length).toBeGreaterThan(0)
  55. for (const handle of handles) {
  56. await expect(handle.waitForExit()).resolves.toBe(true)
  57. await expect(handle.done).resolves.toHaveProperty('exitCode')
  58. }
  59. }
  60. describe.skipIf(!process.env.DEEPSEEK_API_KEY)(
  61. 'Claude Code provider with real DeepSeek API',
  62. () => {
  63. it('returns one unique nonce through the production provider and real SDK/CLI', async () => {
  64. const apiKey = process.env.DEEPSEEK_API_KEY
  65. if (apiKey === undefined) throw new Error('e2e ran without DEEPSEEK_API_KEY')
  66. const root = mkdtempSync(join(tmpdir(), 'dsh-claude-deepseek-e2e-'))
  67. roots.push(root)
  68. const workspace = join(root, 'workspace')
  69. const claudeConfig = join(root, 'claude-config')
  70. const xdgConfig = join(root, 'xdg-config')
  71. const xdgCache = join(root, 'xdg-cache')
  72. const xdgData = join(root, 'xdg-data')
  73. const xdgState = join(root, 'xdg-state')
  74. for (const directory of [
  75. workspace,
  76. claudeConfig,
  77. xdgConfig,
  78. xdgCache,
  79. xdgData,
  80. xdgState,
  81. ]) mkdirSync(directory)
  82. const env = {
  83. ANTHROPIC_AUTH_TOKEN: apiKey,
  84. ANTHROPIC_BASE_URL: `${deepSeekBaseUrl()}/anthropic`,
  85. ANTHROPIC_MODEL: 'deepseek-v4-pro[1m]',
  86. ANTHROPIC_DEFAULT_OPUS_MODEL: 'deepseek-v4-pro[1m]',
  87. ANTHROPIC_DEFAULT_SONNET_MODEL: 'deepseek-v4-pro[1m]',
  88. ANTHROPIC_DEFAULT_HAIKU_MODEL: 'deepseek-v4-flash',
  89. CLAUDE_CODE_SUBAGENT_MODEL: 'deepseek-v4-flash',
  90. CLAUDE_CODE_EFFORT_LEVEL: 'max',
  91. CLAUDE_CONFIG_DIR: claudeConfig,
  92. HOME: root,
  93. XDG_CONFIG_HOME: xdgConfig,
  94. XDG_CACHE_HOME: xdgCache,
  95. XDG_DATA_HOME: xdgData,
  96. XDG_STATE_HOME: xdgState,
  97. CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: '1',
  98. CLAUDE_CODE_DISABLE_OFFICIAL_MARKETPLACE_AUTOINSTALL: '1',
  99. DISABLE_TELEMETRY: '1',
  100. DISABLE_ERROR_REPORTING: '1',
  101. HTTP_PROXY: '',
  102. HTTPS_PROXY: '',
  103. ALL_PROXY: '',
  104. NO_PROXY: '127.0.0.1,localhost',
  105. }
  106. const ctx = new Context()
  107. contexts.push(ctx)
  108. await ctx.plugin(SubagentRuntime)
  109. await ctx.plugin(LocalSubprocessRuntime)
  110. const handles: SubprocessHandle[] = []
  111. const spawn = ctx.subprocess.spawn.bind(ctx.subprocess)
  112. vi.spyOn(ctx.subprocess, 'spawn').mockImplementation((spec) => {
  113. const handle = spawn(spec)
  114. handles.push(handle)
  115. return handle
  116. })
  117. await ctx.plugin(claudeCode, { env, disposeGraceMs: 3_000 })
  118. expect(sdkPackage.version).toBe('0.3.220')
  119. expect(sdkPackage.claudeCodeVersion).toBe('2.1.220')
  120. expect(sdkPackage.optionalDependencies[platformPackage]).toBe('0.3.220')
  121. const version = await execFileAsync(claudeBin, ['--version'], {
  122. env: { ...process.env, ...env },
  123. })
  124. expect(version.stdout.trim()).toBe('2.1.220 (Claude Code)')
  125. const nonce = `DSH_CLAUDE_DEEPSEEK_${randomUUID()}`
  126. const parent = {
  127. id: 'deepseek-e2e-parent',
  128. session: { header: { cwd: workspace } },
  129. } as unknown as Agent
  130. const run = await ctx.subagents.start('claude-code', {
  131. prompt: [{
  132. type: 'text',
  133. text: `Reply with exactly ${nonce} and nothing else. Do not use tools.`,
  134. }],
  135. parent,
  136. signal: new AbortController().signal,
  137. })
  138. const result = await run.result
  139. await run.dispose()
  140. expect(result.stopReason).toBe('completed')
  141. const text = result.output
  142. .filter(block => block.type === 'text')
  143. .map(block => block.text)
  144. .join('')
  145. .trim()
  146. expect(text).toBe(nonce)
  147. await expectQuiescent(handles)
  148. }, 180_000)
  149. },
  150. )