agent-team-headless.e2e.ts 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114
  1. import { mkdir, mkdtemp, readFile, readdir, rm, writeFile } from 'node:fs/promises'
  2. import { tmpdir } from 'node:os'
  3. import { join } from 'node:path'
  4. import { fileURLToPath, pathToFileURL } from 'node:url'
  5. import { execa } from 'execa'
  6. import { describe, expect, it } from 'vitest'
  7. import { resolveExampleLaunch } from '@deepseek-ai/dsh-loader-smoke'
  8. const dshBinScript = fileURLToPath(new URL('../src/bin.ts', import.meta.url))
  9. const tsconfigPath = fileURLToPath(new URL('../../../tsconfig.json', import.meta.url))
  10. const fixturePlugin = pathToFileURL(fileURLToPath(
  11. new URL('./profiles/headless/tests/fixtures/team-llm.mjs', import.meta.url),
  12. )).href
  13. function records(content: string): Record<string, unknown>[] {
  14. return content.split('\n').filter(Boolean).map(line => JSON.parse(line) as Record<string, unknown>)
  15. }
  16. describe('dsh run with Agent Teams enabled', () => {
  17. it('runs two teammates, durable peer mail, dependent tasks, waiting, and final aggregation', async () => {
  18. const cwd = await mkdtemp(join(tmpdir(), 'dsh-agent-team-headless-'))
  19. try {
  20. const home = join(cwd, '.dsh')
  21. const sessions = join(home, 'sessions')
  22. const profileDir = join(home, 'profiles', 'headless')
  23. await mkdir(profileDir, { recursive: true })
  24. await writeFile(join(profileDir, 'package.json'), JSON.stringify({
  25. name: 'dsh-profile-headless',
  26. private: true,
  27. dependencies: {
  28. '@deepseek-ai/dsh-experimental-agent-team-profile': 'workspace:^',
  29. },
  30. dsh: {
  31. profile: {
  32. bundles: [
  33. '@deepseek-ai/dsh-base',
  34. '@deepseek-ai/dsh-headless',
  35. '@deepseek-ai/dsh-experimental-agent-team-profile',
  36. ],
  37. },
  38. },
  39. }, undefined, 2) + '\n')
  40. await writeFile(join(profileDir, 'cordis.patch.yml'), [
  41. '- id: llm-deepseek',
  42. ' disabled: true',
  43. '- id: session-persistence-jsonl',
  44. ' config:',
  45. ` root: '${sessions}'`,
  46. ' compression: none',
  47. '- insert:',
  48. ' - id: team-fixture-llm',
  49. ` name: '${fixturePlugin}'`,
  50. '',
  51. ].join('\n'))
  52. const launch = resolveExampleLaunch({
  53. srcBin: dshBinScript,
  54. configArgs: ['--profile', 'headless', '请明确使用 Agent Teams,把调研和实现拆给两个 teammate,等待完成后汇总。'],
  55. tsconfigPath,
  56. env: {
  57. DSH_HOME: home,
  58. DSH_AGENTS_HOME: join(cwd, '.agents'),
  59. DSH_TELEMETRY_DISABLED: '1',
  60. DEEPSEEK_API_KEY: '',
  61. NODE_OPTIONS: [
  62. process.env.NODE_OPTIONS,
  63. '--disable-warning=ExperimentalWarning',
  64. '--disable-warning=MODULE_TYPELESS_PACKAGE_JSON',
  65. ].filter(Boolean).join(' '),
  66. },
  67. })
  68. const result = await execa(launch.command, launch.args, {
  69. cwd,
  70. env: launch.env,
  71. input: '',
  72. timeout: 90_000,
  73. killSignal: 'SIGKILL',
  74. reject: false,
  75. })
  76. expect(
  77. result.exitCode,
  78. `dsh headless profile exited unexpectedly.\nstdout:\n${result.stdout}\nstderr:\n${result.stderr}`,
  79. ).toBe(0)
  80. expect(result.stderr).toBe('')
  81. expect(result.stdout).toContain('TEAM_WORKFLOW_OK')
  82. const files = (await readdir(sessions, { recursive: true }))
  83. .filter(file => file.endsWith('.jsonl'))
  84. expect(files).toHaveLength(3)
  85. const logs = await Promise.all(files.map(file => readFile(join(sessions, file), 'utf8')))
  86. const parsed = logs.map(records)
  87. const root = parsed.find((log) => {
  88. const header = log[0]
  89. return header?.type === 'session' && typeof header.parentSession !== 'string'
  90. })
  91. expect(root).toBeDefined()
  92. const eventTypes = root!.map(record => record.type)
  93. expect(eventTypes.filter(type => type === 'team/member')).toHaveLength(4)
  94. expect(eventTypes).toContain('team/message/queued')
  95. expect(eventTypes).toContain('team/message/delivered')
  96. const taskEvents = root!.filter(record => record.type === 'team/task')
  97. expect(taskEvents.filter((record) => {
  98. const data = record.data as { task?: { status?: string } } | undefined
  99. return data?.task?.status === 'completed'
  100. })).toHaveLength(2)
  101. const toolNames = root!.filter(record => record.type === 'tool/call')
  102. .map(record => (record.data as { name?: string } | undefined)?.name)
  103. expect(toolNames).toContain('wait_agent')
  104. expect(toolNames).toContain('team_task_list')
  105. expect(toolNames).toContain('list_agents')
  106. } finally {
  107. await rm(cwd, { recursive: true, force: true })
  108. }
  109. }, 105_000)
  110. })