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:
imccyu
2026-07-27 23:06:49 +08:00
parent ba928c5517
commit 4ddec0ba2f
17 files changed
+110 -90

No files matched your search

@@ -31,7 +31,7 @@ const S2_CMDS: CommandDescriptor[] = [
{ name: 'attach', description: 'scoped shadow', input: { hint: 'path' } },
]
type ExecuteValue = { matched: boolean; result?: { kind: 'success' | 'error'; text?: string } }
type ExecuteValue = { matched: boolean }
interface BenchOptions {
/** Scripted catalog per list payload; default serves the fixed catalogs by session. */
@@ -361,16 +361,18 @@ describe('matchEnter (enter column)', () => {
})
describe('execute payload', () => {
it('claim.submit addresses the session and maps the detached result', async () => {
it('claim.submit addresses the session; admitted outcomes stay off the composer (flow card owns them)', async () => {
const { source, warm, executeCalls } = await bench({
execute: () => Promise.resolve({ matched: true, result: { kind: 'success', text: 'goal set' } }),
execute: () => Promise.resolve({ matched: true }),
})
await warm(proj('s1'))
const outcome = source.matchSpace!(proj('s1'), '/goal')
if (outcome === undefined || outcome === 'handled' || !('claim' in outcome)) throw new Error('expected claim')
const settled = await outcome.claim.submit('ship it', new Context())
expect(executeCalls).toEqual([{ sessionId: sid('s1'), line: '/goal ship it' }])
expect(settled).toEqual({ kind: 'success', text: 'goal set' })
// Pure admission: no outcome text ever rides the submit result — the
// durable command lifecycle events render the outcome in the flow.
expect(settled).toEqual({ kind: 'success' })
})
it('maps matched:false to an error outcome and a matched bare result to success', async () => {
@@ -389,33 +391,29 @@ describe('execute payload', () => {
})
})
describe('detached result notices', () => {
describe('detached admission notices', () => {
const flush = () => new Promise(resolve => setTimeout(resolve, 0))
it('success text → info; error result → error; rejection → error, all on the triggering session', async () => {
let mode: 'info' | 'error' | 'reject' = 'info'
it('admitted outcomes stay silent; admission miss and transport rejection notice as errors', async () => {
let mode: 'admitted' | 'miss' | 'reject' = 'admitted'
const { source, mint, warm, notices } = await bench({
execute: () => {
if (mode === 'reject') return Promise.reject(new Error('network down'))
return Promise.resolve({
matched: true,
result: mode === 'info'
? { kind: 'success' as const, text: 'compacted 12 messages' }
: { kind: 'error' as const, text: 'plan mode refused' },
})
return Promise.resolve({ matched: mode === 'admitted' })
},
})
mint('s1')
await warm(proj('s1'))
// Admitted: the durable lifecycle events own the outcome — no notice.
menuPick(source, 'plan', proj('s1'))
await flush()
expect(notices).toEqual([{ scope: sid('s1'), level: 'info', text: 'compacted 12 messages' }])
expect(notices).toEqual([])
notices.length = 0
mode = 'error'
// Admission miss (matched:false): immediate composer feedback stays.
mode = 'miss'
await source.matchEnter!(proj('s1'), '/plan', new AbortController().signal)
await flush()
expect(notices).toEqual([{ scope: sid('s1'), level: 'error', text: 'plan mode refused' }])
expect(notices).toEqual([{ scope: sid('s1'), level: 'error', text: 'unknown or malformed command: /plan' }])
notices.length = 0
mode = 'reject'
@@ -424,9 +422,9 @@ describe('detached result notices', () => {
expect(notices).toEqual([{ scope: sid('s1'), level: 'error', text: 'network down' }])
})
it('success without text stays silent; a torn-down scope drops the notice', async () => {
it('a torn-down scope drops the failure notice', async () => {
const { source, warm, notices } = await bench({
execute: () => Promise.resolve({ matched: true, result: { kind: 'success' as const, text: 'orphan' } }),
execute: () => Promise.reject(new Error('orphan failure')),
})
await warm(proj('ghost')) // never minted: scopeFor misses
menuPick(source, 'plan', proj('ghost'))