loader-composition.spec.ts 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134
  1. import { mkdtemp, rm, writeFile } from 'node:fs/promises'
  2. import { tmpdir } from 'node:os'
  3. import { join } from 'node:path'
  4. import { pathToFileURL } from 'node:url'
  5. import { afterEach, describe, expect, it } from 'vitest'
  6. import { Context } from 'cordis'
  7. import Loader from '@cordisjs/plugin-loader'
  8. import Include from '@cordisjs/plugin-include'
  9. import type { Agent } from '@deepseek-ai/dsh-agent'
  10. import CommandService from '@deepseek-ai/dsh-commands'
  11. import {
  12. CompactService,
  13. type CompactAgentContext,
  14. type CompactionResult,
  15. type CompactionTrigger,
  16. type ManualCompactAgentContext,
  17. } from '@deepseek-ai/dsh-compact'
  18. import * as commandCompact from '@deepseek-ai/dsh-command-compact'
  19. import { Session, SessionId } from '@deepseek-ai/dsh-session'
  20. const RESULT: CompactionResult = {
  21. startSeq: 1,
  22. summarySeq: 2,
  23. endSeq: 4,
  24. summary: [{ type: 'text', text: 'loader summary' }],
  25. shadowedRange: { start: 3, end: 8 },
  26. shadowedSeqs: [3, 5, 8],
  27. shadowedTokenCount: 99,
  28. }
  29. class LoaderCompactService extends CompactService {
  30. override compactIfNeeded(
  31. _agent: CompactAgentContext,
  32. _trigger: CompactionTrigger,
  33. _signal: AbortSignal,
  34. ): Promise<CompactionResult | null> {
  35. return Promise.resolve(null)
  36. }
  37. override compactRegion(): Promise<CompactionResult> {
  38. return Promise.resolve(RESULT)
  39. }
  40. override compactNow(
  41. _agent: ManualCompactAgentContext,
  42. _signal: AbortSignal,
  43. ): Promise<CompactionResult | null> {
  44. return Promise.resolve(RESULT)
  45. }
  46. }
  47. let root: string | undefined
  48. let context: Context | undefined
  49. afterEach(async () => {
  50. await context?.fiber.dispose()
  51. context = undefined
  52. if (root !== undefined) await rm(root, { recursive: true, force: true })
  53. root = undefined
  54. })
  55. describe('command-compact real Loader composition', () => {
  56. it('discovers and executes /compact through the assembled command plane', async () => {
  57. root = await mkdtemp(join(tmpdir(), 'dsh-command-compact-loader-'))
  58. const configPath = join(root, 'cordis.yml')
  59. await writeFile(configPath, [
  60. "- name: '@deepseek-ai/dsh-commands'",
  61. "- name: '@test/compact-backend'",
  62. "- name: '@deepseek-ai/dsh-command-compact'",
  63. '',
  64. ].join('\n'))
  65. context = new Context()
  66. context.baseUrl = pathToFileURL(root).href + '/'
  67. await context.plugin(Loader)
  68. context.loader.builtins.include = Include
  69. const modules = new Map<string, unknown>([
  70. ['@deepseek-ai/dsh-commands', CommandService],
  71. ['@test/compact-backend', LoaderCompactService],
  72. ['@deepseek-ai/dsh-command-compact', commandCompact],
  73. ])
  74. context.loader.internal = {
  75. version: 'v2',
  76. async import(specifier: string) {
  77. if (!modules.has(specifier)) throw new Error(`unexpected Loader import: ${specifier}`)
  78. return modules.get(specifier)
  79. },
  80. } as unknown as NonNullable<typeof context.loader.internal>
  81. await context.loader.create({
  82. name: 'cordis:include',
  83. config: { path: pathToFileURL(configPath).href },
  84. })
  85. await context.loader.await()
  86. const session = new Session(SessionId('loader-command-compact'))
  87. const agent = {
  88. session,
  89. status: 'idle',
  90. options: {},
  91. reserveTurnAdmission: () => () => undefined,
  92. } as unknown as Agent
  93. expect(context.commands.list(agent)).toContainEqual({
  94. name: 'compact',
  95. description: 'Compact older conversation history',
  96. })
  97. const execution = await context.commands.execute(agent, '/compact', new AbortController().signal)
  98. if (execution === undefined) throw new Error('Loader composition did not resolve /compact')
  99. expect(execution.result).toEqual({
  100. kind: 'success',
  101. text: 'Compacted 3 history items (~99 tokens).',
  102. })
  103. expect(session.events.map(event => ({ type: event.type, data: event.data }))).toEqual([
  104. {
  105. type: 'command/run',
  106. data: {
  107. commandId: execution.commandId,
  108. name: 'compact',
  109. args: '',
  110. source: { kind: 'user' },
  111. },
  112. },
  113. {
  114. type: 'command/done',
  115. data: {
  116. commandId: execution.commandId,
  117. kind: 'success',
  118. text: 'Compacted 3 history items (~99 tokens).',
  119. },
  120. },
  121. ])
  122. expect(session.surface.nodes).toEqual([])
  123. expect(session.deriveMessages()).toEqual([])
  124. })
  125. })