1
0

real-deepseek.e2e.ts 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143
  1. import { execFile } from 'node:child_process'
  2. import { randomUUID } from 'node:crypto'
  3. import {
  4. mkdirSync,
  5. mkdtempSync,
  6. readFileSync,
  7. rmSync,
  8. writeFileSync,
  9. } from 'node:fs'
  10. import { createRequire } from 'node:module'
  11. import { tmpdir } from 'node:os'
  12. import { dirname, join, resolve } from 'node:path'
  13. import { promisify } from 'node:util'
  14. import { Context } from '@deepseek-ai/cordis'
  15. import { afterEach, describe, expect, it, vi } from 'vitest'
  16. import type { Agent } from '@deepseek-ai/dsh-agent'
  17. import SubagentRuntime from '@deepseek-ai/dsh-subagent'
  18. import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
  19. import type { SubprocessHandle } from '@deepseek-ai/dsh-subprocess'
  20. import LocalSubprocessRuntime from '@deepseek-ai/dsh-subprocess-local'
  21. import * as codex from '../src/index.ts'
  22. import {
  23. startDeepSeekResponsesBridge,
  24. type DeepSeekResponsesBridge,
  25. } from './deepseek-responses-bridge.ts'
  26. const execFileAsync = promisify(execFile)
  27. const codexPackageJson = createRequire(import.meta.url).resolve('@openai/codex/package.json')
  28. const codexPackage = JSON.parse(readFileSync(
  29. codexPackageJson,
  30. 'utf8',
  31. )) as { version: string; bin: { codex: string } }
  32. const codexEntry = resolve(dirname(codexPackageJson), codexPackage.bin.codex)
  33. const roots: string[] = []
  34. const contexts: Context[] = []
  35. const bridges: DeepSeekResponsesBridge[] = []
  36. afterEach(async () => {
  37. await Promise.all(contexts.splice(0).map(ctx => ctx.fiber.dispose()))
  38. await Promise.all(bridges.splice(0).map(bridge => bridge.close()))
  39. for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true })
  40. })
  41. async function expectQuiescent(handles: readonly SubprocessHandle[]): Promise<void> {
  42. expect(handles.length).toBeGreaterThan(0)
  43. for (const handle of handles) {
  44. await expect(handle.waitForExit()).resolves.toBe(true)
  45. await expect(handle.done).resolves.toHaveProperty('exitCode')
  46. }
  47. }
  48. describe.skipIf(!process.env.DEEPSEEK_API_KEY)(
  49. 'Codex provider with real DeepSeek API',
  50. () => {
  51. it('returns one unique nonce through the production provider and real Codex', async () => {
  52. const apiKey = process.env.DEEPSEEK_API_KEY
  53. if (apiKey === undefined) throw new Error('e2e ran without DEEPSEEK_API_KEY')
  54. const root = mkdtempSync(join(tmpdir(), 'dsh-codex-deepseek-e2e-'))
  55. roots.push(root)
  56. const workspace = join(root, 'workspace')
  57. const codexHome = join(root, 'codex-home')
  58. mkdirSync(workspace)
  59. mkdirSync(codexHome)
  60. const nonce = `DSH_CODEX_DEEPSEEK_${randomUUID()}`
  61. const bridge = await startDeepSeekResponsesBridge(nonce)
  62. bridges.push(bridge)
  63. writeFileSync(join(codexHome, 'config.toml'), [
  64. 'model = "deepseek-v4-flash"',
  65. 'model_provider = "deepseek-e2e"',
  66. 'approval_policy = "never"',
  67. 'sandbox_mode = "read-only"',
  68. 'disable_response_storage = true',
  69. 'check_for_update_on_startup = false',
  70. '',
  71. '[model_providers.deepseek-e2e]',
  72. 'name = "DeepSeek E2E bridge"',
  73. `base_url = "${bridge.baseUrl}"`,
  74. 'env_key = "DEEPSEEK_API_KEY"',
  75. 'wire_api = "responses"',
  76. 'requires_openai_auth = false',
  77. '',
  78. '[analytics]',
  79. 'enabled = false',
  80. '',
  81. ].join('\n'))
  82. const env = {
  83. DEEPSEEK_API_KEY: apiKey,
  84. CODEX_HOME: codexHome,
  85. HOME: root,
  86. XDG_CONFIG_HOME: join(root, 'xdg-config'),
  87. PATH: root,
  88. HTTP_PROXY: '',
  89. HTTPS_PROXY: '',
  90. ALL_PROXY: '',
  91. NO_PROXY: '127.0.0.1,localhost',
  92. }
  93. const ctx = new Context()
  94. contexts.push(ctx)
  95. await ctx.plugin(SessionProjectionRegistry)
  96. await ctx.plugin(SubagentRuntime)
  97. await ctx.plugin(LocalSubprocessRuntime)
  98. const handles: SubprocessHandle[] = []
  99. const spawn = ctx.subprocess.spawn.bind(ctx.subprocess)
  100. vi.spyOn(ctx.subprocess, 'spawn').mockImplementation((spec) => {
  101. const handle = spawn(spec)
  102. handles.push(handle)
  103. return handle
  104. })
  105. await ctx.plugin(codex, { env, disposeGraceMs: 2_000 })
  106. const version = await execFileAsync(process.execPath, [codexEntry, '--version'], {
  107. env: { ...process.env, ...env },
  108. })
  109. expect(codexPackage.version).toBe('0.153.4')
  110. expect(version.stdout.trim()).toBe('codex-cli 0.153.4')
  111. const parent = {
  112. id: 'deepseek-e2e-parent',
  113. session: { header: { cwd: workspace } },
  114. } as unknown as Agent
  115. const run = await ctx.subagents.start('codex', {
  116. prompt: [{
  117. type: 'text',
  118. text: `Reply with exactly ${nonce} and nothing else. Do not use tools.`,
  119. }],
  120. parent,
  121. signal: new AbortController().signal,
  122. })
  123. const result = await run.result
  124. await run.dispose()
  125. expect(result.stopReason).toBe('completed')
  126. const text = result.output
  127. .filter(block => block.type === 'text')
  128. .map(block => block.text)
  129. .join('')
  130. .trim()
  131. expect(text).toBe(nonce)
  132. expect(bridge.completedRequests).toBe(1)
  133. await expectQuiescent(handles)
  134. }, 180_000)
  135. },
  136. )