File size: 10,639 Bytes
d04f74a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
import { describe, expect, it } from 'vitest'
import { Context } from '@deepseek-ai/cordis'
import Loader from '@deepseek-ai/cordis-plugin-loader'
import type { Agent } from '@deepseek-ai/dsh-agent'
import CommandRuntime, { type CommandResult } from '@deepseek-ai/dsh-commands'
import {
  CompactionId,
  CompactionEngine,
  ManualCompactionError,
  type CompactionAgentContext,
  type CompactionResult,
  type CompactionTrigger,
  type ManualCompactAgentContext,
} from '@deepseek-ai/dsh-compaction'
import { Session, SessionId, SessionSeq } from '@deepseek-ai/dsh-session'
import * as commandCompact from '@deepseek-ai/dsh-command-compact'

const COMPACTION_ID = CompactionId('command-compact-test')

const RESULT: CompactionResult = {
  compactionId: COMPACTION_ID,
  startSeq: SessionSeq(1),
  summarySeq: SessionSeq(2),
  endSeq: SessionSeq(3),
  summary: [{ type: 'text', text: 'summary' }],
  shadowedRange: { start: SessionSeq(1), end: SessionSeq(7) },
  shadowedSeqs: [SessionSeq(1), SessionSeq(3), SessionSeq(7)],
  shadowedTokenCount: 42,
}

class StubCompactionEngine extends CompactionEngine {
  result: CompactionResult | null = RESULT
  failure: unknown
  operation: (() => Promise<CompactionResult | null>) | undefined
  calls: { agent: ManualCompactAgentContext; signal: AbortSignal }[] = []

  override compactIfNeeded(
    _agent: CompactionAgentContext,
    _trigger: CompactionTrigger,
    _signal: AbortSignal,
  ): Promise<CompactionResult | null> {
    return Promise.resolve(null)
  }

  override compactRegion(): Promise<CompactionResult> {
    return Promise.resolve(RESULT)
  }

  override compactNow(
    agent: ManualCompactAgentContext,
    signal: AbortSignal,
    sourceCommandId?: Parameters<CompactionEngine['compactNow']>[2],
  ): Promise<CompactionResult | null> {
    this.calls.push({ agent, signal })
    if (this.operation !== undefined) return this.operation()
    return this.failure === undefined
      ? Promise.resolve(this.result === null ? null : this.appendResult(agent, this.result, sourceCommandId))
      // oxlint-disable-next-line typescript/prefer-promise-reject-errors -- exercise arbitrary backend rejection values.
      : Promise.reject(this.failure)
  }

  private appendResult(
    agent: ManualCompactAgentContext,
    result: CompactionResult,
    sourceCommandId: Parameters<CompactionEngine['compactNow']>[2],
  ): CompactionResult {
    const operationIds = {
      compactionId: result.compactionId,
      ...sourceCommandId === undefined ? {} : { sourceCommandId },
    }
    agent.session.append('compaction/start', { ...operationIds, turn: null })
    agent.session.append('compaction/summary', {
      ...operationIds,
      summary: result.summary,
      shadowedRange: result.shadowedRange,
      shadowedSeqs: result.shadowedSeqs,
      shadowedTokenCount: result.shadowedTokenCount,
      provider: 'command-test',
      model: 'command-test',
    })
    agent.session.append('compaction/end', { ...operationIds, turn: null })
    return { ...result, ...operationIds }
  }
}

interface Harness {
  readonly ctx: Context
  readonly compact: StubCompactionEngine
  readonly agent: Agent
  readonly plugin: Awaited<ReturnType<Context['plugin']>>
}

async function harness(): Promise<Harness> {
  const ctx = new Context()
  await ctx.plugin(CommandRuntime)
  const compact = new StubCompactionEngine(ctx)
  const plugin = await ctx.plugin(commandCompact)
  const session = Session.create(SessionId('command-compact'))
  const agent = {
    session,
    status: 'idle',
    options: {},
    reserveTurnAdmission: () => () => undefined,
  } as unknown as Agent
  return { ctx, compact, agent, plugin }
}

async function run(
  test: Harness,
  suffix = '',
  controller = new AbortController(),
): Promise<NonNullable<Awaited<ReturnType<CommandRuntime['execute']>>>> {
  const execution = await test.ctx.commands.execute(test.agent, `/compact${suffix}`, [], controller.signal)
  if (execution === undefined) throw new Error('compact command was not registered')
  return execution
}

/** Assert the executor-owned lifecycle pair and absence from model history. */
function expectLastLifecycle(
  test: Harness,
  args: string,
  outcome: CommandResult,
): string {
  const lifecycle = test.agent.session.snapshotEvents()
    .filter(event => event.type === 'command/run' || event.type === 'command/done')
    .slice(-2)
  const runEvent = lifecycle[0]
  const doneEvent = lifecycle[1]
  if (runEvent?.type !== 'command/run' || doneEvent?.type !== 'command/done') {
    throw new Error(`expected command lifecycle pair, got ${lifecycle.map(event => event.type).join(',')}`)
  }
  expect(lifecycle.map(event => ({ type: event.type, data: event.data }))).toEqual([
    {
      type: 'command/run',
      data: {
        commandId: runEvent.data.commandId,
        name: 'compact',
        args,
        source: { kind: 'user' },
      },
    },
    {
      type: 'command/done',
      data: {
        commandId: runEvent.data.commandId,
        ...outcome,
      },
    },
  ])
  expect(doneEvent.data.commandId).toBe(runEvent.data.commandId)
  expect(test.agent.session.surface.nodes).toEqual([])
  expect(test.agent.session.deriveMessages()).toEqual([])
  return runEvent.data.commandId
}

describe('@deepseek-ai/dsh-command-compact registration', () => {
  it('registers one argument-free command with Loader-safe exports and disposes it', async () => {
    const test = await harness()
    expect(commandCompact.name).toBe('command-compact')
    expect(commandCompact.inject).toEqual(['commands', 'compaction'])
    expect('default' in commandCompact).toBe(false)
    const loader = Object.create(Loader.prototype) as Loader
    expect(loader.unwrapExports(commandCompact)).toBe(commandCompact)
    expect(test.ctx.commands.list(test.agent)).toContainEqual({
      definitionId: '@deepseek-ai/dsh-command-compact',
      name: 'compact',
      description: 'Compact older conversation history',
    })

    await test.plugin.dispose()
    expect(test.ctx.commands.find(test.agent, 'compact')).toBeUndefined()
  })
})

describe('/compact human command', () => {
  it('reports success with useful accounting and forwards the exact target and signal', async () => {
    const test = await harness()
    const controller = new AbortController()
    const execution = await run(test, '', controller)
    expect(execution.result).toEqual({
      kind: 'success',
      text: 'Compacted 3 history items (~42 tokens).',
      sourceEventSeq: RESULT.summarySeq,
    })
    expect(execution.commandId).toBe(expectLastLifecycle(test, '', execution.result))
    expect(test.compact.calls).toEqual([{ agent: test.agent, signal: controller.signal }])
  })

  it('returns direct no-history and argument-rejection results', async () => {
    const test = await harness()
    test.compact.result = null
    const empty = await run(test)
    expect(empty.result).toEqual({
      kind: 'success',
      text: 'No compactable history yet.',
    })
    expect(empty.commandId).toBe(expectLastLifecycle(test, '', empty.result))

    const rejected = await run(test, ' now')
    expect(rejected.result).toEqual({
      kind: 'error',
      text: 'Usage: /compact (no arguments)',
    })
    expect(rejected.commandId).toBe(expectLastLifecycle(test, ' now', rejected.result))
    expect(test.compact.calls).toHaveLength(1)
  })

  it.each([
    ['busy', 'Compaction is unavailable because this process has an active compaction, or the agent is not idle.'],
    ['cancelled', 'Compaction cancelled.'],
    ['changed', 'The history selected for compaction changed before it could be replaced. The attempt is recorded in the session log.'],
    ['summary', 'Compaction could not produce a useful summary. The attempt is recorded in the session log.'],
    ['commit', 'Compaction did not finish cleanly; some session history may have changed. Inspect the current session state before retrying.'],
    ['persistence', 'Compaction finished, but the session could not be saved.'],
  ] as const)('maps expected %s failures to direct errors', async (code, text) => {
    const test = await harness()
    test.compact.failure = new ManualCompactionError(code, 'backend detail')
    const execution = await run(test)
    expect(execution.result).toEqual({ kind: 'error', text })
    expect(execution.commandId).toBe(expectLastLifecycle(test, '', execution.result))
  })

  it('preserves cancellation and unexpected implementation failures', async () => {
    const cancelled = await harness()
    const controller = new AbortController()
    const abort = new Error('operator cancelled')
    cancelled.compact.operation = () => {
      controller.abort(abort)
      return Promise.reject(new ManualCompactionError('summary', 'late failure'))
    }
    await expect(run(cancelled, '', controller)).rejects.toBe(abort)
    expectLastLifecycle(cancelled, '', { kind: 'error', text: abort.message })

    const unexpected = await harness()
    const bug = new Error('unexpected backend bug')
    unexpected.compact.failure = bug
    await expect(run(unexpected)).rejects.toBe(bug)
    expectLastLifecycle(unexpected, '', { kind: 'error', text: bug.message })
  })

  it('drains an aborted handler through close and flush before plugin disposal settles', async () => {
    const test = await harness()
    const controller = new AbortController()
    const abort = new Error('operator cancelled')
    const started = Promise.withResolvers<undefined>()
    const allowClose = Promise.withResolvers<undefined>()
    const closed = Promise.withResolvers<undefined>()
    const allowFlush = Promise.withResolvers<undefined>()
    const flushed = Promise.withResolvers<undefined>()
    test.compact.operation = async () => {
      started.resolve(undefined)
      await allowClose.promise
      closed.resolve(undefined)
      await allowFlush.promise
      flushed.resolve(undefined)
      throw abort
    }

    const execution = run(test, '', controller)
    await started.promise
    controller.abort(abort)
    await expect(execution).rejects.toBe(abort)

    let disposed = false
    const disposal = test.plugin.dispose()
    void disposal.then(() => { disposed = true })
    await new Promise(resolve => setTimeout(resolve, 0))
    expect(test.ctx.commands.find(test.agent, 'compact')).toBeUndefined()
    expect(disposed).toBe(false)

    allowClose.resolve(undefined)
    await closed.promise
    await new Promise(resolve => setTimeout(resolve, 0))
    expect(disposed).toBe(false)

    allowFlush.resolve(undefined)
    await flushed.promise
    await disposal
    expect(disposed).toBe(true)
  })
})