Merge remote-tracking branch 'origin/master' into codex/ask-user-question
# Conflicts: # docs/cordis-catalog/events-and-services.md # docs/module-graph.md # docs/rfc/README.md # packages/README.md # packages/ui/README.md # packages/ui/acp/tests/harness.ts
This commit is contained in:
@@ -20,6 +20,7 @@ import AgentLoop from '@deepseek-ai/dsh-agent-loop'
|
||||
import SessionPersistenceJsonl from '@deepseek-ai/dsh-session-persistence-jsonl'
|
||||
import { LocalBashExecutor } from '@deepseek-ai/dsh-bash-local'
|
||||
import * as ToolBash from '@deepseek-ai/dsh-tool-bash'
|
||||
import * as ToolTodo from '@deepseek-ai/dsh-tool-todo'
|
||||
import {
|
||||
ClientSideConnection,
|
||||
ndJsonStream,
|
||||
@@ -168,6 +169,12 @@ export async function makeBridgeHarness(options: {
|
||||
withBash?: boolean
|
||||
/** Plug the REAL `ask_user_question` tool and ACP user-interaction provider. */
|
||||
withAskUser?: boolean
|
||||
/**
|
||||
* Plug the REAL `dsh-tool-todo` tool so a test can drive `todo_write` through
|
||||
* the bridge and assert the resulting `plan` sessionUpdate — the shipping
|
||||
* tool + the bridge's own todo/write→plan mapping, not a stand-in.
|
||||
*/
|
||||
withTodo?: boolean
|
||||
} = { storageDir: '' }): Promise<BridgeHarness> {
|
||||
const adapter = new MockAdapter(options.script ?? [])
|
||||
|
||||
@@ -187,6 +194,9 @@ export async function makeBridgeHarness(options: {
|
||||
await ctx.plugin(LocalBashExecutor, { timeoutMs: 10_000 })
|
||||
await ctx.plugin(ToolBash)
|
||||
}
|
||||
if (options.withTodo) {
|
||||
await ctx.plugin(ToolTodo)
|
||||
}
|
||||
ctx.llm.registerAdapter(['mock'], adapter)
|
||||
|
||||
// Two identity byte pipes cross-wired into the two ndJsonStreams: bytes the
|
||||
|
||||
@@ -94,6 +94,44 @@ describe('acp bridge — session/load replay', () => {
|
||||
expect(content[0]?.content.text).toBe('```console\nhello\n```')
|
||||
})
|
||||
|
||||
it('replays a persisted todo/write as a plan sessionUpdate on load', async () => {
|
||||
// A turn whose model called todo_write persists a todo/write event. A fresh
|
||||
// bridge loading the session must re-emit the ACP `plan` update from the log
|
||||
// (the load replay runs every event through streamSessionEventUpdate), so an
|
||||
// editor reopening the session sees the current plan.
|
||||
live = await makeBridgeHarness({
|
||||
storageDir,
|
||||
withTodo: true,
|
||||
script: [
|
||||
toolCallResponse('c1', 'todo_write', {
|
||||
todos: [
|
||||
{ content: 'first step', status: 'in_progress' },
|
||||
{ content: 'second step', status: 'pending' },
|
||||
],
|
||||
}),
|
||||
textResponse('planned'),
|
||||
],
|
||||
})
|
||||
await live.client.initialize({ protocolVersion: PROTOCOL_VERSION, clientCapabilities: {} })
|
||||
const { sessionId } = await live.client.newSession({ cwd: process.cwd(), mcpServers: [] })
|
||||
await live.client.prompt({ sessionId, prompt: [{ type: 'text', text: 'plan it' }] })
|
||||
await live.dispose()
|
||||
live = undefined
|
||||
|
||||
loader = await makeBridgeHarness({ storageDir, withTodo: true, script: [] })
|
||||
await loader.client.initialize({ protocolVersion: PROTOCOL_VERSION, clientCapabilities: {} })
|
||||
await loader.client.loadSession({ sessionId, cwd: process.cwd(), mcpServers: [] })
|
||||
|
||||
const plan = loader.updates.find(u => u.sessionUpdate === 'plan')
|
||||
expect(plan).toEqual({
|
||||
sessionUpdate: 'plan',
|
||||
entries: [
|
||||
{ content: 'first step', priority: 'medium', status: 'in_progress' },
|
||||
{ content: 'second step', priority: 'medium', status: 'pending' },
|
||||
],
|
||||
})
|
||||
})
|
||||
|
||||
it('replays a persisted bash call as a TERMINAL card when the loader advertises the capability', async () => {
|
||||
// The presentation is resolved at replay time, so a loader that advertised
|
||||
// _meta.terminal_output must reconstruct the terminal card (content + _meta)
|
||||
|
||||
@@ -3,7 +3,7 @@ import { CallId } from '@deepseek-ai/dsh-llm'
|
||||
import { SessionId, type SessionEvent } from '@deepseek-ai/dsh-session'
|
||||
import type { SessionNotification } from '@agentclientprotocol/sdk'
|
||||
import type { ToolDefinition, ToolRegistry } from '@deepseek-ai/dsh-tools'
|
||||
import { streamSessionEventUpdate, agentOptions, ToolPresenter } from '../src/index.ts'
|
||||
import { streamSessionEventUpdate, agentOptions, todosToPlan, ToolPresenter } from '../src/index.ts'
|
||||
|
||||
/** Collect the updates a single event produces (no presenter → generic fallback). */
|
||||
function updatesFor(event: SessionEvent): SessionNotification['update'][] {
|
||||
@@ -118,6 +118,43 @@ describe('streamSessionEventUpdate', () => {
|
||||
expect(updatesFor(evt('turn/end', { turn: 1, reason: { kind: 'completed' } }))).toEqual([])
|
||||
expect(updatesFor(evt('step/start', { turn: 1, step: 1 }))).toEqual([])
|
||||
})
|
||||
|
||||
it('maps todo/write to a plan sessionUpdate with priority synthesized as medium', () => {
|
||||
expect(updatesFor(evt('todo/write', {
|
||||
todos: [
|
||||
{ content: 'plan the work', status: 'in_progress' },
|
||||
{ content: 'write the code', status: 'pending' },
|
||||
{ content: 'run the tests', status: 'completed' },
|
||||
],
|
||||
}))).toEqual([{
|
||||
sessionUpdate: 'plan',
|
||||
entries: [
|
||||
{ content: 'plan the work', priority: 'medium', status: 'in_progress' },
|
||||
{ content: 'write the code', priority: 'medium', status: 'pending' },
|
||||
{ content: 'run the tests', priority: 'medium', status: 'completed' },
|
||||
],
|
||||
}])
|
||||
})
|
||||
|
||||
it('maps an empty todo list to a plan with no entries', () => {
|
||||
expect(updatesFor(evt('todo/write', { todos: [] }))).toEqual([{ sessionUpdate: 'plan', entries: [] }])
|
||||
})
|
||||
})
|
||||
|
||||
describe('todosToPlan', () => {
|
||||
it('maps status 1:1 and stamps every entry priority medium', () => {
|
||||
expect(todosToPlan([
|
||||
{ content: 'a', status: 'pending' },
|
||||
{ content: 'b', status: 'in_progress' },
|
||||
{ content: 'c', status: 'completed' },
|
||||
])).toEqual({
|
||||
entries: [
|
||||
{ content: 'a', priority: 'medium', status: 'pending' },
|
||||
{ content: 'b', priority: 'medium', status: 'in_progress' },
|
||||
{ content: 'c', priority: 'medium', status: 'completed' },
|
||||
],
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('ToolPresenter (tool-owned presentation via the tool registry)', () => {
|
||||
|
||||
Reference in New Issue
Block a user