tui-agent.spec.ts 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151
  1. import { describe, expect, it } from 'vitest'
  2. import { join } from 'node:path'
  3. import type { Context } from 'cordis'
  4. import Loader from '@cordisjs/plugin-loader'
  5. import { TOOL_ORDER_REST } from '@deepseek-ai/dsh-system-prompt'
  6. import * as tuiAgent from '../src/index.ts'
  7. interface PluginCall {
  8. readonly name: string
  9. readonly config: unknown
  10. }
  11. function recordingContext(): { readonly ctx: Context; readonly calls: PluginCall[] } {
  12. const calls: PluginCall[] = []
  13. const ctx = {
  14. plugin(plugin: { name?: string }, config?: unknown) {
  15. calls.push({ name: plugin.name ?? '', config })
  16. },
  17. } as unknown as Context
  18. return { ctx, calls }
  19. }
  20. describe('dsh-tui-demo app', () => {
  21. it('composes the TUI cluster around one fresh exact session identity', () => {
  22. const { ctx, calls } = recordingContext()
  23. tuiAgent.composeTuiApp(ctx, {
  24. provider: 'mock',
  25. model: 'mock-model',
  26. maxParallelToolCalls: 3,
  27. persona: 'test persona',
  28. toolOrder: ['zulu', TOOL_ORDER_REST],
  29. tools: { mode: 'code' },
  30. dshHome: '/tmp/dsh-home',
  31. persistenceRoot: '/tmp/tui-sessions',
  32. persistenceCompression: 'none',
  33. sessionReferences: {
  34. maxReferences: 2,
  35. candidateLimit: 7,
  36. maxReferenceBytes: 1234,
  37. },
  38. welcome: 'TUI ready',
  39. resumeCommand: 'dsh --resume {session}',
  40. ui: { color: false, maxToolOutputLines: 3 },
  41. skills: { tool: { catalogDescriptionMaxLength: 8 } },
  42. toolBash: { enableRunInBackground: false },
  43. toolTasks: { waitTimeoutMs: 7, maxWaitTimeoutMs: 11 },
  44. workspaceContext: false,
  45. })
  46. expect(calls.map(call => call.name)).toEqual([
  47. 'CommandService',
  48. 'command-goal',
  49. 'SessionPersistenceJsonl',
  50. 'session-checkpoint-policy',
  51. 'SessionQuerySqlite',
  52. 'SessionReferenceService',
  53. 'UserInteractionService',
  54. 'ui-tui',
  55. 'agent-spine-demo',
  56. 'tool-ask-user',
  57. ])
  58. expect(calls[0]?.config).toBeUndefined()
  59. expect(calls[2]?.config).toEqual({ root: '/tmp/tui-sessions', compression: 'none' })
  60. expect(calls[4]?.config).toEqual({ path: join('/tmp/tui-sessions', 'session-query.db') })
  61. expect(calls[5]?.config).toEqual({
  62. maxReferences: 2,
  63. candidateLimit: 7,
  64. maxReferenceBytes: 1234,
  65. })
  66. const tuiConfig = calls[7]?.config as { sessionId: string }
  67. expect(tuiConfig).toMatchObject({
  68. welcome: 'TUI ready',
  69. resumeCommand: 'dsh --resume {session}',
  70. color: false,
  71. maxToolOutputLines: 3,
  72. })
  73. expect(tuiConfig.sessionId).toMatch(/^main-session-[0-9a-f-]{36}$/)
  74. const spineConfig = calls[8]?.config as {
  75. readonly agents: Array<Record<string, unknown>>
  76. readonly goals: Record<string, never>
  77. readonly maxParallelToolCalls: number
  78. readonly persona: string
  79. readonly toolOrder: string[]
  80. readonly tools: { mode: string }
  81. }
  82. expect(spineConfig).toMatchObject({
  83. maxParallelToolCalls: 3,
  84. persona: 'test persona',
  85. toolOrder: ['zulu', TOOL_ORDER_REST],
  86. tools: { mode: 'code' },
  87. goals: {},
  88. })
  89. expect(spineConfig.agents[0]).toMatchObject({
  90. id: 'main',
  91. provider: 'mock',
  92. model: 'mock-model',
  93. cwd: process.cwd(),
  94. sessionId: tuiConfig.sessionId,
  95. })
  96. })
  97. it('resumes the configured session and applies runtime defaults', () => {
  98. const { ctx, calls } = recordingContext()
  99. tuiAgent.composeTuiApp(ctx, {
  100. provider: 'mock',
  101. model: 'mock-model',
  102. resumeSessionId: 'persisted-session',
  103. workspaceContext: false,
  104. })
  105. expect(calls[2]?.config).toEqual({ root: './.sessions' })
  106. expect(calls[5]?.config).toEqual({})
  107. // No configured welcome forwards none: the TUI banner sweeps in without a subtitle.
  108. expect(calls[7]?.config).toEqual({ sessionId: 'persisted-session' })
  109. expect((calls[8]?.config as { agents: Array<Record<string, unknown>> }).agents[0]).toMatchObject({
  110. id: 'main',
  111. resumeSessionId: 'persisted-session',
  112. })
  113. })
  114. it('normalizes an empty resume id and routes apply through the same composition', () => {
  115. const { ctx, calls } = recordingContext()
  116. tuiAgent.apply(ctx, {
  117. provider: 'mock',
  118. model: 'mock-model',
  119. resumeSessionId: '',
  120. goals: false,
  121. workspaceContext: false,
  122. })
  123. const tuiConfig = calls[6]?.config as { sessionId: string }
  124. expect(tuiConfig.sessionId).toMatch(/^main-session-[0-9a-f-]{36}$/)
  125. expect((calls[7]?.config as { agents: Array<Record<string, unknown>> }).agents[0])
  126. .toMatchObject({ sessionId: tuiConfig.sessionId })
  127. expect(calls.map(call => call.name)).not.toContain('command-goal')
  128. expect(calls[7]?.config).toMatchObject({ goals: false })
  129. })
  130. it('has the namespace-plugin export shape so the Loader keeps its schema', () => {
  131. expect(tuiAgent.name).toBe('tui-demo')
  132. expect(tuiAgent.Config).toBeDefined()
  133. expect('default' in tuiAgent).toBe(false)
  134. expect(typeof tuiAgent.apply).toBe('function')
  135. const loader = Object.create(Loader.prototype) as Loader
  136. const unwrapped = loader.unwrapExports(tuiAgent) as Record<string, unknown>
  137. expect(unwrapped).toBe(tuiAgent)
  138. expect(unwrapped.name).toBe('tui-demo')
  139. expect(unwrapped.Config).toBeDefined()
  140. })
  141. })