agent-execution.spec.ts 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162
  1. import { describe, expect, it } from 'vitest'
  2. import { Context } from 'cordis'
  3. import { runInNewContext } from 'node:vm'
  4. import { AgentId, type Agent } from '@deepseek-ai/dsh-agent'
  5. import AgentExecutionProvider from '@deepseek-ai/dsh-agent-execution'
  6. import type { AgentExecution, AgentExecutionService } from '@deepseek-ai/dsh-agent-execution'
  7. function execution(id: string): AgentExecution {
  8. return { agent: { id: AgentId(id) } as Agent }
  9. }
  10. async function harness(): Promise<{
  11. ctx: Context
  12. service: AgentExecutionService
  13. dispose: () => Promise<void>
  14. }> {
  15. const ctx = new Context()
  16. const fiber = await ctx.plugin(AgentExecutionProvider)
  17. return {
  18. ctx,
  19. service: ctx.agentExecution,
  20. dispose: fiber.dispose,
  21. }
  22. }
  23. describe('AgentExecutionProvider', () => {
  24. it('reports an absent boundary and requires an active execution', async () => {
  25. const { service, dispose } = await harness()
  26. expect(service.current()).toBeUndefined()
  27. expect(() => service.require()).toThrow('no agent execution context is active')
  28. await dispose()
  29. })
  30. it('preserves exact synchronous and Promise return identities across await', async () => {
  31. const { service, dispose } = await harness()
  32. const active = execution('identity')
  33. const value = { result: true }
  34. expect(service.run(active, () => {
  35. expect(service.require()).toBe(active)
  36. return value
  37. })).toBe(value)
  38. const promise = service.run(active, async () => {
  39. expect(service.require()).toBe(active)
  40. await Promise.resolve()
  41. expect(service.require()).toBe(active)
  42. return value
  43. })
  44. expect(service.run(active, () => promise)).toBe(promise)
  45. await expect(promise).resolves.toBe(value)
  46. expect(service.current()).toBeUndefined()
  47. await dispose()
  48. })
  49. it('isolates overlapping executions', async () => {
  50. const { service, dispose } = await harness()
  51. const a = execution('a')
  52. const b = execution('b')
  53. const bothStarted = Promise.withResolvers<boolean>()
  54. const release = Promise.withResolvers<boolean>()
  55. let starts = 0
  56. const run = (active: AgentExecution): Promise<void> => service.run(active, async () => {
  57. expect(service.require()).toBe(active)
  58. starts += 1
  59. if (starts === 2) bothStarted.resolve(true)
  60. await release.promise
  61. expect(service.require()).toBe(active)
  62. })
  63. const pending = [run(a), run(b)]
  64. await bothStarted.promise
  65. expect(service.current()).toBeUndefined()
  66. release.resolve(true)
  67. await Promise.all(pending)
  68. await dispose()
  69. })
  70. it('restores nested and explicitly cleared boundaries', async () => {
  71. const { service, dispose } = await harness()
  72. const parent = execution('parent')
  73. const child = execution('child')
  74. service.run(parent, () => {
  75. expect(service.require()).toBe(parent)
  76. service.run(child, () => { expect(service.require()).toBe(child) })
  77. expect(service.require()).toBe(parent)
  78. service.run(undefined, () => {
  79. expect(service.current()).toBeUndefined()
  80. expect(() => service.require()).toThrow('no agent execution context is active')
  81. })
  82. expect(service.require()).toBe(parent)
  83. })
  84. expect(service.current()).toBeUndefined()
  85. await dispose()
  86. })
  87. it('restores context after synchronous throws and rejected operations', async () => {
  88. const { service, dispose } = await harness()
  89. const parent = execution('parent')
  90. const child = execution('child')
  91. const syncError = new Error('sync failure')
  92. const asyncError = new Error('async failure')
  93. service.run(parent, () => {
  94. expect(() => service.run(child, () => { throw syncError })).toThrow(syncError)
  95. expect(service.require()).toBe(parent)
  96. })
  97. await expect(service.run(child, async () => {
  98. await Promise.resolve()
  99. throw asyncError
  100. })).rejects.toBe(asyncError)
  101. expect(service.current()).toBeUndefined()
  102. await dispose()
  103. })
  104. it('stops new boundaries, drains active Promises, and invalidates retained references', async () => {
  105. const { ctx, service, dispose } = await harness()
  106. const active = execution('draining')
  107. const release = Promise.withResolvers<boolean>()
  108. const pending = service.run(active, async () => {
  109. await release.promise
  110. expect(service.require()).toBe(active)
  111. })
  112. let disposed = false
  113. const disposal = dispose().then(() => { disposed = true })
  114. await Promise.resolve()
  115. expect(() => service.run(active, () => 1)).toThrow('agent execution service is disposed')
  116. expect(disposed).toBe(false)
  117. expect(ctx.get('agentExecution')).toBeUndefined()
  118. release.resolve(true)
  119. await pending
  120. await disposal
  121. expect(() => service.current()).toThrow('agent execution service is disposed')
  122. expect(() => service.require()).toThrow('agent execution service is disposed')
  123. })
  124. it('drains cross-realm Promise boundaries before disposal', async () => {
  125. const { service, dispose } = await harness()
  126. const active = execution('cross-realm')
  127. const release = Promise.withResolvers<boolean>()
  128. const operation = runInNewContext(
  129. '(async () => { await release; inspect() })',
  130. {
  131. release: release.promise,
  132. inspect: () => { expect(service.require()).toBe(active) },
  133. },
  134. ) as () => Promise<void>
  135. const pending = service.run(active, operation)
  136. expect(pending).not.toBeInstanceOf(Promise)
  137. let disposed = false
  138. const disposal = dispose().then(() => { disposed = true })
  139. await Promise.resolve()
  140. expect(disposed).toBe(false)
  141. release.resolve(true)
  142. await pending
  143. await disposal
  144. expect(disposed).toBe(true)
  145. })
  146. })