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:
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'))
|
||||
|
||||
Reference in New Issue
Block a user