Merge remote-tracking branch 'origin/master' into feat/send-unify
# Conflicts: # examples/acp-agent/tests/snapshots/cordis-inspect-jsdoc/session.jsonl # examples/acp-agent/tests/snapshots/cordis-inspect-jsdoc/stdout.expected.jsonl # packages/cordis/tool-cordis/src/api-catalog.ts # packages/pty/pty-local/tests/index.spec.ts # packages/session-query/session-query/tests/tracing.spec.ts
This commit is contained in:
@@ -32,20 +32,23 @@ class StubSession implements PtyBackendSession {
|
||||
autoSettle = true
|
||||
rejectOperation = false
|
||||
closeGate: PromiseWithResolvers<undefined> | undefined
|
||||
viewport = 'command output'
|
||||
delta = 'live output'
|
||||
deltaTruncated = false
|
||||
|
||||
startSend(_request: PtySendRequest): PtySendOperation {
|
||||
let settle!: () => void
|
||||
let reject!: (error: unknown) => void
|
||||
let cancelled = false
|
||||
const done = new Promise<void>((resolve, rejectPromise) => { settle = resolve; reject = rejectPromise }).then(() => ({
|
||||
viewport: cancelled ? '^C' : 'command output',
|
||||
viewport: cancelled ? '^C' : this.viewport,
|
||||
waitReason: 'stdin_read' as const,
|
||||
sessionStatus: this.statusValue,
|
||||
truncated: false,
|
||||
}))
|
||||
const operation: PtySendOperation = {
|
||||
done,
|
||||
readOutput: () => ({ delta: 'live output', truncated: false }),
|
||||
readOutput: () => ({ delta: this.delta, truncated: this.deltaTruncated }),
|
||||
cancel: () => {
|
||||
if (cancelled) return false
|
||||
cancelled = true
|
||||
@@ -88,7 +91,13 @@ function stubBackend() {
|
||||
return { backend, sessions }
|
||||
}
|
||||
|
||||
async function setup(tasks: boolean) {
|
||||
async function setup(tasks: boolean, config: ToolPty.Config = {}) {
|
||||
const base = await setupBase(tasks)
|
||||
await base.ctx.plugin(ToolPty, config)
|
||||
return base
|
||||
}
|
||||
|
||||
async function setupBase(tasks: boolean) {
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(SystemPrompt)
|
||||
await ctx.plugin(ToolRegistry)
|
||||
@@ -100,7 +109,6 @@ async function setup(tasks: boolean) {
|
||||
await ctx.plugin(TaskService)
|
||||
await ctx.plugin(ToolTasks)
|
||||
}
|
||||
await ctx.plugin(ToolPty)
|
||||
return { ctx, stub, agent: fakeAgent(ctx, tasks ? 'with-tasks' : 'foreground') }
|
||||
}
|
||||
|
||||
@@ -291,6 +299,101 @@ describe('tool-pty foreground surface', () => {
|
||||
expect(ctx.tools.get('terminal_close')?.presentCall?.({ sessionId: 'pty-1' })).toMatchObject({ card: 'generic', title: 'Close terminal pty-1' })
|
||||
expect(ctx.tools.get('terminal_list')?.presentCall?.({})).toMatchObject({ card: 'generic', title: 'List terminal sessions' })
|
||||
})
|
||||
|
||||
it('configuration-gates background sends and validates the final result bound', async () => {
|
||||
const disabled = await setup(true, { enableRunInBackground: false })
|
||||
const definition = disabled.ctx.tools.get('terminal_send')
|
||||
expect(definition?.parameters).not.toHaveProperty('properties.run_in_background')
|
||||
expect(definition?.description).not.toContain('Background mode')
|
||||
await call(disabled.ctx, 'terminal_open', { type: 'stub' }, disabled.agent)
|
||||
expect((await call(disabled.ctx, 'terminal_send', {
|
||||
sessionId: 'pty-1', text: 'work', run_in_background: true,
|
||||
}, disabled.agent)).isError).toBe(true)
|
||||
|
||||
const defaults = await setupBase(false)
|
||||
ToolPty.apply(defaults.ctx)
|
||||
expect(defaults.ctx.tools.get('terminal_send')?.parameters).toHaveProperty('properties.run_in_background')
|
||||
|
||||
const invalid = await setupBase(false)
|
||||
expect(() => { ToolPty.apply(invalid.ctx, { maxResultBytes: 0 }) }).toThrow('maxResultBytes')
|
||||
expect(() => { ToolPty.apply(invalid.ctx, { maxResultBytes: 63 }) }).toThrow('at least 64')
|
||||
})
|
||||
|
||||
it('bounds normalized errors and preserves allocated ids at the minimum result cap', async () => {
|
||||
const { ctx, agent } = await setup(true, { maxResultBytes: 64 })
|
||||
const failed = await call(ctx, 'terminal_open', { type: 'x'.repeat(1_000) }, agent)
|
||||
expect(failed.isError).toBe(true)
|
||||
expect(Buffer.byteLength(text(failed))).toBeLessThanOrEqual(64)
|
||||
expect(text(failed)).toContain('[output truncated]')
|
||||
|
||||
const opened = await call(ctx, 'terminal_open', { type: 'stub', name: 'n'.repeat(1_000) }, agent)
|
||||
expect(text(opened)).toContain('pty-1')
|
||||
expect(Buffer.byteLength(text(opened))).toBeLessThanOrEqual(64)
|
||||
const background = await call(ctx, 'terminal_send', {
|
||||
sessionId: 'pty-1', text: 'work', run_in_background: true,
|
||||
}, agent)
|
||||
expect(text(background)).toContain('pty-send-1')
|
||||
expect(Buffer.byteLength(text(background))).toBeLessThanOrEqual(64)
|
||||
})
|
||||
|
||||
it('bounds terminal results after policy decisions and pipeline failures', async () => {
|
||||
const { ctx, agent } = await setup(false, { maxResultBytes: 64 })
|
||||
ctx.on('tools/pre-execute', async (exec, next) => {
|
||||
if (exec.name === 'terminal_list') return { kind: 'deny', reason: 'd'.repeat(1_000) }
|
||||
if (exec.name === 'terminal_signal') throw new Error(`pre failed: ${'p'.repeat(1_000)}`)
|
||||
return next()
|
||||
})
|
||||
ctx.on('tools/execute', async (exec, next) => {
|
||||
if (exec.name === 'terminal_close') throw new Error(`around failed: ${'e'.repeat(1_000)}`)
|
||||
return next()
|
||||
})
|
||||
ctx.on('tools/post-execute', async (exec, _result, next) => {
|
||||
if (exec.name === 'terminal_open') {
|
||||
return { kind: 'accept', content: [{ type: 'text', text: 'a'.repeat(1_000) }] }
|
||||
}
|
||||
if (exec.name === 'terminal_read') {
|
||||
return { kind: 'block', feedback: [{ type: 'text', text: 'b'.repeat(1_000) }] }
|
||||
}
|
||||
if (exec.name === 'terminal_send') throw new Error(`post failed: ${'o'.repeat(1_000)}`)
|
||||
return next()
|
||||
})
|
||||
|
||||
const denied = await call(ctx, 'terminal_list', {}, agent)
|
||||
expect(denied.isError).toBe(true)
|
||||
expect(Buffer.byteLength(text(denied))).toBeLessThanOrEqual(64)
|
||||
expect(text(denied)).toContain('[output truncated]')
|
||||
|
||||
const replaced = await call(ctx, 'terminal_open', { type: 'stub' }, agent)
|
||||
expect(replaced.isError).toBe(false)
|
||||
expect(Buffer.byteLength(text(replaced))).toBeLessThanOrEqual(64)
|
||||
expect(text(replaced)).toContain('[output truncated]')
|
||||
|
||||
const blocked = await call(ctx, 'terminal_read', { sessionId: 'pty-1' }, agent)
|
||||
expect(blocked.isError).toBe(true)
|
||||
expect(Buffer.byteLength(text(blocked))).toBeLessThanOrEqual(64)
|
||||
expect(text(blocked)).toContain('[output truncated]')
|
||||
|
||||
const failures = [
|
||||
await call(ctx, 'terminal_signal', { sessionId: 'pty-1', signal: 'SIGINT' }, agent),
|
||||
await call(ctx, 'terminal_close', { sessionId: 'pty-1' }, agent),
|
||||
await call(ctx, 'terminal_send', { sessionId: 'pty-1', text: 'work' }, agent),
|
||||
]
|
||||
for (const failure of failures) {
|
||||
expect(failure.isError).toBe(true)
|
||||
expect(Buffer.byteLength(text(failure))).toBeLessThanOrEqual(64)
|
||||
expect(text(failure)).toContain('[output truncated]')
|
||||
}
|
||||
})
|
||||
|
||||
it('leaves a structured around-dispatch failure unchanged', async () => {
|
||||
const { ctx, agent } = await setup(false, { maxResultBytes: 64 })
|
||||
ctx.on('tools/execute', async (exec, next) => exec.name === 'terminal_list'
|
||||
? { content: [], isError: true, error: { message: 'structured failure' } }
|
||||
: next())
|
||||
const result = await call(ctx, 'terminal_list', {}, agent)
|
||||
expect(result.isError).toBe(true)
|
||||
expect(result.content).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
describe('tool-pty task integration', () => {
|
||||
@@ -305,6 +408,23 @@ describe('tool-pty task integration', () => {
|
||||
expect(text(output)).toContain('[status: completed, wait: stdin_read]')
|
||||
})
|
||||
|
||||
it('bounds foreground and background results after terminal and task metadata', async () => {
|
||||
const { ctx, agent, stub } = await setup(true, { maxResultBytes: 64 })
|
||||
await call(ctx, 'terminal_open', { type: 'stub' }, agent)
|
||||
stub.sessions[0]!.viewport = '界'.repeat(100)
|
||||
const foreground = await call(ctx, 'terminal_send', { sessionId: 'pty-1', text: 'foreground' }, agent)
|
||||
expect(Buffer.byteLength(text(foreground))).toBeLessThanOrEqual(64)
|
||||
|
||||
stub.sessions[0]!.delta = '界'.repeat(100)
|
||||
stub.sessions[0]!.deltaTruncated = true
|
||||
await call(ctx, 'terminal_send', { sessionId: 'pty-1', text: 'background', run_in_background: true }, agent)
|
||||
const background = await call(ctx, 'task_output', { task_id: 'pty-send-1', wait: true }, agent)
|
||||
expect(Buffer.byteLength(text(background))).toBeLessThanOrEqual(64)
|
||||
expect(text(background)).toContain('[status: completed')
|
||||
expect(text(background).match(/\[output truncated\]/g)).toHaveLength(1)
|
||||
expect(text(background)).toContain('[output truncated]\n[status: completed')
|
||||
})
|
||||
|
||||
it('rejects pre-aborted background calls, maps task cancellation, and contains operation failure', async () => {
|
||||
const { ctx, agent, stub } = await setup(true)
|
||||
await call(ctx, 'terminal_open', { type: 'stub' }, agent)
|
||||
|
||||
Reference in New Issue
Block a user