agent-disposal.spec.ts 3.7 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091
  1. /** AgentHandle disposal must release blocking browser work before waiting for idle. */
  2. import { Context } from '@deepseek-ai/cordis'
  3. import { mountAgentLoopTestDependencies, mountAgentLoopTestHarness } from '@deepseek-ai/dsh-agent-loop-testkit'
  4. import { SessionId } from '@deepseek-ai/dsh-session'
  5. import { expect, it, vi } from 'vitest'
  6. import { SessionResources } from '../src/index.ts'
  7. it('closes an owned resource during disposed cancellation before the real Agent reaches idle', async () => {
  8. const ctx = new Context()
  9. const entered = Promise.withResolvers<undefined>()
  10. const closing = Promise.withResolvers<undefined>()
  11. const stopped = Promise.withResolvers<undefined>()
  12. const releaseClose = Promise.withResolvers<undefined>()
  13. await mountAgentLoopTestDependencies(ctx)
  14. await mountAgentLoopTestHarness(ctx)
  15. const owner = await ctx.agents.create({ sessionId: SessionId('browser-disposal') })
  16. const resources = new SessionResources(ctx, {
  17. label: 'browser-fixture', exclusive: true,
  18. open: async () => ({
  19. value: {},
  20. async close() { closing.resolve(undefined); stopped.resolve(undefined); await releaseClose.promise },
  21. }),
  22. })
  23. let disposed: Promise<void> | undefined
  24. try {
  25. const operation = owner.agent.runMaintenance(signal => resources.run(owner.agent, signal, async () => {
  26. entered.resolve(undefined)
  27. await stopped.promise
  28. }))
  29. const canceled = expect(operation).rejects.toMatchObject({ kind: 'disposed' })
  30. await entered.promise
  31. disposed = owner.dispose()
  32. await closing.promise
  33. await canceled
  34. expect(resources.available(owner.agent)).toBe(false)
  35. expect(ctx.agents.get(owner.agent.id)).toBe(owner.agent)
  36. releaseClose.resolve(undefined)
  37. await disposed
  38. expect(ctx.agents.get(owner.agent.id)).toBeUndefined()
  39. } finally {
  40. stopped.resolve(undefined)
  41. releaseClose.resolve(undefined)
  42. await disposed
  43. await resources.dispose()
  44. await ctx.fiber.dispose()
  45. }
  46. })
  47. it('disposes an initializing browser after its first caller was already user-canceled', async () => {
  48. const ctx = new Context()
  49. const entered = Promise.withResolvers<undefined>()
  50. const rollback = Promise.withResolvers<undefined>()
  51. let acquisitionAborted = false
  52. await mountAgentLoopTestDependencies(ctx)
  53. await mountAgentLoopTestHarness(ctx)
  54. const owner = await ctx.agents.create({ sessionId: SessionId('browser-initialization-disposal') })
  55. const execute = vi.fn(async () => true)
  56. const resources = new SessionResources(ctx, {
  57. label: 'initializing-browser', exclusive: true,
  58. async open(_agent, signal) {
  59. signal.addEventListener('abort', () => { acquisitionAborted = true }, { once: true })
  60. entered.resolve(undefined)
  61. await rollback.promise
  62. signal.throwIfAborted()
  63. return { value: {}, close: async () => {} }
  64. },
  65. })
  66. let disposing: Promise<void> | undefined
  67. try {
  68. const operation = owner.agent.runMaintenance(signal => resources.run(owner.agent, signal, execute))
  69. const canceled = operation.catch((error: unknown) => error)
  70. await entered.promise
  71. owner.agent.cancel({ kind: 'user' })
  72. let disposed = false
  73. disposing = owner.dispose().then(() => { disposed = true })
  74. await vi.waitFor(() => { expect(acquisitionAborted).toBe(true) })
  75. expect(await canceled).toMatchObject({ cause: { kind: 'user' } })
  76. expect(disposed).toBe(false)
  77. expect(ctx.agents.get(owner.agent.id)).toBe(owner.agent)
  78. rollback.resolve(undefined)
  79. await disposing
  80. expect(execute).not.toHaveBeenCalled()
  81. expect(ctx.agents.get(owner.agent.id)).toBeUndefined()
  82. } finally {
  83. rollback.resolve(undefined)
  84. await resources.dispose()
  85. await disposing
  86. await ctx.fiber.dispose()
  87. }
  88. })