import { describe, expect, it } from 'vitest' import { Context } from 'cordis' import Loader from '@cordisjs/plugin-loader' import type { Agent } from '@deepseek-ai/dsh-agent' import CommandService from '@deepseek-ai/dsh-commands' import { CompactService, ManualCompactionError, type CompactAgentContext, type CompactionResult, type CompactionTrigger, type ManualCompactAgentContext, } from '@deepseek-ai/dsh-compact' import { Session, SessionId } from '@deepseek-ai/dsh-session' import * as commandCompact from '@deepseek-ai/dsh-command-compact' const RESULT: CompactionResult = { startSeq: 10, summarySeq: 11, endSeq: 13, summary: [{ type: 'text', text: 'summary' }], shadowedRange: { start: 1, end: 7 }, shadowedSeqs: [1, 3, 7], shadowedTokenCount: 42, } class StubCompactService extends CompactService { result: CompactionResult | null = RESULT failure: unknown operation: (() => Promise) | undefined calls: { agent: ManualCompactAgentContext; signal: AbortSignal }[] = [] override compactIfNeeded( _agent: CompactAgentContext, _trigger: CompactionTrigger, _signal: AbortSignal, ): Promise { return Promise.resolve(null) } override compactRegion(): Promise { return Promise.resolve(RESULT) } override compactNow( agent: ManualCompactAgentContext, signal: AbortSignal, ): Promise { this.calls.push({ agent, signal }) if (this.operation !== undefined) return this.operation() return this.failure === undefined ? Promise.resolve(this.result) // oxlint-disable-next-line typescript/prefer-promise-reject-errors -- exercise arbitrary backend rejection values. : Promise.reject(this.failure) } } interface Harness { readonly ctx: Context readonly compact: StubCompactService readonly agent: Agent readonly plugin: Awaited> } async function harness(): Promise { const ctx = new Context() await ctx.plugin(CommandService) const compact = new StubCompactService(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>>> { 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: { readonly kind: 'success' | 'error'; readonly text?: string }, ): string { const lifecycle = test.agent.session.events.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', 'compact']) 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).', }) 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.'], ['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() const allowClose = Promise.withResolvers() const closed = Promise.withResolvers() const allowFlush = Promise.withResolvers() const flushed = Promise.withResolvers() 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) }) })