real-deepseek.e2e.ts 5.8 KB

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