refactor: command.execute degrades to pure admission; composer notice channel retired
The wire response now carries only the matched bit — CommandExecuteResult is deleted from the api, schema, and client mirrors (pre-release, no shim); outcomes ride the durably logged command/run/command/done pair broadcast on the mux stream and render as flow nodes. ui-command's runDetached→noticeFor outcome routing is retired: admitted commands surface nothing through the composer, while admission misses (matched:false, syntax feedback) and transport failures keep their immediate notice. The connection fixture mirrors the host: an admitted command appends the lifecycle pair to the session log instead of returning result text.
This commit is contained in:
@@ -9,7 +9,7 @@ export type {
|
||||
ApiProxy, SessionsApi, SessionSummary, HostApi, EventsApi, MuxFrame, HostFrame,
|
||||
ApprovalResponsePayload, QuestionResponsePayload, HistoryEntry, ToolEventView,
|
||||
WorkspaceApi, WorkspaceId, WorkspaceView,
|
||||
CommandsApi, CommandDescriptor, CommandExecuteResult, SkillsApi, SkillEntry,
|
||||
CommandsApi, CommandDescriptor, SkillsApi, SkillEntry,
|
||||
} from '@deepseek-ai/dsh-host-apiproxy/api'
|
||||
export type { ToolCallView, ToolResultView } from '@deepseek-ai/dsh-tools/presentation'
|
||||
export type {
|
||||
|
||||
@@ -808,25 +808,27 @@ export function createFixtureApi(options: FixtureOptions = {}): ApiProxy {
|
||||
],
|
||||
})
|
||||
},
|
||||
// Pure admission, mirroring the host: an admitted command logs the
|
||||
// command/run + command/done lifecycle pair (mux-broadcast by append),
|
||||
// and the response only reports resolution.
|
||||
execute: (request) => {
|
||||
const missing = requireSession(request)
|
||||
if (missing !== undefined) return missing
|
||||
const id = request.payload.sessionId
|
||||
const line = request.payload.line.trim()
|
||||
const match = /^\/(\S+)(?:\s+(.*))?$/.exec(line)
|
||||
const name = match?.[1]
|
||||
if (name === 'compact' || name === 'echo') {
|
||||
return ok(request, {
|
||||
matched: true as const,
|
||||
result: { kind: 'success' as const, text: name === 'echo' ? (match?.[2] ?? '') : 'fixture:已压缩(假动作)' },
|
||||
})
|
||||
const outcomes: Record<string, string> = {
|
||||
compact: 'fixture:已压缩(假动作)',
|
||||
echo: match?.[2] ?? '',
|
||||
'goal-fixture': `fixture:goal 已设置(${id})`,
|
||||
}
|
||||
if (name === 'goal-fixture') {
|
||||
return ok(request, {
|
||||
matched: true as const,
|
||||
result: { kind: 'success' as const, text: `fixture:goal 已设置(${request.payload.sessionId})` },
|
||||
})
|
||||
}
|
||||
return ok(request, { matched: false as const })
|
||||
const text = name === undefined ? undefined : outcomes[name]
|
||||
if (name === undefined || text === undefined) return ok(request, { matched: false as const })
|
||||
const commandId = `fx-cmd-${logOf(id).length}`
|
||||
append(id, { type: 'command/run', data: { commandId, name, line, source: { kind: 'user' } } })
|
||||
append(id, { type: 'command/done', data: { commandId, kind: 'success', ...text === '' ? {} : { text } } })
|
||||
return ok(request, { matched: true as const })
|
||||
},
|
||||
},
|
||||
skills: {
|
||||
|
||||
@@ -14,7 +14,7 @@ export type {
|
||||
ApiProxy, SessionsApi, SessionSummary, HostApi, EventsApi, MuxFrame, HostFrame,
|
||||
ApprovalResponsePayload, QuestionResponsePayload, HistoryEntry, ToolEventView,
|
||||
ToolCallView, ToolResultView, WorkspaceApi, WorkspaceId, WorkspaceView,
|
||||
CommandsApi, CommandDescriptor, CommandExecuteResult, SkillsApi, SkillEntry,
|
||||
CommandsApi, CommandDescriptor, SkillsApi, SkillEntry,
|
||||
RpcRequest, RpcResponse, RpcResult, RpcError, RpcErrorCode,
|
||||
ClientRequest, ServerResponse, ServerRequest, ClientResponse, RpcMessage, RpcReceipt,
|
||||
IApiClient, SessionId, SessionEvent, ContentBlock, StreamChunk,
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// data source on a real clock; behavior tests need per-case responses and
|
||||
// deferred-controlled timing). Streams are hand pumps: pushMux/pushHost.
|
||||
import type {
|
||||
CommandDescriptor, CommandExecuteResult, HostFrame, IApiClient, MuxFrame,
|
||||
CommandDescriptor, HostFrame, IApiClient, MuxFrame,
|
||||
RpcRequest, RpcResponse, SessionId, SkillEntry,
|
||||
} from '../src/client/api.ts'
|
||||
import { RpcId } from '../src/client/api.ts'
|
||||
@@ -94,7 +94,7 @@ export class FakeApiClient implements IApiClient {
|
||||
// wire shapes so cases can program catalogs and skill lists without casts.
|
||||
onCommandList: (payload: unknown) => Promise<RpcResponse<{ commands: CommandDescriptor[] }>>
|
||||
= () => Promise.resolve(ok({ commands: [] }))
|
||||
onCommandExecute: (payload: unknown) => Promise<RpcResponse<{ matched: boolean; result?: CommandExecuteResult }>>
|
||||
onCommandExecute: (payload: unknown) => Promise<RpcResponse<{ matched: boolean }>>
|
||||
= () => Promise.resolve(ok({ matched: false }))
|
||||
onSkillList: (payload: unknown) => Promise<RpcResponse<{ skills: SkillEntry[] }>>
|
||||
= () => Promise.resolve(ok({ skills: [] }))
|
||||
|
||||
@@ -36,20 +36,36 @@ describe('createFixtureApi commands/skills', () => {
|
||||
expect(response.result).toMatchObject({ ok: false, error: { code: 'session-not-found' } })
|
||||
})
|
||||
|
||||
it('executes a known command line and reports matched with a result', async () => {
|
||||
it('executes a known command line: pure admission plus a mux-broadcast lifecycle pair', async () => {
|
||||
const api = createFixtureApi()
|
||||
const frames: unknown[] = []
|
||||
const abort = new AbortController()
|
||||
const stream = api.events.mux(req({}), abort.signal)
|
||||
const pump = (async () => {
|
||||
for await (const frame of stream) {
|
||||
frames.push(frame.payload)
|
||||
if (frames.filter(f => (f as { type: string }).type === 'session/event').length >= 2) abort.abort()
|
||||
}
|
||||
})()
|
||||
const response = await api.commands.execute(req({ sessionId: sid('fx-alpha'), line: '/echo hello world' }), signal)
|
||||
if (!response.result.ok) throw new Error('execute failed')
|
||||
expect(response.result.value.matched).toBe(true)
|
||||
expect(response.result.value.result).toEqual({ kind: 'success', text: 'hello world' })
|
||||
expect(response.result.value).toEqual({ matched: true })
|
||||
await pump
|
||||
const events = frames
|
||||
.filter((f): f is { type: string; event: { type: string; data: Record<string, unknown> } } => (f as { type: string }).type === 'session/event')
|
||||
.map(f => f.event)
|
||||
expect(events).toMatchObject([
|
||||
{ type: 'command/run', data: { name: 'echo', line: '/echo hello world', source: { kind: 'user' } } },
|
||||
{ type: 'command/done', data: { kind: 'success', text: 'hello world' } },
|
||||
])
|
||||
expect(events[0]?.data.commandId).toBe(events[1]?.data.commandId)
|
||||
})
|
||||
|
||||
it('addresses execute to the session (result text carries the id)', async () => {
|
||||
it('addresses execute to the session; an unknown session errs', async () => {
|
||||
const api = createFixtureApi()
|
||||
const hit = await api.commands.execute(req({ sessionId: sid('fx-alpha'), line: '/goal-fixture ship' }), signal)
|
||||
if (!hit.result.ok) throw new Error('execute failed')
|
||||
expect(hit.result.value.matched).toBe(true)
|
||||
expect(hit.result.value.result?.text).toContain('fx-alpha')
|
||||
|
||||
const missing = await api.commands.execute(req({ sessionId: sid('fx-nope'), line: '/goal-fixture ship' }), signal)
|
||||
expect(missing.result).toMatchObject({ ok: false, error: { code: 'session-not-found' } })
|
||||
|
||||
Reference in New Issue
Block a user