agent-loop-testkit.spec.ts 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108
  1. import { describe, expect, it } from 'vitest'
  2. import { Context } from '@deepseek-ai/cordis'
  3. import { createUserMessage } from '@deepseek-ai/dsh-llm'
  4. import { Session, SessionId, SessionSeq } from '@deepseek-ai/dsh-session'
  5. import { renderPrompt } from '@deepseek-ai/dsh-system-prompt'
  6. import {
  7. createInboxStub,
  8. mountAgentLoopTestDependencies,
  9. mountAgentLoopTestHarness,
  10. unsupportedInbox,
  11. } from '../src/index.ts'
  12. function message(text: string) {
  13. return createUserMessage({ content: [{ type: 'text', text }], source: { kind: 'user' } })
  14. }
  15. describe('dsh-agent-loop-testkit', () => {
  16. it('rejects mutations through an unsupported Agent stub Inbox', () => {
  17. const inbox = unsupportedInbox()
  18. expect(inbox.nextTurn).toEqual([])
  19. expect(inbox.nextStep).toEqual([])
  20. expect(() => { inbox.clear() }).toThrow('this test Agent does not support Inbox mutations')
  21. })
  22. it('mounts a configurable prerequisite spine and the production AgentLoop', async () => {
  23. const ctx = new Context()
  24. await mountAgentLoopTestDependencies(ctx, {
  25. systemPrompt: { personaPrefix: 'Test persona.' },
  26. tools: { mode: 'native' },
  27. })
  28. expect(renderPrompt(await ctx.systemPrompt.assemble())).toContain('Test persona.')
  29. await expect(mountAgentLoopTestHarness(ctx)).resolves.toBeDefined()
  30. await ctx.fiber.dispose()
  31. })
  32. it('provides a mutable in-memory Inbox stub for structural Agent tests', () => {
  33. const inbox = createInboxStub()
  34. const firstTurn = message('first turn')
  35. const secondTurn = message('second turn')
  36. const firstStep = message('first step')
  37. const editedTurn = message('edited turn')
  38. const editedStep = message('edited step')
  39. inbox.append('next-turn', firstTurn)
  40. inbox.prepend('next-turn', secondTurn)
  41. inbox.append('next-step', firstStep)
  42. expect(inbox.nextTurn).toEqual([secondTurn, firstTurn])
  43. expect(inbox.nextStep).toEqual([firstStep])
  44. expect(inbox.replace(firstTurn.id, editedTurn)).toBe(true)
  45. expect(inbox.replace(firstStep.id, editedStep)).toBe(true)
  46. expect(inbox.replace(firstTurn.id, message('missing replacement'))).toBe(false)
  47. expect(inbox.remove(firstTurn.id)).toBe(false)
  48. expect(inbox.splice('next-turn', -1, 1, [])).toEqual([editedTurn])
  49. expect(inbox.remove(editedStep.id)).toBe(true)
  50. inbox.clear()
  51. expect(inbox.nextTurn).toEqual([])
  52. expect(inbox.nextStep).toEqual([])
  53. })
  54. it('drives durable Inbox behavior through a production Agent', async () => {
  55. const ctx = new Context()
  56. await mountAgentLoopTestDependencies(ctx)
  57. const harness = await mountAgentLoopTestHarness(ctx)
  58. const agent = await harness.create(SessionId('agent-loop-testkit-inbox'))
  59. const turn = message('turn')
  60. const step = message('step')
  61. const inserted: string[] = []
  62. const claimed: Array<{ id: string; turn: number }> = []
  63. ctx.on('agent/inbox/inserted', ({ agent: subject, message: pending }) => {
  64. if (subject === agent) inserted.push(pending.id)
  65. })
  66. ctx.on('agent/inbox/claimed', ({ agent: subject, message: pending, turn: ownerTurn }) => {
  67. if (subject === agent) claimed.push({ id: pending.id, turn: ownerTurn })
  68. })
  69. agent.inbox.append('next-turn', turn)
  70. agent.inbox.append('next-step', step)
  71. expect(inserted).toEqual([turn.id, step.id])
  72. expect(() => { agent.inbox.append('next-step', turn) }).toThrow(`message "${turn.id}" is already pending`)
  73. const invalid = Session.create(SessionId('invalid-persisted-inbox'), [{
  74. type: 'agent/inbox/spliced',
  75. seq: SessionSeq(0),
  76. time: 1,
  77. data: { target: 'next-turn', start: 99, inserted: [] },
  78. }])
  79. expect(() => ctx.sessionProjections.stateOf(invalid, 'inbox'))
  80. .toThrow(/invalid persisted inbox splice/)
  81. expect(harness.claim(agent, 'next-turn', 3)).toEqual([step, turn])
  82. expect(claimed).toEqual([
  83. { id: step.id, turn: 3 },
  84. { id: turn.id, turn: 3 },
  85. ])
  86. expect(agent.session.snapshotEvents().map(event => event.type)).toEqual([
  87. 'agent/inbox/spliced',
  88. 'agent/inbox/spliced',
  89. 'agent/inbox/spliced',
  90. 'agent/inbox/spliced',
  91. ])
  92. await ctx.fiber.dispose()
  93. })
  94. })