|
|
@@ -44,7 +44,7 @@ describe('parseCommand()', () => {
|
|
|
})
|
|
|
|
|
|
describe('CommandService', () => {
|
|
|
- it('lists immutable global descriptors with default surfaces and ACP input metadata', async () => {
|
|
|
+ it('lists immutable global descriptors with input metadata', async () => {
|
|
|
const ctx = await mount()
|
|
|
const { agent } = await mintAgentScope(ctx, 'a')
|
|
|
const definition: CommandDefinition = {
|
|
|
@@ -55,20 +55,17 @@ describe('CommandService', () => {
|
|
|
}
|
|
|
ctx.commands.register(definition)
|
|
|
|
|
|
- const listed = ctx.commands.list(agent, 'acp')
|
|
|
+ const listed = ctx.commands.list(agent)
|
|
|
expect(listed).toEqual([{
|
|
|
name: 'inspect',
|
|
|
description: 'Inspect state',
|
|
|
input: { hint: '<target>' },
|
|
|
- surfaces: ['tui', 'acp'],
|
|
|
}])
|
|
|
expect(Object.isFrozen(listed)).toBe(true)
|
|
|
expect(Object.isFrozen(listed[0])).toBe(true)
|
|
|
expect(Object.isFrozen(listed[0]?.input)).toBe(true)
|
|
|
- expect(Object.isFrozen(listed[0]?.surfaces)).toBe(true)
|
|
|
- expect(ctx.commands.find(agent, 'tui', 'inspect')).toMatchObject({ name: 'inspect' })
|
|
|
- expect(ctx.commands.find(agent, 'other', 'inspect')).toBeUndefined()
|
|
|
- expect(ctx.commands.find(agent, 'tui', 'missing')).toBeUndefined()
|
|
|
+ expect(ctx.commands.find(agent, 'inspect')).toMatchObject({ name: 'inspect' })
|
|
|
+ expect(ctx.commands.find(agent, 'missing')).toBeUndefined()
|
|
|
})
|
|
|
|
|
|
it('sorts distinct effective command names', async () => {
|
|
|
@@ -77,7 +74,7 @@ describe('CommandService', () => {
|
|
|
ctx.commands.register(command('zeta'))
|
|
|
ctx.commands.register(command('alpha'))
|
|
|
ctx.commands.register(command('middle'))
|
|
|
- expect(ctx.commands.list(agent, 'tui').map(item => item.name)).toEqual(['alpha', 'middle', 'zeta'])
|
|
|
+ expect(ctx.commands.list(agent).map(item => item.name)).toEqual(['alpha', 'middle', 'zeta'])
|
|
|
})
|
|
|
|
|
|
it('uses agent-scoped shadows and removes them with their scope', async () => {
|
|
|
@@ -85,17 +82,16 @@ describe('CommandService', () => {
|
|
|
const { scope, agent } = await mintAgentScope(ctx, 'a')
|
|
|
const other = { id: 'other' as SessionId } as Agent
|
|
|
ctx.commands.register(command('shared', 'global'))
|
|
|
- scope.ctx.commands.register({ ...command('shared', 'scoped'), surfaces: ['tui'] })
|
|
|
+ scope.ctx.commands.register(command('shared', 'scoped'))
|
|
|
|
|
|
- expect(ctx.commands.list(agent, 'tui').map(item => item.name)).toEqual(['shared'])
|
|
|
- expect(ctx.commands.list(agent, 'acp')).toEqual([])
|
|
|
- expect(ctx.commands.find(agent, 'tui', 'shared')?.handler).toBeDefined()
|
|
|
- expect(ctx.commands.list(other, 'acp').map(item => item.name)).toEqual(['shared'])
|
|
|
- expect(await ctx.commands.execute(agent, 'tui', '/shared', new AbortController().signal))
|
|
|
+ expect(ctx.commands.list(agent).map(item => item.name)).toEqual(['shared'])
|
|
|
+ expect(ctx.commands.find(agent, 'shared')?.handler).toBeDefined()
|
|
|
+ expect(ctx.commands.list(other).map(item => item.name)).toEqual(['shared'])
|
|
|
+ expect(await ctx.commands.execute(agent, '/shared', new AbortController().signal))
|
|
|
.toEqual({ kind: 'success', text: 'scoped' })
|
|
|
|
|
|
await scope.dispose()
|
|
|
- expect((await ctx.commands.execute(agent, 'tui', '/shared', new AbortController().signal))?.text).toBe('global')
|
|
|
+ expect((await ctx.commands.execute(agent, '/shared', new AbortController().signal))?.text).toBe('global')
|
|
|
})
|
|
|
|
|
|
it('rejects duplicates within one layer while allowing a scoped shadow', async () => {
|
|
|
@@ -124,14 +120,14 @@ describe('CommandService', () => {
|
|
|
ctx.on('commands/change', afterFailures)
|
|
|
const removeContained = ctx.commands.register(command('contained'))
|
|
|
const { agent } = await mintAgentScope(ctx, 'a')
|
|
|
- expect(ctx.commands.find(agent, 'tui', 'contained')).toBeDefined()
|
|
|
+ expect(ctx.commands.find(agent, 'contained')).toBeDefined()
|
|
|
expect(afterFailures).toHaveBeenCalledTimes(1)
|
|
|
await vi.waitFor(() => {
|
|
|
expect(warn).toHaveBeenCalledWith('commands/change listener threw: Error: observer threw')
|
|
|
expect(warn).toHaveBeenCalledWith('commands/change listener rejected: Error: observer rejected')
|
|
|
})
|
|
|
removeContained()
|
|
|
- expect(ctx.commands.find(agent, 'tui', 'contained')).toBeUndefined()
|
|
|
+ expect(ctx.commands.find(agent, 'contained')).toBeUndefined()
|
|
|
expect(afterFailures).toHaveBeenCalledTimes(2)
|
|
|
})
|
|
|
|
|
|
@@ -155,22 +151,20 @@ describe('CommandService', () => {
|
|
|
const ctx = await mount()
|
|
|
const { agent } = await mintAgentScope(ctx, 'a')
|
|
|
const seen = vi.fn(() => ({ kind: 'success' as const, text: 'ok' }))
|
|
|
- ctx.commands.register({ name: 'run', description: 'Run it', surfaces: ['acp'], handler: seen })
|
|
|
+ ctx.commands.register({ name: 'run', description: 'Run it', handler: seen })
|
|
|
const controller = new AbortController()
|
|
|
|
|
|
- const result = await ctx.commands.execute(agent, 'acp', '/run untouched ', controller.signal)
|
|
|
+ const result = await ctx.commands.execute(agent, '/run untouched ', controller.signal)
|
|
|
|
|
|
expect(result).toEqual({ kind: 'success', text: 'ok' })
|
|
|
expect(Object.isFrozen(result)).toBe(true)
|
|
|
expect(seen).toHaveBeenCalledWith(expect.objectContaining({
|
|
|
agent,
|
|
|
- surface: 'acp',
|
|
|
rawInput: ' untouched ',
|
|
|
signal: controller.signal,
|
|
|
}))
|
|
|
- await expect(ctx.commands.execute(agent, 'tui', '/run', controller.signal)).resolves.toBeUndefined()
|
|
|
- await expect(ctx.commands.execute(agent, 'acp', 'run', controller.signal)).resolves.toBeUndefined()
|
|
|
- await expect(ctx.commands.execute(agent, 'acp', '/missing', controller.signal)).resolves.toBeUndefined()
|
|
|
+ await expect(ctx.commands.execute(agent, 'run', controller.signal)).resolves.toBeUndefined()
|
|
|
+ await expect(ctx.commands.execute(agent, '/missing', controller.signal)).resolves.toBeUndefined()
|
|
|
})
|
|
|
|
|
|
it('stops awaiting an aborted handler and handles an already-aborted signal', async () => {
|
|
|
@@ -183,18 +177,18 @@ describe('CommandService', () => {
|
|
|
handler: () => new Promise((resolve) => { release = resolve }),
|
|
|
})
|
|
|
const running = new AbortController()
|
|
|
- const promise = ctx.commands.execute(agent, 'tui', '/wait', running.signal)
|
|
|
+ const promise = ctx.commands.execute(agent, '/wait', running.signal)
|
|
|
running.abort('operator cancelled command')
|
|
|
await expect(promise).rejects.toThrow('operator cancelled command')
|
|
|
release({ kind: 'success', text: 'late' })
|
|
|
|
|
|
const already = new AbortController()
|
|
|
already.abort(new Error('already gone'))
|
|
|
- await expect(ctx.commands.execute(agent, 'tui', '/wait', already.signal)).rejects.toThrow('already gone')
|
|
|
+ await expect(ctx.commands.execute(agent, '/wait', already.signal)).rejects.toThrow('already gone')
|
|
|
|
|
|
const defaultReason = new AbortController()
|
|
|
defaultReason.abort({ source: 'test' })
|
|
|
- await expect(ctx.commands.execute(agent, 'tui', '/wait', defaultReason.signal)).rejects.toThrow('command aborted')
|
|
|
+ await expect(ctx.commands.execute(agent, '/wait', defaultReason.signal)).rejects.toThrow('command aborted')
|
|
|
})
|
|
|
|
|
|
it('propagates an asynchronously rejected handler', async () => {
|
|
|
@@ -205,7 +199,7 @@ describe('CommandService', () => {
|
|
|
description: 'Reject',
|
|
|
handler: () => Promise.reject(new Error('handler rejected')),
|
|
|
})
|
|
|
- await expect(ctx.commands.execute(agent, 'tui', '/reject', new AbortController().signal))
|
|
|
+ await expect(ctx.commands.execute(agent, '/reject', new AbortController().signal))
|
|
|
.rejects.toThrow('handler rejected')
|
|
|
|
|
|
ctx.commands.register({
|
|
|
@@ -214,7 +208,7 @@ describe('CommandService', () => {
|
|
|
// eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors -- exercise untyped plugin normalization
|
|
|
handler: () => Promise.reject('not an Error'),
|
|
|
})
|
|
|
- await expect(ctx.commands.execute(agent, 'tui', '/reject-value', new AbortController().signal))
|
|
|
+ await expect(ctx.commands.execute(agent, '/reject-value', new AbortController().signal))
|
|
|
.rejects.toThrow('command handler rejected with a non-Error value: not an Error')
|
|
|
|
|
|
const hostile = { toString(): string { throw new Error('cannot render') } }
|
|
|
@@ -224,7 +218,7 @@ describe('CommandService', () => {
|
|
|
// eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors -- exercise hostile plugin normalization
|
|
|
handler: () => Promise.reject(hostile),
|
|
|
})
|
|
|
- await expect(ctx.commands.execute(agent, 'tui', '/reject-hostile', new AbortController().signal))
|
|
|
+ await expect(ctx.commands.execute(agent, '/reject-hostile', new AbortController().signal))
|
|
|
.rejects.toMatchObject({
|
|
|
message: 'command handler rejected with a non-Error value: <unrenderable thrown value>',
|
|
|
cause: hostile,
|
|
|
@@ -243,7 +237,7 @@ describe('CommandService', () => {
|
|
|
return { kind: 'success' }
|
|
|
},
|
|
|
})
|
|
|
- await expect(ctx.commands.execute(agent, 'tui', '/self-abort', controller.signal))
|
|
|
+ await expect(ctx.commands.execute(agent, '/self-abort', controller.signal))
|
|
|
.rejects.toThrow('aborted in handler')
|
|
|
})
|
|
|
|
|
|
@@ -255,7 +249,7 @@ describe('CommandService', () => {
|
|
|
description: 'Denied',
|
|
|
handler: () => ({ kind: 'error', text: 'not now' }),
|
|
|
})
|
|
|
- const result = await ctx.commands.execute(agent, 'tui', '/denied', new AbortController().signal)
|
|
|
+ const result = await ctx.commands.execute(agent, '/denied', new AbortController().signal)
|
|
|
expect(result).toEqual({ kind: 'error', text: 'not now' })
|
|
|
expect(Object.isFrozen(result)).toBe(true)
|
|
|
|
|
|
@@ -264,7 +258,7 @@ describe('CommandService', () => {
|
|
|
description: 'No output',
|
|
|
handler: () => ({ kind: 'success' }),
|
|
|
})
|
|
|
- const silent = await ctx.commands.execute(agent, 'tui', '/silent', new AbortController().signal)
|
|
|
+ const silent = await ctx.commands.execute(agent, '/silent', new AbortController().signal)
|
|
|
expect(silent).toEqual({ kind: 'success' })
|
|
|
expect(Object.isFrozen(silent)).toBe(true)
|
|
|
})
|
|
|
@@ -273,9 +267,6 @@ describe('CommandService', () => {
|
|
|
[{ ...command('Bad') }, /command name/],
|
|
|
[{ ...command('empty-description'), description: ' ' }, /description/],
|
|
|
[{ ...command('empty-hint'), input: { hint: '' } }, /input hint/],
|
|
|
- [{ ...command('no-surface'), surfaces: [] }, /at least one surface/],
|
|
|
- [{ ...command('bad-surface'), surfaces: ['ACP'] }, /surface/],
|
|
|
- [{ ...command('duplicate-surface'), surfaces: ['tui', 'tui'] }, /duplicated/],
|
|
|
[{ ...command('bad-handler'), handler: undefined }, /handler/],
|
|
|
] as const)('rejects invalid definition %#', async (definition, expected) => {
|
|
|
const ctx = await mount()
|
|
|
@@ -298,6 +289,6 @@ describe('CommandService', () => {
|
|
|
description: 'Broken',
|
|
|
handler: () => output as never,
|
|
|
})
|
|
|
- await expect(ctx.commands.execute(agent, 'tui', '/broken', new AbortController().signal)).rejects.toThrow(expected)
|
|
|
+ await expect(ctx.commands.execute(agent, '/broken', new AbortController().signal)).rejects.toThrow(expected)
|
|
|
})
|
|
|
})
|