plugin.spec.ts 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105
  1. import { describe, expect, it } from 'vitest'
  2. import { Context, Service } from '@deepseek-ai/cordis'
  3. import Loader from '@deepseek-ai/cordis-plugin-loader'
  4. import { agentEvents } from '@deepseek-ai/dsh-agent'
  5. import AgentLoop from '@deepseek-ai/dsh-agent-loop'
  6. import { mountAgentLoopTestDependencies } from '@deepseek-ai/dsh-agent-loop-testkit'
  7. import { CallId } from '@deepseek-ai/dsh-llm'
  8. import { SessionId } from '@deepseek-ai/dsh-session'
  9. import * as toolSchedule from '../src/index.ts'
  10. class PersistenceProbe extends Service {
  11. constructor(ctx: Context) {
  12. super(ctx, 'sessionPersistence')
  13. }
  14. }
  15. async function harness(): Promise<Context> {
  16. const ctx = new Context()
  17. await mountAgentLoopTestDependencies(ctx)
  18. await ctx.plugin(PersistenceProbe)
  19. ctx.on('session/flush', () => {})
  20. await ctx.plugin(AgentLoop, { agents: [] })
  21. return ctx
  22. }
  23. async function settle(): Promise<void> {
  24. for (let index = 0; index < 8; index += 1) await Promise.resolve()
  25. }
  26. describe('Schedule plugin composition', () => {
  27. it('has the Loader-safe function-plugin export shape', () => {
  28. expect('default' in toolSchedule).toBe(false)
  29. expect(toolSchedule.name).toBe('tool-schedule')
  30. expect(toolSchedule.inject).toEqual(['agents', 'sessions', 'tools', 'sessionPersistence'])
  31. const loader = Object.create(Loader.prototype) as Loader
  32. expect(loader.unwrapExports(toolSchedule)).toBe(toolSchedule)
  33. })
  34. it('installs only on future root agents and unwinds on plugin disposal', async () => {
  35. const ctx = await harness()
  36. const existing = await ctx.agents.create({ sessionId: SessionId('schedule-existing') })
  37. const plugin = await ctx.plugin(toolSchedule)
  38. expect(ctx.tools.get('schedule_create', existing.agent)).toBeUndefined()
  39. expect(ctx.tools.get('schedule_create')).toBeUndefined()
  40. const root = await ctx.agents.create({ sessionId: SessionId('schedule-root') })
  41. expect(ctx.tools.get('schedule_create', root.agent)?.name).toBe('schedule_create')
  42. expect(ctx.tools.get('schedule_list', root.agent)?.name).toBe('schedule_list')
  43. expect(ctx.tools.get('schedule_delete', root.agent)?.name).toBe('schedule_delete')
  44. expect(ctx.tools.get('schedule_create')).toBeUndefined()
  45. const created = await ctx.agents.withInitiator(root.agent, () => ctx.tools.execute({
  46. signal: new AbortController().signal,
  47. callId: CallId('schedule-plugin-create'),
  48. name: 'schedule_create',
  49. arguments: { prompt: 'future reminder', after_seconds: 3_600 },
  50. agent: root.agent,
  51. }))
  52. expect(created.isError).toBe(false)
  53. if (created.isError) throw new Error('expected Schedule create value')
  54. expect(created.value).toMatchObject({ id: 'schedule-1', deliveryMode: 'session-local' })
  55. agentEvents(ctx, root.agent).emit('agent/status', { status: 'running' })
  56. agentEvents(ctx, root.agent).emit('agent/status', { status: 'idle' })
  57. const child = await root.agent.ctx.agents.create({ sessionId: SessionId('schedule-child') })
  58. expect(ctx.agents.roots()).toEqual([existing.agent, root.agent])
  59. expect(ctx.tools.get('schedule_create', child.agent)).toBeUndefined()
  60. const departing = await ctx.agents.create({ sessionId: SessionId('schedule-departing') })
  61. expect(ctx.tools.get('schedule_create', departing.agent)).toBeDefined()
  62. await departing.dispose()
  63. expect(ctx.tools.get('schedule_create', departing.agent)).toBeUndefined()
  64. await plugin.dispose()
  65. expect(ctx.tools.get('schedule_create', root.agent)).toBeUndefined()
  66. expect(ctx.tools.get('schedule_list', root.agent)).toBeUndefined()
  67. expect(ctx.tools.get('schedule_delete', root.agent)).toBeUndefined()
  68. await child.dispose()
  69. await root.dispose()
  70. await existing.dispose()
  71. await ctx.fiber.dispose()
  72. })
  73. it('does not checkpoint unrelated idle sessions', async () => {
  74. const ctx = await harness()
  75. const plugin = await ctx.plugin(toolSchedule)
  76. const root = await ctx.agents.create({ sessionId: SessionId('schedule-unrelated-idle') })
  77. await settle()
  78. let flushes = 0
  79. const stopFlush = ctx.on('session/flush', (session) => {
  80. if (session === root.agent.session) flushes += 1
  81. })
  82. agentEvents(ctx, root.agent).emit('agent/status', { status: 'running' })
  83. agentEvents(ctx, root.agent).emit('agent/status', { status: 'idle' })
  84. await settle()
  85. expect(flushes).toBe(0)
  86. stopFlush()
  87. await root.dispose()
  88. await plugin.dispose()
  89. await ctx.fiber.dispose()
  90. })
  91. })