Files
deepseek-harness/packages/compact/compact/tests/compact.spec.ts
T
Yichen Jiang 765052a7d1 fix(agent-loop): compose the session prefix before pre-step; hand it to the pressure gate
ds-review-bot critical (follow-up): on the first step of a resumed or
seeded/forked instance, auto-compaction ran before runStep composed
this instance's prefix, so the gate read the PREVIOUS instance's logged
prefix from the header fold — a contributor that grew across
resume/fork (skills added, AGENTS.md grown: exactly the
environment-dependent case) could under-gate and ship an over-window
first request.

The loop now composes agent/session-prefix before the instance's first
agent/pre-step (still once per instance; runStep just reads the cache),
and agent/pre-step carries the composed prefix to its listeners.
CompactService.compactIfNeeded gains the sessionPrefix parameter;
BasicCompactService.estimatePressure gates on the handed value — the
header-fold read is gone, so the estimate is exact at every step
including a resumed/forked instance's first. Composition moving before
the boundary snapshot also means a composing listener's session append
now joins the CURRENT request (documented on the seam).

New coverage: composition precedes pre-step and the seam receives the
composed prefix; cancel and disposal landing inside the composition
window drop the step cleanly; the compact gate test hands the prefix
directly.
2026-07-08 20:30:10 +08:00

116 lines
4.2 KiB
TypeScript

import { describe, expect, it } from 'vitest'
import { Context } from 'cordis'
import { CompactService } from '@deepseek-ai/dsh-compact'
import type { CompactionResult } from '@deepseek-ai/dsh-compact'
import type { Message } from '@deepseek-ai/dsh-llm'
import { Session, SessionId } from '@deepseek-ai/dsh-session'
import type { CompactAgentContext } from '@deepseek-ai/dsh-compact'
/**
* A trivial concrete CompactService implementing the abstract contract. The
* interface package owns no algorithm — these tests exercise the seam itself:
* service registration, the abstract method shape, and the `compact/*` event
* declaration merge.
*/
class StubCompactService extends CompactService {
/** Records the signal handed to the most recent call, to prove it threads through. */
lastSignal: AbortSignal | undefined
override async compactIfNeeded(
_agent: CompactAgentContext,
_fullSystemPrompt: string,
_sessionPrefix: readonly Message[],
signal: AbortSignal,
): Promise<CompactionResult | null> {
this.lastSignal = signal
return null
}
override async compactRegion(
session: Session,
start: number,
end: number,
_agent: CompactAgentContext,
signal?: AbortSignal,
): Promise<CompactionResult> {
this.lastSignal = signal
// Minimal stub honoring the lock + log-only event contract.
const startEvent = session.append('compact/start', { turn: 0 })
const summaryEvent = session.append('compact/summary', {
summary: [{ type: 'text', text: 'stub' }],
shadowedRange: { start, end },
shadowedSeqs: [],
shadowedTokenCount: 0,
model: 'stub',
})
const endEvent = session.append('compact/end', { turn: 0 })
return {
startSeq: startEvent.seq,
summarySeq: summaryEvent.seq,
endSeq: endEvent.seq,
summary: [{ type: 'text', text: 'stub' }],
shadowedRange: { start, end },
shadowedSeqs: [],
shadowedTokenCount: 0,
}
}
}
describe('CompactService seam', () => {
function stubAgent(session: Session, model?: string): CompactAgentContext {
return { session, options: model === undefined ? {} : { model } }
}
it('registers as ctx.compact', () => {
const ctx = new Context()
void new StubCompactService(ctx)
expect(ctx.compact).toBeDefined()
expect(ctx.compact).toBeInstanceOf(StubCompactService)
})
it('disposing the fiber unregisters ctx.compact (HMR safety)', async () => {
const ctx = new Context()
const fiber = await ctx.plugin(StubCompactService)
expect(ctx.compact).toBeInstanceOf(StubCompactService)
await fiber.dispose()
expect(ctx.compact).toBeUndefined()
})
it('exposes the abstract contract methods', async () => {
const ctx = new Context()
const svc = new StubCompactService(ctx)
const session = new Session(SessionId('s'))
expect(await svc.compactIfNeeded(stubAgent(session), '', [], new AbortController().signal)).toBeNull()
})
it('compact/* events merge into SessionEventMap and are log-only', async () => {
const ctx = new Context()
const svc = new StubCompactService(ctx)
const session = new Session(SessionId('s'))
const result = await svc.compactRegion(session, 0, 0, stubAgent(session, 'm'))
const startEvent = session.events.find(e => e.type === 'compact/start')
expect(startEvent).toBeDefined()
// Log-only: the compiler rejects surfaceOp on compact/* (not a SurfaceEventType);
// verify the runtime value is absent.
const raw = startEvent as unknown as { surfaceOp?: unknown }
expect(raw.surfaceOp).toBeUndefined()
expect(result.summarySeq).toBeGreaterThan(result.startSeq)
expect(result.endSeq).toBeGreaterThan(result.summarySeq)
})
it('threads the cancellation signal through to the backend', async () => {
const ctx = new Context()
const svc = new StubCompactService(ctx)
const session = new Session(SessionId('s'))
const controller = new AbortController()
await svc.compactRegion(session, 0, 0, stubAgent(session, 'm'), controller.signal)
expect(svc.lastSignal).toBe(controller.signal)
await svc.compactIfNeeded(stubAgent(session), '', [], controller.signal)
expect(svc.lastSignal).toBe(controller.signal)
})
})