integration.spec.ts 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100
  1. import { describe, expect, it } from 'vitest'
  2. import { Context } from 'cordis'
  3. import { SessionId, type SessionEvent } from '@deepseek-ai/dsh-session'
  4. import type { Agent } from '@deepseek-ai/dsh-agent'
  5. import AgentLoop from '@deepseek-ai/dsh-agent-loop'
  6. import { mountAgentLoopTestDependencies } from '@deepseek-ai/dsh-agent-loop-testkit'
  7. import * as ToolTodo from '@deepseek-ai/dsh-tool-todo'
  8. import { MockAdapter, textResponse, toolCallResponse } from '../../../core/agent-loop/tests/mock-adapter.ts'
  9. /**
  10. * Full-loop integration: a scripted mock model drives the REAL todo_write tool
  11. * through the agent loop, exercising the same seams a live model would — the
  12. * tool/call + tool/result session events AND the todo/write event the tool
  13. * appends. Only the model is mocked; the tool and the session log are real.
  14. */
  15. async function harness(adapter: MockAdapter): Promise<Context> {
  16. const ctx = new Context()
  17. await mountAgentLoopTestDependencies(ctx)
  18. await ctx.plugin(AgentLoop, { agents: [] })
  19. await ctx.plugin(ToolTodo)
  20. ctx.llm.registerAdapter(['mock'], adapter)
  21. return ctx
  22. }
  23. function waitForIdle(ctx: Context, agent: Agent): Promise<void> {
  24. return new Promise((resolve) => {
  25. const dispose = ctx.on('agent/status', (subject, status) => {
  26. if (subject === agent && status === 'idle') {
  27. dispose()
  28. resolve()
  29. }
  30. })
  31. })
  32. }
  33. function findEvent<T extends SessionEvent['type']>(
  34. log: readonly SessionEvent[],
  35. type: T,
  36. position: 'first' | 'last' = 'first',
  37. ): Extract<SessionEvent, { type: T }> {
  38. const found = position === 'first'
  39. ? log.find(event => event.type === type)
  40. : log.findLast(event => event.type === type)
  41. if (!found) throw new Error(`no ${type} event in the session log`)
  42. return found as Extract<SessionEvent, { type: T }>
  43. }
  44. describe('todo_write tool through the agent loop', () => {
  45. it('model calls todo_write: a tool/call, a non-error tool/result, and a todo/write snapshot land', async () => {
  46. const adapter = new MockAdapter([
  47. toolCallResponse('call-1', 'todo_write', {
  48. todos: [
  49. { content: 'read the code', status: 'in_progress' },
  50. { content: 'write the fix', status: 'pending' },
  51. ],
  52. }, 'Planning the work.'),
  53. textResponse('Plan recorded.'),
  54. ])
  55. const ctx = await harness(adapter)
  56. const agent = ctx.agentLoop.create(SessionId('it-todo'), { provider: 'mock', model: 'mock' })
  57. agent.followup([{ type: 'text', text: 'plan a two-step task' }])
  58. await waitForIdle(ctx, agent)
  59. const log = agent.session.events
  60. expect(findEvent(log, 'tool/call').data.name).toBe('todo_write')
  61. expect(findEvent(log, 'tool/result').data.isError).toBe(false)
  62. const todoEvent = findEvent(log, 'todo/write')
  63. expect(todoEvent.data.todos).toEqual([
  64. { content: 'read the code', status: 'in_progress' },
  65. { content: 'write the fix', status: 'pending' },
  66. ])
  67. })
  68. it('a second todo_write replaces the list (last-write-wins on the log)', async () => {
  69. const adapter = new MockAdapter([
  70. toolCallResponse('call-1', 'todo_write', { todos: [{ content: 'step one', status: 'in_progress' }] }),
  71. toolCallResponse('call-2', 'todo_write', {
  72. todos: [
  73. { content: 'step one', status: 'completed' },
  74. { content: 'step two', status: 'in_progress' },
  75. ],
  76. }),
  77. textResponse('Done planning.'),
  78. ])
  79. const ctx = await harness(adapter)
  80. const agent = ctx.agentLoop.create(SessionId('it-todo-2'), { provider: 'mock', model: 'mock' })
  81. agent.followup([{ type: 'text', text: 'plan then update' }])
  82. await waitForIdle(ctx, agent)
  83. const todoEvents = agent.session.events.filter(e => e.type === 'todo/write')
  84. expect(todoEvents).toHaveLength(2)
  85. expect(findEvent(agent.session.events, 'todo/write', 'last').data.todos).toEqual([
  86. { content: 'step one', status: 'completed' },
  87. { content: 'step two', status: 'in_progress' },
  88. ])
  89. })
  90. })