shutdown-drain.spec.ts 3.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869
  1. /** Root-fiber shutdown drains buffered session events durably (both mount orders). */
  2. import { describe, expect, it, afterEach } from 'vitest'
  3. import { Context } from '@deepseek-ai/cordis'
  4. import { mkdtemp, rm } from 'node:fs/promises'
  5. import { tmpdir } from 'node:os'
  6. import { join } from 'node:path'
  7. import { createUserMessage } from '@deepseek-ai/dsh-llm'
  8. import LlmRuntime from '@deepseek-ai/dsh-llm'
  9. import SessionStore, { SessionId } from '@deepseek-ai/dsh-session'
  10. import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
  11. import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
  12. import ToolRuntime from '@deepseek-ai/dsh-tools'
  13. import AgentRegistry, { type Agent } from '@deepseek-ai/dsh-agent'
  14. import JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
  15. import AgentLoop from '@deepseek-ai/dsh-agent-loop'
  16. import { MockAdapter, textResponse } from './mock-adapter.ts'
  17. const dirs: string[] = []
  18. afterEach(async () => { for (const d of dirs.splice(0)) await rm(d, { recursive: true, force: true }) })
  19. function waitForIdle(ctx: Context, agent: Agent): Promise<void> {
  20. return new Promise((resolve) => {
  21. const dispose = ctx.on('agent/status', ({ agent: subject, status }) => {
  22. if (subject === agent && status === 'idle') { dispose(); resolve() }
  23. })
  24. })
  25. }
  26. async function mount(order: 'backend-first' | 'loop-first'): Promise<{ ctx: Context; root: string }> {
  27. const root = await mkdtemp(join(tmpdir(), 'dsh-shutdown-drain-'))
  28. dirs.push(root)
  29. const ctx = new Context()
  30. await ctx.plugin(LlmRuntime)
  31. await ctx.plugin(SessionStore)
  32. await ctx.plugin(SessionProjectionRegistry)
  33. await ctx.plugin(SystemPrompt)
  34. await ctx.plugin(ToolRuntime)
  35. await ctx.plugin(AgentRegistry)
  36. if (order === 'backend-first') {
  37. await ctx.plugin(JsonlSessionPersistence, { root })
  38. await ctx.plugin(AgentLoop, { agents: [] })
  39. } else {
  40. await ctx.plugin(AgentLoop, { agents: [] })
  41. await ctx.plugin(JsonlSessionPersistence, { root })
  42. }
  43. ctx.llm.registerAdapter(['mock'], new MockAdapter([textResponse('done')]))
  44. return { ctx, root }
  45. }
  46. describe.each(['backend-first', 'loop-first'] as const)('root shutdown drain (%s)', (order) => {
  47. it('persists buffered turn events without an explicit flush before dispose', async () => {
  48. const { ctx, root } = await mount(order)
  49. const sessionId = SessionId('shutdown-drain')
  50. const handle = await ctx.agents.create({ sessionId, agentOptions: { provider: 'mock', model: 'mock' } })
  51. handle.agent.followup(createUserMessage({ content: [{ type: 'text', text: 'q' }], source: { kind: 'user' } }))
  52. await waitForIdle(ctx, handle.agent)
  53. // No explicit flush and no agent dispose: root teardown must drain.
  54. await ctx.fiber.dispose()
  55. const verify = new Context()
  56. await verify.plugin(JsonlSessionPersistence, { root })
  57. const reader = await verify.sessionPersistence.open(sessionId, 'read')
  58. const { events } = await reader.read()
  59. await reader.close()
  60. expect(events.at(-1)).toMatchObject({ type: 'turn/end', data: { reason: { kind: 'completed' } } })
  61. await verify.fiber.dispose()
  62. })
  63. })