settings.spec.ts 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108
  1. /** The `agent-loop` settings section layered over the composition entry. */
  2. import { describe, expect, it } from 'vitest'
  3. import { Context } from '@deepseek-ai/cordis'
  4. import type { Fiber } from '@deepseek-ai/cordis'
  5. import LlmRuntime from '@deepseek-ai/dsh-llm'
  6. import SessionStore from '@deepseek-ai/dsh-session'
  7. import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
  8. import ToolRuntime from '@deepseek-ai/dsh-tools'
  9. import AgentRegistry from '@deepseek-ai/dsh-agent'
  10. import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
  11. import { SettingsProvider } from '@deepseek-ai/dsh-settings'
  12. import type { SettingsNamespace } from '@deepseek-ai/dsh-settings'
  13. import AgentLoop, { AGENT_LOOP_SETTINGS_NAMESPACE } from '@deepseek-ai/dsh-agent-loop'
  14. /** The smallest real provider: one in-memory document, always writable. */
  15. class MemorySettings extends SettingsProvider {
  16. doc: Record<string, unknown> = {}
  17. get writable(): boolean {
  18. return true
  19. }
  20. protected load(): Promise<Record<string, unknown>> {
  21. return Promise.resolve(structuredClone(this.doc))
  22. }
  23. protected persist(ns: SettingsNamespace, section: Record<string, unknown>): Promise<void> {
  24. this.doc = { ...this.doc, [ns]: structuredClone(section) }
  25. return Promise.resolve()
  26. }
  27. }
  28. async function boot(): Promise<{ ctx: Context; settingsFiber: Fiber; loopFiber: Fiber }> {
  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. const settingsFiber = ctx.plugin(MemorySettings)
  37. await settingsFiber.await()
  38. const loopFiber = ctx.plugin(AgentLoop, { agents: [], maxParallelToolCalls: 4 })
  39. await loopFiber.await()
  40. return { ctx, settingsFiber, loopFiber }
  41. }
  42. describe('agent-loop settings section', () => {
  43. it('layers the stored parallel cap over the composition entry', async () => {
  44. const bench = await boot()
  45. expect(bench.ctx.agentLoop.config.maxParallelToolCalls).toBe(4)
  46. await bench.ctx.settings.update(AGENT_LOOP_SETTINGS_NAMESPACE, { maxParallelToolCalls: 1 })
  47. expect(bench.ctx.agentLoop.config.maxParallelToolCalls).toBe(1)
  48. await bench.ctx.fiber.dispose()
  49. })
  50. it('refuses a non-positive cap at the write', async () => {
  51. const bench = await boot()
  52. await expect(bench.ctx.settings.update(AGENT_LOOP_SETTINGS_NAMESPACE, { maxParallelToolCalls: 0 }))
  53. .rejects.toThrow()
  54. expect(bench.ctx.agentLoop.config.maxParallelToolCalls).toBe(4)
  55. await bench.ctx.fiber.dispose()
  56. })
  57. it('never offers the composed agents array to the settings document', async () => {
  58. const bench = await boot()
  59. const descriptor = bench.ctx.settings.describe().find(row => String(row.ns) === 'agent-loop')
  60. expect(Object.keys(descriptor?.value as object)).toEqual(['maxParallelToolCalls'])
  61. await bench.ctx.fiber.dispose()
  62. })
  63. it('keeps serving the composed agents array to its own consumers', async () => {
  64. const bench = await boot()
  65. await bench.ctx.settings.update(AGENT_LOOP_SETTINGS_NAMESPACE, { maxParallelToolCalls: 2 })
  66. expect(bench.ctx.agentLoop.config.agents).toEqual([])
  67. await bench.ctx.fiber.dispose()
  68. })
  69. it('falls back to the composition entry when the settings provider detaches', async () => {
  70. const bench = await boot()
  71. await bench.ctx.settings.update(AGENT_LOOP_SETTINGS_NAMESPACE, { maxParallelToolCalls: 1 })
  72. expect(bench.ctx.agentLoop.config.maxParallelToolCalls).toBe(1)
  73. await bench.settingsFiber.dispose()
  74. expect(bench.ctx.agentLoop.config.maxParallelToolCalls).toBe(4)
  75. await bench.ctx.fiber.dispose()
  76. })
  77. it('releases the namespace when the service unloads', async () => {
  78. const bench = await boot()
  79. expect(bench.ctx.settings.describe().map(row => String(row.ns))).toContain('agent-loop')
  80. await bench.loopFiber.dispose()
  81. expect(bench.ctx.settings.describe().map(row => String(row.ns))).not.toContain('agent-loop')
  82. await bench.ctx.fiber.dispose()
  83. })
  84. })