integration.spec.ts 4.1 KB

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