| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280 |
- 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 provenance = {
- compactionId: result.compactionId,
- ...sourceCommandId === undefined ? {} : { sourceCommandId },
- }
- agent.session.append('compaction/start', { ...provenance, turn: null })
- agent.session.append('compaction/summary', {
- ...provenance,
- summary: result.summary,
- shadowedRange: result.shadowedRange,
- shadowedSeqs: result.shadowedSeqs,
- shadowedTokenCount: result.shadowedTokenCount,
- provider: 'command-test',
- model: 'command-test',
- })
- agent.session.append('compaction/end', { ...provenance, turn: null })
- return { ...result, ...provenance }
- }
- }
- 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({
- 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 conversation is unchanged; the attempt is recorded in the session log.'],
- ['summary', 'Compaction could not produce a useful summary. The conversation is unchanged; 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)
- })
- })
|