Merge refreshed schema DSL into canonical tool output
# Conflicts: # docs/config-catalog.md # examples/headless-agent/tests/snapshots/advanced-toolchain/session.1.jsonl # examples/headless-agent/tests/snapshots/advanced-toolchain/session.2.jsonl # examples/headless-agent/tests/snapshots/advanced-toolchain/session.jsonl # packages/context/workspace-context/tests/workspace-context.spec.ts # packages/core/tools/tests/tools.spec.ts # packages/ui/tui/src/index.ts # packages/ui/tui/tests/tui.snapshot.ts
This commit is contained in:
@@ -143,15 +143,18 @@ describe('acp bridge', () => {
|
||||
questions: [{
|
||||
id: 'language',
|
||||
question: 'Which language?',
|
||||
detail: 'Choose the implementation language for this project.',
|
||||
options: [{ label: 'TypeScript' }],
|
||||
}],
|
||||
})
|
||||
|
||||
expect(result).toEqual({ answers: [{ id: 'language', selected: [], custom: 'Use Zig' }] })
|
||||
expect(harness.elicitationRequests[0]).toMatchObject({
|
||||
message: 'Which language?\n\nChoose the implementation language for this project.',
|
||||
requestedSchema: {
|
||||
properties: {
|
||||
choice: {
|
||||
title: 'Which language?',
|
||||
description: 'Choose one option, or fill a custom answer below.',
|
||||
oneOf: [{ const: 'TypeScript', title: 'TypeScript' }],
|
||||
},
|
||||
|
||||
@@ -17,6 +17,7 @@ import * as FsPolicy from '@deepseek-ai/dsh-fs-policy'
|
||||
import * as ToolBash from '@deepseek-ai/dsh-tool-bash'
|
||||
import * as ToolFs from '@deepseek-ai/dsh-tool-fs'
|
||||
import * as ToolTodo from '@deepseek-ai/dsh-tool-todo'
|
||||
import PlanModeService from '@deepseek-ai/dsh-plan-mode'
|
||||
import {
|
||||
ClientSideConnection,
|
||||
ndJsonStream,
|
||||
@@ -191,6 +192,8 @@ export async function makeBridgeHarness(options: {
|
||||
* tool + the bridge's own todo/write→plan mapping, not a stand-in.
|
||||
*/
|
||||
withTodo?: boolean
|
||||
/** Plug the REAL `dsh-plan-mode` plugin so a test can drive the session-mode picker. */
|
||||
withModes?: boolean
|
||||
/**
|
||||
* Plug the REAL filesystem stack (`dsh-fs-local` + `dsh-fs-policy` +
|
||||
* `dsh-tool-fs`) so a test can drive `read`/`write`/`edit` through the bridge
|
||||
@@ -225,6 +228,9 @@ export async function makeBridgeHarness(options: {
|
||||
if (options.withTodo) {
|
||||
await ctx.plugin(ToolTodo)
|
||||
}
|
||||
if (options.withModes) {
|
||||
await ctx.plugin(PlanModeService, { section: 'Test plan mode instructions.' })
|
||||
}
|
||||
if (options.withFs) {
|
||||
await ctx.plugin(LocalFileSystem, { cwd: options.fsCwd ?? options.storageDir })
|
||||
await ctx.plugin(FsPolicy)
|
||||
|
||||
116
packages/ui/acp/tests/modes.spec.ts
Normal file
116
packages/ui/acp/tests/modes.spec.ts
Normal file
@@ -0,0 +1,116 @@
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
|
||||
import { mkdtemp, rm } from 'node:fs/promises'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { PROTOCOL_VERSION } from '@agentclientprotocol/sdk'
|
||||
import { SessionId } from '@deepseek-ai/dsh-session'
|
||||
import { makeBridgeHarness, textResponse, type BridgeHarness, type CapturedUpdate } from './harness.ts'
|
||||
|
||||
/** The `current_mode_update` notifications, in order. */
|
||||
function modeUpdates(updates: CapturedUpdate[]): string[] {
|
||||
return updates
|
||||
.filter(update => update.sessionUpdate === 'current_mode_update')
|
||||
.map(update => update.currentModeId)
|
||||
}
|
||||
|
||||
describe('acp bridge — plan mode projection', () => {
|
||||
let storageDir: string
|
||||
let harness: BridgeHarness | undefined
|
||||
let loader: BridgeHarness | undefined
|
||||
|
||||
beforeEach(async () => { storageDir = await mkdtemp(join(tmpdir(), 'acp-modes-')) })
|
||||
afterEach(async () => {
|
||||
if (harness) await harness.dispose()
|
||||
if (loader) await loader.dispose()
|
||||
harness = loader = undefined
|
||||
await rm(storageDir, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
it('advertises no mode surface and rejects session/set_mode when plan mode is not composed', async () => {
|
||||
harness = await makeBridgeHarness({ storageDir })
|
||||
await harness.client.initialize({ protocolVersion: PROTOCOL_VERSION, clientCapabilities: {} })
|
||||
const res = await harness.client.newSession({ cwd: process.cwd(), mcpServers: [] })
|
||||
expect(res.modes).toBeUndefined()
|
||||
await expect(harness.client.setSessionMode({ sessionId: res.sessionId, modeId: 'plan' }))
|
||||
.rejects.toMatchObject({ message: expect.stringContaining('session modes are not composed') as string })
|
||||
})
|
||||
|
||||
it('advertises availableModes/currentModeId on session/new', async () => {
|
||||
harness = await makeBridgeHarness({ storageDir, withModes: true })
|
||||
await harness.client.initialize({ protocolVersion: PROTOCOL_VERSION, clientCapabilities: {} })
|
||||
const res = await harness.client.newSession({ cwd: process.cwd(), mcpServers: [] })
|
||||
expect(res.modes).toEqual({
|
||||
availableModes: [
|
||||
{ id: 'default', name: 'default' },
|
||||
{ id: 'plan', name: 'plan' },
|
||||
],
|
||||
currentModeId: 'default',
|
||||
})
|
||||
})
|
||||
|
||||
it('session/set_mode records the pending intent and echoes one optimistic current_mode_update', async () => {
|
||||
harness = await makeBridgeHarness({ storageDir, withModes: true })
|
||||
await harness.client.initialize({ protocolVersion: PROTOCOL_VERSION, clientCapabilities: {} })
|
||||
const { sessionId } = await harness.client.newSession({ cwd: process.cwd(), mcpServers: [] })
|
||||
await harness.client.setSessionMode({ sessionId, modeId: 'plan' })
|
||||
expect(modeUpdates(harness.updates)).toEqual(['plan'])
|
||||
const agent = harness.ctx.agents.get(SessionId(sessionId))!
|
||||
expect(harness.ctx.planMode.get(agent)).toEqual({ active: false, pending: true })
|
||||
})
|
||||
|
||||
it('rejects an unknown ACP mode id at the adapter boundary', async () => {
|
||||
harness = await makeBridgeHarness({ storageDir, withModes: true })
|
||||
await harness.client.initialize({ protocolVersion: PROTOCOL_VERSION, clientCapabilities: {} })
|
||||
const { sessionId } = await harness.client.newSession({ cwd: process.cwd(), mcpServers: [] })
|
||||
await expect(harness.client.setSessionMode({ sessionId, modeId: 'nope' }))
|
||||
.rejects.toMatchObject({ message: expect.stringContaining('unknown session mode "nope"') as string })
|
||||
expect(modeUpdates(harness.updates)).toEqual([])
|
||||
})
|
||||
|
||||
it('does not re-notify when the boundary flush logs the mode the picker already showed', async () => {
|
||||
harness = await makeBridgeHarness({ storageDir, withModes: true, script: [textResponse('planning')] })
|
||||
await harness.client.initialize({ protocolVersion: PROTOCOL_VERSION, clientCapabilities: {} })
|
||||
const { sessionId } = await harness.client.newSession({ cwd: process.cwd(), mcpServers: [] })
|
||||
await harness.client.setSessionMode({ sessionId, modeId: 'plan' })
|
||||
await harness.client.prompt({ sessionId, prompt: [{ type: 'text', text: 'go plan' }] })
|
||||
const agent = harness.ctx.agents.get(SessionId(sessionId))!
|
||||
expect(agent.session.events.some(event => event.type === 'plan/mode')).toBe(true)
|
||||
expect(modeUpdates(harness.updates)).toEqual(['plan'])
|
||||
})
|
||||
|
||||
it('re-notifies on a logged flip the picker has not seen (the tool-driven exit shape)', async () => {
|
||||
harness = await makeBridgeHarness({ storageDir, withModes: true, script: [textResponse('planning')] })
|
||||
await harness.client.initialize({ protocolVersion: PROTOCOL_VERSION, clientCapabilities: {} })
|
||||
const { sessionId } = await harness.client.newSession({ cwd: process.cwd(), mcpServers: [] })
|
||||
await harness.client.setSessionMode({ sessionId, modeId: 'plan' })
|
||||
await harness.client.prompt({ sessionId, prompt: [{ type: 'text', text: 'go plan' }] })
|
||||
// A writer other than the picker (exit_plan_mode's execute) appends the
|
||||
// flip back; the bridge must re-notify the client off the logged event.
|
||||
const agent = harness.ctx.agents.get(SessionId(sessionId))!
|
||||
agent.session.append('plan/mode', { active: false })
|
||||
// The notification crosses the in-memory JSON-RPC transport asynchronously.
|
||||
await new Promise(resolve => setTimeout(resolve, 20))
|
||||
expect(modeUpdates(harness.updates)).toEqual(['plan', 'default'])
|
||||
})
|
||||
|
||||
it('advertises the folded mode on session/load', async () => {
|
||||
harness = await makeBridgeHarness({ storageDir, withModes: true, script: [textResponse('planning')] })
|
||||
await harness.client.initialize({ protocolVersion: PROTOCOL_VERSION, clientCapabilities: {} })
|
||||
const { sessionId } = await harness.client.newSession({ cwd: process.cwd(), mcpServers: [] })
|
||||
await harness.client.setSessionMode({ sessionId, modeId: 'plan' })
|
||||
await harness.client.prompt({ sessionId, prompt: [{ type: 'text', text: 'go plan' }] })
|
||||
await harness.dispose()
|
||||
harness = undefined
|
||||
|
||||
loader = await makeBridgeHarness({ storageDir, withModes: true, script: [] })
|
||||
await loader.client.initialize({ protocolVersion: PROTOCOL_VERSION, clientCapabilities: {} })
|
||||
const res = await loader.client.loadSession({ sessionId, cwd: process.cwd(), mcpServers: [] })
|
||||
expect(res.modes).toEqual({
|
||||
availableModes: [
|
||||
{ id: 'default', name: 'default' },
|
||||
{ id: 'plan', name: 'plan' },
|
||||
],
|
||||
currentModeId: 'plan',
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user