test(preset): cover manual compaction in minimal mode

This commit is contained in:
LogosVeritas committed 2026-10-12 12:02:27 +08:00
1 parent f15091a922
commit 110ef57123
6 files changed
+301 -1

No files matched your search

+119 -1
View File
@@ -1,4 +1,4 @@
import { mkdir, writeFile } from 'node:fs/promises'
import { mkdir, readFile, writeFile } from 'node:fs/promises'
import { join } from 'node:path'
import { fileURLToPath } from 'node:url'
import type { Browser, Page } from 'playwright'
@@ -6,6 +6,7 @@ import { chromium } from 'playwright'
import { afterAll, beforeAll, describe, expect, it, onTestFailed } from 'vitest'
import type { AgentHandle } from '@deepseek-ai/dsh-agent'
import { ToolCallId, createUserMessage } from '@deepseek-ai/dsh-llm'
import type { GenerateOptions } from '@deepseek-ai/dsh-llm'
import { SessionId } from '@deepseek-ai/dsh-session'
import type { Session } from '@deepseek-ai/dsh-session'
import type {} from '@deepseek-ai/dsh-agent-preset-registry'
@@ -14,6 +15,7 @@ import {
assertFixtureInventory,
captureStableAria,
compareOrRefreshGolden,
fixtureUserPrompts,
launchWebScaffold,
watchConsole,
webSnapshotMode,
@@ -195,6 +197,122 @@ describe('minimal agent preset', () => {
})
})
describe('minimal manual compaction', () => {
const snapshotDir = fileURLToPath(new URL('../../../snapshots/web/minimal-compact', import.meta.url))
let scaffold: WebScaffold
let handle: AgentHandle
let browser: Browser | undefined
let page: Page | undefined
const requests: GenerateOptions[] = []
beforeAll(async () => {
const fixture = join(snapshotDir, 'session.v4.jsonl')
const prompts = fixtureUserPrompts(await readFile(fixture, 'utf8'))
expect(prompts).toHaveLength(2)
const [initial, continuation] = prompts
if (initial === undefined || continuation === undefined) throw new Error('minimal compaction fixture requires two user prompts')
scaffold = await launchWebScaffold({ replayFixture: fixture, compareReplaySession: true })
scaffold.ctx.on('llm/stream', async function* (options, next) {
requests.push(options)
yield* next()
})
handle = await scaffold.ctx.agents.create({
sessionId: SessionId('minimal-compact-smoke'),
meta: { cwd: scaffold.workspaceCwd, agentPreset: 'minimal' },
agentOptions: { provider: 'deepseek-official', model: 'deepseek-v4-flash' },
setup: agentCtx => scaffold.ctx.agentPresets.mount(agentCtx, 'minimal').then(() => undefined),
})
handle.agent.followup(createUserMessage({
content: [{ type: 'text', text: initial }], source: { kind: 'user' },
}))
await handle.agent.whenIdle()
const session = handle.agent.session
expect(session.snapshotEvents().some(event => event.type === 'compaction/start')).toBe(false)
const generation = session.surface.replaceGeneration
const execution = await scaffold.ctx.commands.execute(handle.agent, '/compact', [], new AbortController().signal)
expect(execution?.result.kind).toBe('success')
expect(session.surface.replaceGeneration).toBe(generation + 1)
handle.agent.followup(createUserMessage({
content: [{ type: 'text', text: continuation }], source: { kind: 'user' },
}))
await handle.agent.whenIdle()
})
afterAll(async () => {
const failures: unknown[] = []
await page?.close().catch((error: unknown) => failures.push(error))
await browser?.close().catch((error: unknown) => failures.push(error))
await handle?.dispose().catch((error: unknown) => failures.push(error))
await scaffold?.close().catch((error: unknown) => failures.push(error))
if (failures.length === 1) throw failures[0]
if (failures.length > 1) throw new AggregateError(failures, 'minimal compaction teardown failed')
})
it('records the manual command, summary, and durable history replacement', () => {
const session = handle.agent.session
const events = session.snapshotEvents()
const command = events.find(event => event.type === 'command/run')
const start = events.find(event => event.type === 'compaction/start')
const summary = events.find(event => event.type === 'compaction/summary')
const end = events.find(event => event.type === 'compaction/end')
const done = events.find(event => event.type === 'command/done')
expect(command?.data.name).toBe('compact')
expect(start?.data).toMatchObject({ turn: null, sourceCommandId: command?.data.commandId })
expect(summary?.data).toMatchObject({ sourceCommandId: command?.data.commandId, llmStreamCall: true })
expect(end?.data).toEqual(start?.data)
expect(done?.data).toMatchObject({ kind: 'success', sourceEventSeq: summary?.seq })
const checkpoint = events.find(event => event.type === 'user/message' && event.data.source.kind === 'compact-checkpoint')
if (checkpoint?.type !== 'user/message') throw new Error('manual compaction produced no checkpoint')
expect(checkpoint.surfaceOp).toMatchObject({
op: 'replace', startSeq: summary?.data.shadowedRange.start, endSeq: summary?.data.shadowedRange.end,
})
expect(session.deriveMessages().some(message => message.id === checkpoint.data.id)).toBe(true)
const original = events.find(event => event.type === 'user/message' && event.data.source.kind === 'user')
if (original?.type !== 'user/message') throw new Error('minimal compaction has no original user message')
expect(session.deriveMessages().some(message => message.id === original.data.id)).toBe(false)
})
it('continues with the summary and the same minimal prompt and tools', () => {
expect(requests.map(request => request.purpose)).toEqual([undefined, 'compaction', undefined])
const summary = handle.agent.session.snapshotEvents().find(event => event.type === 'compaction/summary')
if (summary === undefined) throw new Error('minimal compaction produced no summary')
const [initial, , continuation] = requests
if (initial === undefined || continuation === undefined) throw new Error('minimal compaction did not continue')
expect(continuation.messages.flatMap(message => message.content)).toEqual(expect.arrayContaining(summary.data.summary))
expect(continuation.messages[0]).toEqual(initial.messages[0])
expect(continuation.tools).toEqual(initial.tools)
expect(continuation.messages.flatMap(message => message.content)
.some(block => block.type === 'text' && block.text === 'MINIMAL_COMPACT_READY')).toBe(true)
expect(handle.agent.session.deriveMessages().at(-1)?.content)
.toEqual([{ type: 'text', text: 'MINIMAL_COMPACT_OK' }])
})
it.skipIf(MODE === 'record')('renders the manual compaction card and its summary', async () => {
onTestFailed(() => { if (page !== undefined) void saveFailureShot(page, 'web-minimal-compact') })
browser = await chromium.launch()
page = await newEnglishPage(browser)
const tripwire = watchConsole(page)
await page.goto(scaffold.authenticatedUrl, { waitUntil: 'load' })
await page.waitForSelector('[class*="frame"]', { timeout: 30_000 })
await page.locator('[role="treeitem"]').first().click()
await page.locator('[role="treeitem"]').nth(1).click()
await page.getByText('MINIMAL_COMPACT_OK', { exact: true }).waitFor()
const card = page.getByRole('button', { name: /^compact Compacted / })
await card.click()
await expect.poll(() => card.getAttribute('aria-expanded')).toBe('true')
await compareOrRefreshGolden(join(snapshotDir, 'ui.expected.md'),
await captureStableAria(page, '[class*="centerCol"]', scaffold.workspaceCwd), MODE)
expect(tripwire.pageErrors).toEqual([])
expect(tripwire.warnings).toEqual([])
})
it('keeps its snapshot inventory closed', async () => {
await assertFixtureInventory(snapshotDir, [
'session.v4.jsonl', 'system-prompt.expected.md', 'tool-schemas.expected.json', 'ui.expected.md',
])
})
})
describe('minimal image reading', () => {
const snapshotDir = fileURLToPath(new URL('../../../snapshots/web/minimal-read-image', import.meta.url))
const prompt = 'Use read_image to inspect red.png. Then reply exactly MINIMAL_IMAGE_OK and stop.'
@@ -0,0 +1,33 @@
{"type":"session","version":4,"id":"{{session:1}}","createdAt":1788240634286,"cwd":"{{cwd}}","isSeeded":false,"delegationDepth":0,"agentPreset":"minimal"}
{"type":"permission/preset","data":{"preset":"workspace-write"}}
{"type":"sandbox/mode","data":{"mode":"workspace-write"}}
{"type":"approval/policy","data":{"policy":"ask"}}
{"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Remember the project requirements: implement image reading in minimal mode by reusing the existing filesystem tool. Preserve the fixed system prompt and keep standard mode unchanged. The new tool selection configuration must default to the full filesystem suite and reject unknown names, duplicate names, and empty lists. Minimal mode selects read_image only. Image reads must use the inherited filesystem authorization and durable attachment store, and the calling model must explicitly support image input. Do not register text read, write, or edit tools in minimal mode. Enable automatic compaction under context pressure and manual /compact while the agent is idle, using the existing backend to generate the summary. Keep the token meter shared and the compaction service isolated per preset. Preserve balanced tool-call and result pairs when replacing older history, retain recent context, and continue from the checkpoint. Dispose every tool, command, and listener with its owning plugin. Reply exactly MINIMAL_COMPACT_READY and stop."}],"source":{"kind":"user"},"role":"user","id":"{{message:1}}"}]}}
{"type":"turn/start","data":{"turn":1}}
{"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}}
{"type":"step/start","data":{"turn":1,"step":1}}
{"type":"system/message","data":{"turn":1,"step":1,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"system-prompt"},"id":"{{message:2}}"}},"surfaceOp":"append"}
{"type":"user/message","data":{"content":[{"type":"text","text":"Remember the project requirements: implement image reading in minimal mode by reusing the existing filesystem tool. Preserve the fixed system prompt and keep standard mode unchanged. The new tool selection configuration must default to the full filesystem suite and reject unknown names, duplicate names, and empty lists. Minimal mode selects read_image only. Image reads must use the inherited filesystem authorization and durable attachment store, and the calling model must explicitly support image input. Do not register text read, write, or edit tools in minimal mode. Enable automatic compaction under context pressure and manual /compact while the agent is idle, using the existing backend to generate the summary. Keep the token meter shared and the compaction service isolated per preset. Preserve balanced tool-call and result pairs when replacing older history, retain recent context, and continue from the checkpoint. Dispose every tool, command, and listener with its owning plugin. Reply exactly MINIMAL_COMPACT_READY and stop."}],"source":{"kind":"user"},"role":"user","id":"{{message:1}}"},"surfaceOp":"append"}
{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\"."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."}]},"role":"user","id":"{{message:3}}"},"surfaceOp":"append"}
{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"tools":"{{tools}}"},"reason":"initial"}}
{"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash","contextWindow":128000}}
{"type":"session/title","data":{"title":"Remember the project requirements: imple","messageSeqs":[8],"source":{"kind":"fallback"}}}
{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"text","text":"MINIMAL_COMPACT_READY"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:4}}"},"usage":{"inputTokens":10,"outputTokens":4},"stream":[{"type":"chunk","time":1791729497737,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"chunk","time":1791729497737,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"MINIMAL_COMPACT_READY"}}},{"type":"chunk","time":1791729497737,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":4}}},{"type":"chunk","time":1791729497737,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"}
{"type":"step/end","data":{"turn":1,"step":1}}
{"type":"turn/end","data":{"turn":1,"reason":{"kind":"completed"}}}
{"type":"command/run","data":{"commandId":"{{command:1}}","name":"compact","args":"","source":{"kind":"user"}}}
{"type":"compaction/start","data":{"compactionId":"{{id:1}}","sourceCommandId":"{{command:1}}","turn":null}}
{"type":"compaction/summary","data":{"compactionId":"{{id:1}}","sourceCommandId":"{{command:1}}","summary":[{"type":"text","text":"Implement minimal image reading through the existing filesystem tool, preserving permissions, model capability checks, and standard mode. Enable automatic and manual compaction."}],"rawOutput":[{"type":"text","text":"Implement minimal image reading through the existing filesystem tool, preserving permissions, model capability checks, and standard mode. Enable automatic and manual compaction."}],"llmStreamCall":true,"shadowedRange":{"start":8,"end":9},"shadowedSeqs":[8,9],"shadowedTokenCount":326,"provider":"deepseek-official","model":"deepseek-v4-flash","maxTokens":65536}}
{"type":"user/message","data":{"content":[{"type":"text","text":"This is an automatically generated checkpoint condensing an earlier span of the conversation to free up context. Treat the captured context as established background and build on it without restating it. Continue the task directly from the messages that follow, without acknowledging this checkpoint.\n\n<compacted-summary>"},{"type":"text","text":"Implement minimal image reading through the existing filesystem tool, preserving permissions, model capability checks, and standard mode. Enable automatic and manual compaction."},{"type":"text","text":"</compacted-summary>"}],"source":{"kind":"compact-checkpoint","compactionId":"{{id:1}}","sourceCommandId":"{{command:1}}"},"role":"user","id":"{{message:5}}"},"sourceEventSeqs":[17,18,8,9],"surfaceOp":{"op":"replace","startSeq":8,"endSeq":9}}
{"type":"compaction/end","data":{"compactionId":"{{id:1}}","sourceCommandId":"{{command:1}}","turn":null}}
{"type":"command/done","data":{"commandId":"{{command:1}}","kind":"success","text":"Compacted 2 history items (~326 tokens).","sourceEventSeq":18}}
{"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Continue using the project requirements. Reply exactly MINIMAL_COMPACT_OK and stop."}],"source":{"kind":"user"},"role":"user","id":"{{message:6}}"}]}}
{"type":"turn/start","data":{"turn":2}}
{"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}}
{"type":"step/start","data":{"turn":2,"step":1}}
{"type":"user/message","data":{"content":[{"type":"text","text":"Continue using the project requirements. Reply exactly MINIMAL_COMPACT_OK and stop."}],"source":{"kind":"user"},"role":"user","id":"{{message:6}}"},"surfaceOp":"append"}
{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\"."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."}]},"role":"user","id":"{{message:7}}"},"surfaceOp":"append"}
{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"tools":"{{tools}}"},"reason":"series"}}
{"type":"assistant/message","data":{"turn":2,"step":1,"message":{"role":"assistant","content":[{"type":"text","text":"MINIMAL_COMPACT_OK"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:8}}"},"usage":{"inputTokens":10,"outputTokens":4},"stream":[{"type":"chunk","time":1791729497769,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"chunk","time":1791729497769,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"MINIMAL_COMPACT_OK"}}},{"type":"chunk","time":1791729497769,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":4}}},{"type":"chunk","time":1791729497769,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"}
{"type":"step/end","data":{"turn":2,"step":1}}
{"type":"turn/end","data":{"turn":2,"reason":{"kind":"completed"}}}
@@ -0,0 +1,9 @@
version: 1
scenario: minimal-compact
profile: web
composition: web-minimal-compact
recording: authored
header:
class: web-minimal-compact
pin: true
changes: 1
@@ -0,0 +1 @@
You are a helpful software engineer assistant.
@@ -0,0 +1,98 @@
{
"initial": [
{
"name": "bash",
"description": "Run commands in a bash shell\n* When invoking this tool, the contents of the \"command\" parameter does NOT need to be XML-escaped.\n* Network access depends on the task environment. Prefer configured mirrors/proxies when they are available.\n* State is persistent across command calls and discussions with the user.\n* To inspect a particular line range of a file, e.g. lines 10-25, try 'sed -n 10,25p /path/to/the/file'.\n* Please avoid commands that may produce a very large amount of output.\n* Please run long lived commands in the background, e.g. 'sleep 10 &' or start a server in the background.",
"parameters": {
"type": "object",
"properties": {
"command": {
"type": "string",
"description": "The bash command to run. Relative path is preferred in the command."
}
},
"required": [
"command"
]
}
},
{
"name": "read_image",
"description": "Read a PNG/JPEG/WebP/GIF file and return the image itself. Large images are downscaled automatically; do not install image libraries or create thumbnails to inspect an image.",
"parameters": {
"type": "object",
"properties": {
"file_path": {
"type": "string",
"description": "Path to the image file, resolved by the filesystem backend."
}
},
"required": [
"file_path"
]
}
},
{
"name": "working_directory",
"description": "Read the current working directory, or change it with cd. Relative paths use the current directory. Existing shells and running processes keep their own directories.",
"parameters": {
"type": "object",
"properties": {
"cd": {
"type": "string",
"description": "Existing directory to enter. Omit to read the current directory."
}
}
}
}
],
"changes": [
[
{
"name": "bash",
"description": "Run commands in a bash shell\n* When invoking this tool, the contents of the \"command\" parameter does NOT need to be XML-escaped.\n* Network access depends on the task environment. Prefer configured mirrors/proxies when they are available.\n* State is persistent across command calls and discussions with the user.\n* To inspect a particular line range of a file, e.g. lines 10-25, try 'sed -n 10,25p /path/to/the/file'.\n* Please avoid commands that may produce a very large amount of output.\n* Please run long lived commands in the background, e.g. 'sleep 10 &' or start a server in the background.",
"parameters": {
"type": "object",
"properties": {
"command": {
"type": "string",
"description": "The bash command to run. Relative path is preferred in the command."
}
},
"required": [
"command"
]
}
},
{
"name": "read_image",
"description": "Read a PNG/JPEG/WebP/GIF file and return the image itself. Large images are downscaled automatically; do not install image libraries or create thumbnails to inspect an image.",
"parameters": {
"type": "object",
"properties": {
"file_path": {
"type": "string",
"description": "Path to the image file, resolved by the filesystem backend."
}
},
"required": [
"file_path"
]
}
},
{
"name": "working_directory",
"description": "Read the current working directory, or change it with cd. Relative paths use the current directory. Existing shells and running processes keep their own directories.",
"parameters": {
"type": "object",
"properties": {
"cd": {
"type": "string",
"description": "Existing directory to enter. Omit to read the current directory."
}
}
}
}
]
]
}
@@ -0,0 +1,41 @@
- banner:
- navigation "Session hierarchy": "Remember the project requirements: imple"
- text: Minimal mode
- button "More actions"
- button "Open right sidebar"
- tablist:
- tab "Chat" [selected]
- tab "Trajectory"
- navigation "Turn navigation":
- button "Jump to turn 1"
- button "Jump to turn 2"
- text: "Remember the project requirements: implement image reading in minimal mode by reusing the existing filesystem tool. Preserve the fixed system prompt and keep standard mode unchanged. The new tool selection configuration must default to the full filesystem suite and reject unknown names, duplicate names, and empty lists. Minimal mode selects read_image only. Image reads must use the inherited filesystem authorization and durable attachment store, and the calling model must explicitly support image input. Do not register text read, write, or edit tools in minimal mode. Enable automatic compaction under context pressure and manual /compact while the agent is idle, using the existing backend to generate the summary. Keep the token meter shared and the compaction service isolated per preset. Preserve balanced tool-call and result pairs when replacing older history, retain recent context, and continue from the checkpoint. Dispose every tool, command, and listener with its owning plugin. Reply exactly MINIMAL_COMPACT_READY and stop. {{clock}}"
- button "Copy"
- status: Completed
- button "Completed in {{duration}}" [disabled]
- paragraph: MINIMAL_COMPACT_READY
- button "Copy"
- button "Good response"
- button "Bad response"
- button "Branch into a new conversation"
- text: {{clock}}
- button "compact Compacted 2 history items (~{{tokens}} tokens)" [expanded]
- paragraph: Implement minimal image reading through the existing filesystem tool, preserving permissions, model capability checks, and standard mode. Enable automatic and manual compaction.
- text: Continue using the project requirements. Reply exactly MINIMAL_COMPACT_OK and stop. {{clock}}
- button "Copy"
- status: Completed
- button "Completed in {{duration}}" [disabled]
- paragraph: MINIMAL_COMPACT_OK
- button "Copy"
- button "Good response"
- button "Bad response"
- button "Branch into a new conversation"
- text: {{clock}}
- textbox "Message or run a task, / commands, @ files or sessions"
- button "Add files or run commands"
- 'button "Access mode, current: Workspace Write"': Workspace Write
- button "Select model, current DeepSeek-V4-Flash": DeepSeek-V4-Flash
- button "Send message" [disabled]
- button "2 turns 2 steps"
- button "28 tok · Cache hit 0%": 28 tokCache hit 0%
- button "0% of context used": 0%