code-mode.e2e.ts 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110
  1. import { mkdtemp, readFile, rm } from 'node:fs/promises'
  2. import { tmpdir } from 'node:os'
  3. import { join } from 'node:path'
  4. import { afterEach, describe, expect, it } from 'vitest'
  5. import { Context } from 'cordis'
  6. import LlmService from '@deepseek-ai/dsh-llm'
  7. import SessionStore from '@deepseek-ai/dsh-session'
  8. import type { SessionEvent } from '@deepseek-ai/dsh-session'
  9. import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
  10. import ToolRegistry, { RUN_CODE_NAME } from '@deepseek-ai/dsh-tools'
  11. import AgentRegistry, { AgentId } from '@deepseek-ai/dsh-agent'
  12. import AgentLoop, { type ReactLoopAgent } from '@deepseek-ai/dsh-agent-loop'
  13. import { LocalBashExecutor } from '@deepseek-ai/dsh-bash-local'
  14. import * as ToolBash from '@deepseek-ai/dsh-tool-bash'
  15. import * as LlmDeepSeek from '@deepseek-ai/dsh-llm-deepseek'
  16. import { WorkerCodeRuntime } from '@deepseek-ai/dsh-code-runtime-worker'
  17. /**
  18. * With-key Code Mode proof: a real model receives only `run_code`, composes two
  19. * sub-calls, writes a file, and returns curated output while the log records
  20. * each `tool/code-dispatch`. The keyless Loader smoke is in the sibling test.
  21. */
  22. const PERSONA = 'You are coding-agent. You work by writing TypeScript programs for run_code: '
  23. + 'batch related tool work into one program and print or return ONLY the findings that matter.'
  24. let ctx: Context | undefined
  25. let workdir: string | undefined
  26. afterEach(async () => {
  27. // Always dispose, even on failure/retry/timeout: agent-loop teardown stops
  28. // the loop, the executor kills stray processes, and the code runtime's
  29. // dispose awaits worker exits.
  30. await ctx?.fiber.dispose()
  31. ctx = undefined
  32. if (workdir !== undefined) await rm(workdir, { recursive: true, force: true })
  33. workdir = undefined
  34. })
  35. async function codeModeHarness(cwd: string): Promise<Context> {
  36. const harness = new Context()
  37. await harness.plugin(LlmService)
  38. await harness.plugin(SessionStore)
  39. await harness.plugin(SystemPrompt, { persona: PERSONA })
  40. await harness.plugin(ToolRegistry, { mode: 'code' })
  41. await harness.plugin(AgentRegistry)
  42. await harness.plugin(AgentLoop, { agents: [] })
  43. await harness.plugin(LlmDeepSeek, { models: ['deepseek-v4-flash'] })
  44. await harness.plugin(LocalBashExecutor, { cwd, timeoutMs: 30_000 })
  45. await harness.plugin(ToolBash)
  46. await harness.plugin(WorkerCodeRuntime, {})
  47. return harness
  48. }
  49. function waitForIdle(harness: Context, agent: ReactLoopAgent): Promise<void> {
  50. return new Promise((resolve) => {
  51. const dispose = harness.on('agent/status', (subject, status) => {
  52. if (subject === agent && status === 'idle') {
  53. dispose()
  54. resolve()
  55. }
  56. })
  57. })
  58. }
  59. describe.skipIf(!process.env.DEEPSEEK_API_KEY)('Code Mode: real model writes a program over real tools', () => {
  60. it('collapses the wire tool list to [run_code], bridges sub-calls, and returns curated output', async () => {
  61. workdir = await mkdtemp(join(tmpdir(), 'dsh-code-mode-e2e-'))
  62. ctx = await codeModeHarness(workdir)
  63. const agent = ctx.agentLoop.create(AgentId('e2e-code-mode'), { model: 'deepseek-v4-flash' })
  64. agent.send([{
  65. type: 'text',
  66. text: 'Using one run_code program: run `echo alpha-7` with the bash tool, run `echo beta-9` with the bash tool, '
  67. + 'then write both outputs joined by a plus sign into combined.txt (bash heredoc or redirect), '
  68. + 'and return only the joined string.',
  69. }])
  70. await waitForIdle(ctx, agent)
  71. const events: SessionEvent[] = [...agent.session.events]
  72. // The wire contract: every request this session made offered EXACTLY ONE
  73. // tool — run_code (the logged header snapshots the assembled list).
  74. const headers = events.filter(event => event.type === 'request/header')
  75. expect(headers.length).toBeGreaterThan(0)
  76. for (const header of headers) {
  77. expect(header.data.header.tools?.map(tool => tool.name)).toEqual([RUN_CODE_NAME])
  78. }
  79. // The model actually went through run_code…
  80. const calls = events.filter(event => event.type === 'tool/call')
  81. expect(calls.length).toBeGreaterThan(0)
  82. expect(calls.every(event => event.data.name === RUN_CODE_NAME)).toBe(true)
  83. // …and the program's tool calls landed as dispatch events under it.
  84. const dispatches = events.filter(event => event.type === 'tool/code-dispatch')
  85. expect(dispatches.length).toBeGreaterThanOrEqual(2)
  86. expect(dispatches.every(event => event.data.name === 'bash')).toBe(true)
  87. const parents = new Set(calls.map(event => event.data.callId))
  88. expect(dispatches.every(event => parents.has(event.data.parentCallId))).toBe(true)
  89. // World verification: the file the program wrote, and the curated answer.
  90. const combined = await readFile(join(workdir, 'combined.txt'), 'utf8')
  91. expect(combined).toContain('alpha-7')
  92. expect(combined).toContain('beta-9')
  93. const finalMessage = events.findLast(event => event.type === 'assistant/message')
  94. const finalText = finalMessage !== undefined
  95. ? finalMessage.data.content.filter(block => block.type === 'text').map(block => block.text).join('')
  96. : ''
  97. expect(finalText).toContain('alpha-7')
  98. expect(finalText).toContain('beta-9')
  99. }, 180_000)
  100. })