compact.spec.ts 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119
  1. import { describe, expect, it } from 'vitest'
  2. import { Context } from 'cordis'
  3. import { CompactService } from '@deepseek-ai/dsh-compact'
  4. import type { CompactionResult, CompactionTrigger } from '@deepseek-ai/dsh-compact'
  5. import { Session, SessionId } from '@deepseek-ai/dsh-session'
  6. import type { CompactAgentContext } from '@deepseek-ai/dsh-compact'
  7. /**
  8. * A trivial concrete CompactService implementing the abstract contract. The
  9. * interface package owns no algorithm — these tests exercise the seam itself:
  10. * service registration, the abstract method shape, and the `compact/*` event
  11. * declaration merge.
  12. */
  13. class StubCompactService extends CompactService {
  14. /** Records the signal handed to the most recent call, to prove it threads through. */
  15. lastSignal: AbortSignal | undefined
  16. override async compactIfNeeded(
  17. _agent: CompactAgentContext,
  18. _trigger: CompactionTrigger,
  19. signal: AbortSignal,
  20. ): Promise<CompactionResult | null> {
  21. this.lastSignal = signal
  22. return null
  23. }
  24. override async compactRegion(
  25. start: number,
  26. end: number,
  27. agent: CompactAgentContext,
  28. signal?: AbortSignal,
  29. ): Promise<CompactionResult> {
  30. this.lastSignal = signal
  31. const session = agent.session
  32. const summary = [{ type: 'text' as const, text: 'stub' }]
  33. // Minimal stub honoring the lock + log-only event contract.
  34. const startEvent = session.append('compact/start', { turn: 0 })
  35. const summaryEvent = session.append('compact/summary', {
  36. summary,
  37. shadowedRange: { start, end },
  38. shadowedSeqs: [],
  39. shadowedTokenCount: 0,
  40. provider: 'mock',
  41. model: 'stub',
  42. })
  43. const endEvent = session.append('compact/end', { turn: 0 })
  44. return {
  45. startSeq: startEvent.seq,
  46. summarySeq: summaryEvent.seq,
  47. endSeq: endEvent.seq,
  48. summary,
  49. shadowedRange: { start, end },
  50. shadowedSeqs: [],
  51. shadowedTokenCount: 0,
  52. }
  53. }
  54. }
  55. describe('CompactService seam', () => {
  56. function stubAgent(session: Session, model?: string): CompactAgentContext {
  57. return { session, options: model === undefined ? {} : { model } }
  58. }
  59. it('registers as ctx.compact', () => {
  60. const ctx = new Context()
  61. void new StubCompactService(ctx)
  62. expect(ctx.compact).toBeDefined()
  63. expect(ctx.compact).toBeInstanceOf(StubCompactService)
  64. })
  65. it('disposing the fiber unregisters ctx.compact (HMR safety)', async () => {
  66. const ctx = new Context()
  67. const fiber = await ctx.plugin(StubCompactService)
  68. expect(ctx.compact).toBeInstanceOf(StubCompactService)
  69. await fiber.dispose()
  70. expect(ctx.compact).toBeUndefined()
  71. })
  72. it('exposes the abstract contract methods', async () => {
  73. const ctx = new Context()
  74. const svc = new StubCompactService(ctx)
  75. const session = new Session(SessionId('s'))
  76. expect(await svc.compactIfNeeded(stubAgent(session), 'pressure', new AbortController().signal)).toBeNull()
  77. })
  78. it('compact/* events merge into SessionEventMap and are log-only', async () => {
  79. const ctx = new Context()
  80. const svc = new StubCompactService(ctx)
  81. const session = new Session(SessionId('s'))
  82. const result = await svc.compactRegion(0, 0, stubAgent(session, 'm'))
  83. const startEvent = session.events.find(e => e.type === 'compact/start')
  84. expect(startEvent).toBeDefined()
  85. // Log-only: the compiler rejects surfaceOp on compact/* (not a SurfaceEventType);
  86. // verify the runtime value is absent.
  87. const raw = startEvent as unknown as { surfaceOp?: unknown }
  88. expect(raw.surfaceOp).toBeUndefined()
  89. expect(result.summary).toEqual([{ type: 'text', text: 'stub' }])
  90. expect(result.summarySeq).toBeGreaterThan(result.startSeq)
  91. expect(result.endSeq).toBeGreaterThan(result.summarySeq)
  92. expect(result.shadowedRange).toEqual({ start: 0, end: 0 })
  93. expect(session.events.filter(e => e.type.startsWith('compact/')).map(e => e.type))
  94. .toEqual(['compact/start', 'compact/summary', 'compact/end'])
  95. })
  96. it('threads the cancellation signal through to the backend', async () => {
  97. const ctx = new Context()
  98. const svc = new StubCompactService(ctx)
  99. const session = new Session(SessionId('s'))
  100. const controller = new AbortController()
  101. await svc.compactRegion(0, 0, stubAgent(session, 'm'), controller.signal)
  102. expect(svc.lastSignal).toBe(controller.signal)
  103. await svc.compactIfNeeded(stubAgent(session), 'context-overflow', controller.signal)
  104. expect(svc.lastSignal).toBe(controller.signal)
  105. })
  106. })