Files
deepseek-harness/packages/ui/acp/tests/modes.spec.ts
T

117 lines
6.2 KiB
TypeScript

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',
})
})
})