real-deepseek.e2e.ts 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154
  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_MESSAGES_BASE_URL = 'https://api.deepseek.com/anthropic'
  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. async function expectQuiescent(handles: readonly SubprocessHandle[]): Promise<void> {
  48. expect(handles.length).toBeGreaterThan(0)
  49. for (const handle of handles) {
  50. await expect(handle.waitForExit()).resolves.toBe(true)
  51. await expect(handle.done).resolves.toHaveProperty('exitCode')
  52. }
  53. }
  54. describe.skipIf(!process.env.DEEPSEEK_API_KEY)(
  55. 'Claude Code provider with real DeepSeek API',
  56. () => {
  57. it('returns one unique nonce through the production provider and real SDK/CLI', async () => {
  58. const apiKey = process.env.DEEPSEEK_API_KEY
  59. if (apiKey === undefined) throw new Error('e2e ran without DEEPSEEK_API_KEY')
  60. const root = mkdtempSync(join(tmpdir(), 'dsh-claude-deepseek-e2e-'))
  61. roots.push(root)
  62. const workspace = join(root, 'workspace')
  63. const claudeConfig = join(root, 'claude-config')
  64. const xdgConfig = join(root, 'xdg-config')
  65. const xdgCache = join(root, 'xdg-cache')
  66. const xdgData = join(root, 'xdg-data')
  67. const xdgState = join(root, 'xdg-state')
  68. for (const directory of [
  69. workspace,
  70. claudeConfig,
  71. xdgConfig,
  72. xdgCache,
  73. xdgData,
  74. xdgState,
  75. ]) mkdirSync(directory)
  76. const env = {
  77. ANTHROPIC_AUTH_TOKEN: apiKey,
  78. ANTHROPIC_BASE_URL: OFFICIAL_DEEPSEEK_MESSAGES_BASE_URL,
  79. ANTHROPIC_MODEL: DEEPSEEK_MODEL,
  80. ANTHROPIC_DEFAULT_OPUS_MODEL: DEEPSEEK_MODEL,
  81. ANTHROPIC_DEFAULT_SONNET_MODEL: DEEPSEEK_MODEL,
  82. ANTHROPIC_DEFAULT_HAIKU_MODEL: DEEPSEEK_MODEL,
  83. CLAUDE_CODE_SUBAGENT_MODEL: DEEPSEEK_MODEL,
  84. CLAUDE_CODE_EFFORT_LEVEL: 'max',
  85. CLAUDE_CONFIG_DIR: claudeConfig,
  86. HOME: root,
  87. XDG_CONFIG_HOME: xdgConfig,
  88. XDG_CACHE_HOME: xdgCache,
  89. XDG_DATA_HOME: xdgData,
  90. XDG_STATE_HOME: xdgState,
  91. CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: '1',
  92. CLAUDE_CODE_DISABLE_OFFICIAL_MARKETPLACE_AUTOINSTALL: '1',
  93. DISABLE_TELEMETRY: '1',
  94. DISABLE_ERROR_REPORTING: '1',
  95. HTTP_PROXY: '',
  96. HTTPS_PROXY: '',
  97. ALL_PROXY: '',
  98. NO_PROXY: '127.0.0.1,localhost',
  99. }
  100. const ctx = new Context()
  101. contexts.push(ctx)
  102. await ctx.plugin(SessionProjectionRegistry)
  103. await ctx.plugin(SubagentRuntime)
  104. await ctx.plugin(LocalSubprocessRuntime)
  105. const handles: SubprocessHandle[] = []
  106. const spawn = ctx.subprocess.spawn.bind(ctx.subprocess)
  107. vi.spyOn(ctx.subprocess, 'spawn').mockImplementation((spec) => {
  108. const handle = spawn(spec)
  109. handles.push(handle)
  110. return handle
  111. })
  112. await ctx.plugin(claudeCode, { env, disposeGraceMs: 3_000 })
  113. expect(sdkPackage.version).toBe('0.3.263')
  114. expect(sdkPackage.claudeCodeVersion).toBe('2.1.263')
  115. expect(sdkPackage.optionalDependencies[platformPackage]).toBe('0.3.263')
  116. const version = await execFileAsync(claudeBin, ['--version'], {
  117. env: { ...process.env, ...env },
  118. })
  119. expect(version.stdout.trim()).toBe('2.1.263 (Claude Code)')
  120. const nonce = `DSH_CLAUDE_DEEPSEEK_${randomUUID()}`
  121. const parent = {
  122. id: 'deepseek-e2e-parent',
  123. session: { header: { cwd: workspace } },
  124. } as unknown as Agent
  125. const run = await ctx.subagents.start('claude-code', {
  126. prompt: [{
  127. type: 'text',
  128. text: `Reply with exactly ${nonce} and nothing else. Do not use tools.`,
  129. }],
  130. parent,
  131. signal: new AbortController().signal,
  132. })
  133. const result = await run.result
  134. await run.dispose()
  135. expect(result.stopReason).toBe('completed')
  136. const text = result.output
  137. .filter(block => block.type === 'text')
  138. .map(block => block.text)
  139. .join('')
  140. .trim()
  141. expect(text).toBe(nonce)
  142. await expectQuiescent(handles)
  143. }, 180_000)
  144. },
  145. )