refactor(agent): unify sourced message delivery
This commit is contained in:
@@ -73,7 +73,7 @@ describe('hooks-codex bridge', () => {
|
||||
let ran = false
|
||||
ctx.tools.register(defineContentToolFixture({ name: 'Bash', description: 'b', parameters: { command: { type: 'string' } }, async execute() { ran = true; return [{ type: 'text', text: 'no' }] } }))
|
||||
const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' })
|
||||
agent.followup([{ type: 'text', text: 'run ls' }])
|
||||
agent.followup({ content: [{ type: 'text', text: 'run ls' }], source: { kind: 'user' } })
|
||||
await waitForIdle(ctx, agent)
|
||||
|
||||
expect(ran).toBe(false)
|
||||
@@ -94,7 +94,7 @@ describe('hooks-codex bridge', () => {
|
||||
const adapter = new MockAdapter([textResponse('first answer'), textResponse('second answer after goal')])
|
||||
const ctx = await harness(dir, adapter)
|
||||
const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' })
|
||||
agent.followup([{ type: 'text', text: 'go' }])
|
||||
agent.followup({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } })
|
||||
await waitForIdle(ctx, agent)
|
||||
|
||||
expect(adapter.requests).toHaveLength(2)
|
||||
@@ -111,7 +111,7 @@ describe('hooks-codex bridge', () => {
|
||||
const adapter = new MockAdapter([textResponse('must not run')])
|
||||
const ctx = await harness(dir, adapter)
|
||||
const agent = ctx.agentLoop.create(SessionId('cancel-prompt-hook'), { provider: 'mock', model: 'mock' })
|
||||
agent.followup([{ type: 'text', text: 'cancel the hook' }])
|
||||
agent.followup({ content: [{ type: 'text', text: 'cancel the hook' }], source: { kind: 'user' } })
|
||||
await waitFor(() => existsSync(marker))
|
||||
const pid = Number(readFileSync(pidFile, 'utf8').trim())
|
||||
|
||||
@@ -122,7 +122,7 @@ describe('hooks-codex bridge', () => {
|
||||
expect(() => process.kill(pid, 0)).toThrow()
|
||||
expect(adapter.requests).toHaveLength(0)
|
||||
expect(events(agent).some(event => event.type === 'turn/start')).toBe(false)
|
||||
expect(events(agent).some(event => event.type === 'hook/result' && event.data.point === 'UserPromptSubmit')).toBe(true)
|
||||
expect(events(agent).some(event => event.type === 'hook/invoked' || event.type === 'hook/result')).toBe(false)
|
||||
})
|
||||
|
||||
it('only the five bridge-supported Codex events are honored — a SubagentStop entry is ignored', async () => {
|
||||
@@ -133,7 +133,7 @@ describe('hooks-codex bridge', () => {
|
||||
const adapter = new MockAdapter([textResponse('fine')])
|
||||
const ctx = await harness(dir, adapter)
|
||||
const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' })
|
||||
agent.followup([{ type: 'text', text: 'go' }])
|
||||
agent.followup({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } })
|
||||
await waitForIdle(ctx, agent)
|
||||
expect(adapter.requests).toHaveLength(1)
|
||||
})
|
||||
@@ -143,7 +143,7 @@ describe('hooks-codex bridge', () => {
|
||||
const adapter = new MockAdapter([textResponse('ok')])
|
||||
const ctx = await harness(dir, adapter)
|
||||
const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' })
|
||||
agent.followup([{ type: 'text', text: 'go' }])
|
||||
agent.followup({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } })
|
||||
await waitForIdle(ctx, agent)
|
||||
expect(adapter.requests).toHaveLength(1)
|
||||
})
|
||||
@@ -163,7 +163,7 @@ describe('hooks-codex bridge', () => {
|
||||
await fiber.dispose()
|
||||
ctx.llm.registerAdapter(['mock'], adapter)
|
||||
const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' })
|
||||
agent.followup([{ type: 'text', text: 'go' }])
|
||||
agent.followup({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } })
|
||||
await waitForIdle(ctx, agent)
|
||||
expect(adapter.requests).toHaveLength(1) // not blocked → the listener is gone
|
||||
expect(events(agent).some(e => e.type === 'hook/invoked')).toBe(false) // no hook ran
|
||||
|
||||
@@ -65,7 +65,7 @@ export function defineCoverageCases(groups: CoverageGroup | readonly CoverageGro
|
||||
const ctx = await harness(path, adapter, { ...sessionRoot !== undefined ? { sessionRoot } : {} })
|
||||
ctx.tools.register(defineContentToolFixture({ name: 'echo', description: 'e', parameters: {}, async execute() { return [{ type: 'text', text: 'ok' }] } }))
|
||||
const agent = ctx.agentLoop.create(SessionId('transcript'), { provider: 'mock', model: 'mock' })
|
||||
agent.followup([{ type: 'text', text: 'go' }])
|
||||
agent.followup({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } })
|
||||
await waitForIdle(ctx, agent)
|
||||
return {
|
||||
payload: JSON.parse(readFileSync(cap, 'utf8')) as { transcript_path: string | null },
|
||||
@@ -84,7 +84,7 @@ export function defineCoverageCases(groups: CoverageGroup | readonly CoverageGro
|
||||
const adapter = new MockAdapter([textResponse('no')])
|
||||
const ctx = await harness(join(d, 'hooks.json'), adapter)
|
||||
const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' })
|
||||
agent.followup([{ type: 'text', text: 'go' }]); await waitForIdle(ctx, agent)
|
||||
agent.followup({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } }); await waitForIdle(ctx, agent)
|
||||
expect(adapter.requests).toHaveLength(0)
|
||||
expect(events(agent).some(e => e.type === 'turn/start')).toBe(false)
|
||||
})
|
||||
@@ -95,7 +95,7 @@ export function defineCoverageCases(groups: CoverageGroup | readonly CoverageGro
|
||||
const adapter = new MockAdapter([textResponse('ok')])
|
||||
const ctx = await harness(join(d, 'hooks.json'), adapter)
|
||||
const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' })
|
||||
agent.followup([{ type: 'text', text: 'go' }]); await waitForIdle(ctx, agent)
|
||||
agent.followup({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } }); await waitForIdle(ctx, agent)
|
||||
expect(JSON.stringify(adapter.requests[0]!.messages)).toContain('ctx-x')
|
||||
})
|
||||
|
||||
@@ -108,7 +108,7 @@ export function defineCoverageCases(groups: CoverageGroup | readonly CoverageGro
|
||||
const ctx = await harness(join(d, 'hooks.json'), adapter)
|
||||
ctx.on('agent/prompt-submit', async () => ({ kind: 'block' as const, reason: 'policy veto' }))
|
||||
const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' })
|
||||
agent.followup([{ type: 'text', text: 'go' }]); await waitForIdle(ctx, agent)
|
||||
agent.followup({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } }); await waitForIdle(ctx, agent)
|
||||
expect(adapter.requests).toHaveLength(0)
|
||||
expect(events(agent).some(e => e.type === 'user/message')).toBe(false)
|
||||
expect(events(agent).some(e => e.type === 'turn/start')).toBe(false)
|
||||
@@ -128,7 +128,7 @@ export function defineCoverageCases(groups: CoverageGroup | readonly CoverageGro
|
||||
}],
|
||||
}))
|
||||
const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' })
|
||||
agent.followup([{ type: 'text', text: 'go' }]); await waitForIdle(ctx, agent)
|
||||
agent.followup({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } }); await waitForIdle(ctx, agent)
|
||||
const req = JSON.stringify(adapter.requests[0]!.messages)
|
||||
expect(req).toContain('from-bridge')
|
||||
expect(req).toContain('from-downstream')
|
||||
@@ -150,7 +150,7 @@ export function defineCoverageCases(groups: CoverageGroup | readonly CoverageGro
|
||||
ctx.tools.register(defineContentToolFixture({ name: 'echo', description: 'e', parameters: {}, async execute() { return [{ type: 'text', text: 'ok' }] } }))
|
||||
ctx.on('tools/post-execute', async () => ({ kind: 'accept' as const, value: [{ type: 'text' as const, text: 'rewritten-result' }] }))
|
||||
const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' })
|
||||
agent.followup([{ type: 'text', text: 'go' }]); await waitForIdle(ctx, agent)
|
||||
agent.followup({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } }); await waitForIdle(ctx, agent)
|
||||
const result = events(agent).find(e => e.type === 'tool/result')
|
||||
expect(result?.type === 'tool/result' && result.data.content.some(b => b.type === 'text' && b.text === 'rewritten-result')).toBe(true)
|
||||
expect(events(agent).some(e => e.type === 'user/message' && e.data.source.kind !== 'user' && e.data.content.some(b => b.type === 'text' && b.text.includes('bridge-note')))).toBe(true)
|
||||
@@ -170,7 +170,7 @@ export function defineCoverageCases(groups: CoverageGroup | readonly CoverageGro
|
||||
}],
|
||||
}))
|
||||
const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' })
|
||||
agent.followup([{ type: 'text', text: 'go' }]); await waitForIdle(ctx, agent)
|
||||
agent.followup({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } }); await waitForIdle(ctx, agent)
|
||||
|
||||
const contexts = events(agent).filter(event => event.type === 'user/message' && event.data.source.kind !== 'user')
|
||||
expect(contexts.map(event => event.type === 'user/message' && event.data.source)).toEqual([
|
||||
@@ -187,7 +187,7 @@ export function defineCoverageCases(groups: CoverageGroup | readonly CoverageGro
|
||||
ctx.tools.register(defineContentToolFixture({ name: 'echo', description: 'e', parameters: {}, async execute() { return [{ type: 'text', text: 'ok' }] } }))
|
||||
ctx.on('tools/post-execute', async () => ({ kind: 'block' as const, feedback: [{ type: 'text' as const, text: 'downstream-block' }] }))
|
||||
const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' })
|
||||
agent.followup([{ type: 'text', text: 'go' }]); await waitForIdle(ctx, agent)
|
||||
agent.followup({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } }); await waitForIdle(ctx, agent)
|
||||
const result = events(agent).find(e => e.type === 'tool/result')
|
||||
expect(result?.type === 'tool/result' && result.data.isError).toBe(true)
|
||||
expect(result?.type === 'tool/result' && result.data.content.some(b => b.type === 'text' && b.text.includes('downstream-block'))).toBe(true)
|
||||
@@ -202,7 +202,7 @@ export function defineCoverageCases(groups: CoverageGroup | readonly CoverageGro
|
||||
const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' })
|
||||
await waitFor(() => events(agent).some(e => e.type === 'user/message'
|
||||
&& e.data.content.some(b => b.type === 'text' && b.text.includes('start-ctx'))))
|
||||
agent.followup([{ type: 'text', text: 'go' }]); await waitForIdle(ctx, agent)
|
||||
agent.followup({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } }); await waitForIdle(ctx, agent)
|
||||
expect(JSON.stringify(adapter.requests[0]!.messages)).toContain('start-ctx')
|
||||
})
|
||||
|
||||
@@ -213,7 +213,7 @@ export function defineCoverageCases(groups: CoverageGroup | readonly CoverageGro
|
||||
const ctx = await harness(join(d, 'hooks.json'), adapter)
|
||||
ctx.tools.register(defineContentToolFixture({ name: 'Bash', description: 'b', parameters: { command: { type: 'string' } }, async execute() { return [{ type: 'text', text: 'ok' }] } }))
|
||||
const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' })
|
||||
agent.followup([{ type: 'text', text: 'go' }]); await waitForIdle(ctx, agent)
|
||||
agent.followup({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } }); await waitForIdle(ctx, agent)
|
||||
const r = events(agent).find(e => e.type === 'tool/result')
|
||||
expect(r?.type === 'tool/result' && r.data.isError).toBe(true)
|
||||
expect(r?.type === 'tool/result' && r.data.content.some(b => b.type === 'text' && b.text.includes('blocked by PostToolUse hook'))).toBe(true)
|
||||
@@ -226,7 +226,7 @@ export function defineCoverageCases(groups: CoverageGroup | readonly CoverageGro
|
||||
const ctx = await harness(join(d, 'hooks.json'), adapter)
|
||||
ctx.tools.register(defineContentToolFixture({ name: 'Bash', description: 'b', parameters: { command: { type: 'string' } }, async execute() { return [{ type: 'text', text: 'ok' }] } }))
|
||||
const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' })
|
||||
agent.followup([{ type: 'text', text: 'go' }]); await waitForIdle(ctx, agent)
|
||||
agent.followup({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } }); await waitForIdle(ctx, agent)
|
||||
expect(events(agent).some(e => e.type === 'user/message' && e.data.source.kind !== 'user' && e.data.content.some(b => b.type === 'text' && b.text.includes('post-ctx')))).toBe(true)
|
||||
})
|
||||
})
|
||||
@@ -240,7 +240,7 @@ export function defineCoverageCases(groups: CoverageGroup | readonly CoverageGro
|
||||
let ran = false
|
||||
ctx.tools.register(defineContentToolFixture({ name: 'Bash', description: 'b', parameters: {}, async execute() { ran = true; return [{ type: 'text', text: 'ok' }] } }))
|
||||
const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' })
|
||||
agent.followup([{ type: 'text', text: 'go' }]); await waitForIdle(ctx, agent)
|
||||
agent.followup({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } }); await waitForIdle(ctx, agent)
|
||||
expect(ran).toBe(true) // clean-exit hook allows; commandOf returned ''
|
||||
})
|
||||
|
||||
@@ -251,7 +251,7 @@ export function defineCoverageCases(groups: CoverageGroup | readonly CoverageGro
|
||||
const ctx = await harness(join(d, 'hooks.json'), adapter)
|
||||
ctx.tools.register(defineContentToolFixture({ name: 'Bash', description: 'b', parameters: { command: { type: 'string' } }, async execute() { return [{ type: 'text', text: 'ok' }] } }))
|
||||
const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' })
|
||||
agent.followup([{ type: 'text', text: 'go' }]); await waitForIdle(ctx, agent)
|
||||
agent.followup({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } }); await waitForIdle(ctx, agent)
|
||||
const res = events(agent).find(e => e.type === 'hook/result')
|
||||
expect(res?.type === 'hook/result' && res.data.exitCode).toBe(0)
|
||||
expect(res?.type === 'hook/result' && 'stderrSummary' in res.data).toBe(false)
|
||||
@@ -264,7 +264,7 @@ export function defineCoverageCases(groups: CoverageGroup | readonly CoverageGro
|
||||
const ctx = await harness(join(d, 'hooks.json'), adapter)
|
||||
ctx.tools.register(defineContentToolFixture({ name: 'Bash', description: 'b', parameters: { command: { type: 'string' } }, async execute() { return [{ type: 'text', text: 'ok' }] } }))
|
||||
const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' })
|
||||
agent.followup([{ type: 'text', text: 'go' }]); await waitForIdle(ctx, agent)
|
||||
agent.followup({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } }); await waitForIdle(ctx, agent)
|
||||
const res = events(agent).find(e => e.type === 'hook/result')
|
||||
expect(res?.type === 'hook/result' && res.data.stderrSummary?.endsWith('…')).toBe(true)
|
||||
expect(res?.type === 'hook/result' && res.data.stderrSummary?.length).toBe(501) // default 500-char cap + ellipsis
|
||||
@@ -287,7 +287,7 @@ export function defineCoverageCases(groups: CoverageGroup | readonly CoverageGro
|
||||
const ctx = await harness(join(d, 'hooks.json'), adapter, { stderrSummaryMaxChars: 40 })
|
||||
ctx.tools.register(defineContentToolFixture({ name: 'Bash', description: 'b', parameters: { command: { type: 'string' } }, async execute() { return [{ type: 'text', text: 'ok' }] } }))
|
||||
const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' })
|
||||
agent.followup([{ type: 'text', text: 'go' }]); await waitForIdle(ctx, agent)
|
||||
agent.followup({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } }); await waitForIdle(ctx, agent)
|
||||
const res = events(agent).find(e => e.type === 'hook/result')
|
||||
expect(res?.type === 'hook/result' && res.data.stderrSummary).toBe('x'.repeat(40) + '…')
|
||||
})
|
||||
@@ -310,7 +310,7 @@ export function defineCoverageCases(groups: CoverageGroup | readonly CoverageGro
|
||||
HooksCodex.apply(ctx, { configPath: join(d, 'hooks.json') })
|
||||
ctx.llm.registerAdapter(['mock'], adapter)
|
||||
const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' })
|
||||
agent.followup([{ type: 'text', text: 'go' }]); await waitForIdle(ctx, agent)
|
||||
agent.followup({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } }); await waitForIdle(ctx, agent)
|
||||
expect(existsSync(marker)).toBe(true)
|
||||
expect(warn).toHaveBeenCalledWith(expect.stringContaining('async hook'))
|
||||
})
|
||||
@@ -323,7 +323,7 @@ export function defineCoverageCases(groups: CoverageGroup | readonly CoverageGro
|
||||
let ran = false
|
||||
ctx.tools.register(defineContentToolFixture({ name: 'Bash', description: 'b', parameters: { command: { type: 'string' } }, async execute() { ran = true; return [{ type: 'text', text: 'ok' }] } }))
|
||||
const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' })
|
||||
agent.followup([{ type: 'text', text: 'go' }]); await waitForIdle(ctx, agent)
|
||||
agent.followup({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } }); await waitForIdle(ctx, agent)
|
||||
expect(ran).toBe(true)
|
||||
})
|
||||
|
||||
@@ -338,7 +338,7 @@ export function defineCoverageCases(groups: CoverageGroup | readonly CoverageGro
|
||||
const ctx = await harness(join(d, 'hooks.json'), adapter)
|
||||
const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' })
|
||||
await waitFor(() => existsSync(marker)) // the clean no-output hook has finished
|
||||
agent.followup([{ type: 'text', text: 'go' }]); await waitForIdle(ctx, agent)
|
||||
agent.followup({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } }); await waitForIdle(ctx, agent)
|
||||
expect(events(agent).some(e => e.type === 'user/message' && e.data.source.kind !== 'user')).toBe(false)
|
||||
})
|
||||
|
||||
@@ -364,7 +364,7 @@ export function defineCoverageCases(groups: CoverageGroup | readonly CoverageGro
|
||||
let ran = false
|
||||
ctx.tools.register(defineContentToolFixture({ name: 'Bash', description: 'b', parameters: { command: { type: 'string' } }, async execute() { ran = true; return [{ type: 'text', text: 'ok' }] } }))
|
||||
const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' })
|
||||
agent.followup([{ type: 'text', text: 'go' }]); await waitForIdle(ctx, agent)
|
||||
agent.followup({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } }); await waitForIdle(ctx, agent)
|
||||
expect(ran).toBe(true)
|
||||
})
|
||||
|
||||
@@ -377,7 +377,7 @@ export function defineCoverageCases(groups: CoverageGroup | readonly CoverageGro
|
||||
let ran = false
|
||||
ctx.tools.register(defineContentToolFixture({ name: 'Bash', description: 'b', parameters: { command: { type: 'string' } }, async execute() { ran = true; return [{ type: 'text', text: 'ok' }] } }))
|
||||
const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' })
|
||||
agent.followup([{ type: 'text', text: 'go' }]); await waitForIdle(ctx, agent)
|
||||
agent.followup({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } }); await waitForIdle(ctx, agent)
|
||||
expect(ran).toBe(true) // matcher didn't match → no hook ran → tool proceeded
|
||||
expect(events(agent).some(e => e.type === 'hook/invoked')).toBe(false)
|
||||
})
|
||||
@@ -393,7 +393,7 @@ export function defineCoverageCases(groups: CoverageGroup | readonly CoverageGro
|
||||
let ran = false
|
||||
ctx.tools.register(defineContentToolFixture({ name: 'Bash', description: 'b', parameters: { command: { type: 'string' } }, async execute() { ran = true; return [{ type: 'text', text: 'ok' }] } }))
|
||||
const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' })
|
||||
agent.followup([{ type: 'text', text: 'go' }]); await waitForIdle(ctx, agent)
|
||||
agent.followup({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } }); await waitForIdle(ctx, agent)
|
||||
const res = events(agent).find(e => e.type === 'hook/result')
|
||||
expect(res?.type === 'hook/result' && res.data.decision).toBe('stop') // recorded
|
||||
expect(ran).toBe(true) // NOT honored: the tool still ran (halt is deferred)
|
||||
@@ -406,7 +406,7 @@ export function defineCoverageCases(groups: CoverageGroup | readonly CoverageGro
|
||||
const ctx = await harness(join(d, 'hooks.json'), adapter)
|
||||
ctx.tools.register(defineContentToolFixture({ name: 'Bash', description: 'b', parameters: { command: { type: 'string' } }, async execute() { return [{ type: 'text', text: 'ok' }] } }))
|
||||
const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' })
|
||||
agent.followup([{ type: 'text', text: 'go' }]); await waitForIdle(ctx, agent)
|
||||
agent.followup({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } }); await waitForIdle(ctx, agent)
|
||||
const r = events(agent).find(e => e.type === 'tool/result')
|
||||
expect(r?.type === 'tool/result' && r.data.content.some(b => b.type === 'text' && b.text.includes('blocked by PreToolUse hook'))).toBe(true)
|
||||
})
|
||||
@@ -418,7 +418,7 @@ export function defineCoverageCases(groups: CoverageGroup | readonly CoverageGro
|
||||
const ctx = await harness(join(d, 'hooks.json'), adapter)
|
||||
ctx.tools.register(defineContentToolFixture({ name: 'Bash', description: 'b', parameters: { command: { type: 'string' } }, async execute() { return [{ type: 'text', text: 'ok' }] } }))
|
||||
const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' })
|
||||
agent.followup([{ type: 'text', text: 'go' }]); await waitForIdle(ctx, agent)
|
||||
agent.followup({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } }); await waitForIdle(ctx, agent)
|
||||
const r = events(agent).find(e => e.type === 'tool/result')
|
||||
expect(r?.type === 'tool/result' && r.data.isError).toBe(true)
|
||||
expect(r?.type === 'tool/result' && r.data.content.some(b => b.type === 'text' && b.text.includes('bad'))).toBe(true)
|
||||
@@ -435,7 +435,7 @@ export function defineCoverageCases(groups: CoverageGroup | readonly CoverageGro
|
||||
const ctx = await harness(join(d, 'hooks.json'), adapter)
|
||||
ctx.tools.register(defineContentToolFixture({ name: 'Bash', description: 'b', parameters: { command: { type: 'number' } }, async execute() { return [{ type: 'text', text: 'ok' }] } }))
|
||||
const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' })
|
||||
agent.followup([{ type: 'text', text: 'go' }]); await waitForIdle(ctx, agent)
|
||||
agent.followup({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } }); await waitForIdle(ctx, agent)
|
||||
const payload = JSON.parse(readFileSync(cap, 'utf8')) as { tool_input: { command: string } }
|
||||
expect(payload.tool_input.command).toBe('')
|
||||
})
|
||||
@@ -471,7 +471,7 @@ export function defineCoverageCases(groups: CoverageGroup | readonly CoverageGro
|
||||
ctx.bash.run = (() => Promise.reject(new Error('executor down')))
|
||||
ctx.tools.register(defineContentToolFixture({ name: 'Bash', description: 'b', parameters: { command: { type: 'string' } }, async execute() { return [{ type: 'text', text: 'ok' }] } }))
|
||||
const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' })
|
||||
agent.followup([{ type: 'text', text: 'go' }]); await waitForIdle(ctx, agent)
|
||||
agent.followup({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } }); await waitForIdle(ctx, agent)
|
||||
const res = events(agent).find(e => e.type === 'hook/result')
|
||||
expect(res?.type === 'hook/result' && 'exitCode' in res.data).toBe(false)
|
||||
})
|
||||
@@ -487,7 +487,7 @@ export function defineCoverageCases(groups: CoverageGroup | readonly CoverageGro
|
||||
const adapter = new MockAdapter([textResponse('one'), textResponse('two')])
|
||||
const ctx = await harness(join(d, 'hooks.json'), adapter)
|
||||
const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' })
|
||||
agent.followup([{ type: 'text', text: 'go' }]); await waitForIdle(ctx, agent)
|
||||
agent.followup({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } }); await waitForIdle(ctx, agent)
|
||||
expect(adapter.requests).toHaveLength(2) // empty-reason block forced continuation
|
||||
expect(JSON.stringify(adapter.requests[1]!.messages)).toContain('blocked by Stop hook')
|
||||
})
|
||||
@@ -500,7 +500,7 @@ export function defineCoverageCases(groups: CoverageGroup | readonly CoverageGro
|
||||
const adapter = new MockAdapter([textResponse('ok')])
|
||||
const ctx = await harness(join(d, 'hooks.json'), adapter)
|
||||
const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' })
|
||||
agent.followup([{ type: 'text', text: 'go' }]); await waitForIdle(ctx, agent)
|
||||
agent.followup({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } }); await waitForIdle(ctx, agent)
|
||||
expect(JSON.stringify(adapter.requests[0]!.messages)).toContain('extra guidance from a plain hook')
|
||||
})
|
||||
|
||||
@@ -528,7 +528,7 @@ export function defineCoverageCases(groups: CoverageGroup | readonly CoverageGro
|
||||
const adapter = new MockAdapter([textResponse('ok')])
|
||||
const ctx = await harness(join(d, 'hooks.json'), adapter)
|
||||
const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' })
|
||||
agent.followup([{ type: 'text', text: 'go' }]); await waitForIdle(ctx, agent)
|
||||
agent.followup({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } }); await waitForIdle(ctx, agent)
|
||||
expect(adapter.requests).toHaveLength(1) // exit 1 is non-blocking → the turn ran
|
||||
expect(JSON.stringify(adapter.requests[0]!.messages)).not.toContain('stale')
|
||||
})
|
||||
@@ -541,7 +541,7 @@ export function defineCoverageCases(groups: CoverageGroup | readonly CoverageGro
|
||||
const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' })
|
||||
await waitFor(() => events(agent).some(e => e.type === 'user/message'
|
||||
&& e.data.content.some(b => b.type === 'text' && b.text.includes('session preamble'))))
|
||||
agent.followup([{ type: 'text', text: 'go' }]); await waitForIdle(ctx, agent)
|
||||
agent.followup({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } }); await waitForIdle(ctx, agent)
|
||||
expect(JSON.stringify(adapter.requests[0]!.messages)).toContain('session preamble')
|
||||
})
|
||||
|
||||
@@ -553,7 +553,7 @@ export function defineCoverageCases(groups: CoverageGroup | readonly CoverageGro
|
||||
const adapter = new MockAdapter([textResponse('ok')])
|
||||
const ctx = await harness(join(d, 'hooks.json'), adapter)
|
||||
const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' })
|
||||
agent.followup([{ type: 'text', text: 'go' }]); await waitForIdle(ctx, agent)
|
||||
agent.followup({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } }); await waitForIdle(ctx, agent)
|
||||
expect(JSON.stringify(adapter.requests[0]!.messages)).not.toContain('unrelated')
|
||||
})
|
||||
|
||||
@@ -568,7 +568,7 @@ export function defineCoverageCases(groups: CoverageGroup | readonly CoverageGro
|
||||
const ctx = await harness(join(d, 'hooks.json'), adapter)
|
||||
ctx.tools.register(defineContentToolFixture({ name: 'shell', description: 'b', parameters: { command: { type: 'string' } }, async execute() { return [{ type: 'text', text: 'ok' }] } }))
|
||||
const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' })
|
||||
agent.followup([{ type: 'text', text: 'go' }]); await waitForIdle(ctx, agent)
|
||||
agent.followup({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } }); await waitForIdle(ctx, agent)
|
||||
const payload = JSON.parse(readFileSync(cap, 'utf8')) as { tool_name: string; tool_input: { command: string } }
|
||||
expect(payload.tool_name).toBe('shell')
|
||||
expect(payload.tool_input.command).toBe('ls')
|
||||
@@ -584,7 +584,7 @@ export function defineCoverageCases(groups: CoverageGroup | readonly CoverageGro
|
||||
let ran = false
|
||||
ctx.tools.register(defineContentToolFixture({ name: 'shell', description: 'b', parameters: { command: { type: 'string' } }, async execute() { ran = true; return [{ type: 'text', text: 'ok' }] } }))
|
||||
const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' })
|
||||
agent.followup([{ type: 'text', text: 'go' }]); await waitForIdle(ctx, agent)
|
||||
agent.followup({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } }); await waitForIdle(ctx, agent)
|
||||
expect(ran).toBe(false) // the matcher fired → the hook denied the tool
|
||||
expect(events(agent).some(e => e.type === 'hook/invoked' && e.data.point === 'PreToolUse')).toBe(true)
|
||||
})
|
||||
@@ -596,7 +596,7 @@ export function defineCoverageCases(groups: CoverageGroup | readonly CoverageGro
|
||||
const ctx = await harness(join(d, 'hooks.json'), adapter)
|
||||
const warn = vi.fn(); ctx.logger.warn = warn as never
|
||||
const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' })
|
||||
agent.followup([{ type: 'text', text: 'go' }]); await waitForIdle(ctx, agent)
|
||||
agent.followup({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } }); await waitForIdle(ctx, agent)
|
||||
expect(warn).toHaveBeenCalledWith(expect.stringContaining('systemMessage'))
|
||||
expect(JSON.stringify(adapter.requests[0]!.messages)).not.toContain('heads up')
|
||||
})
|
||||
@@ -619,7 +619,7 @@ export function defineCoverageCases(groups: CoverageGroup | readonly CoverageGro
|
||||
ctx.tools.register(defineContentToolFixture({ name: 'Bash', description: 'b', parameters: { command: { type: 'string' } }, async execute() { return [{ type: 'text', text: 'ok' }] } }))
|
||||
const { SessionId } = await import('@deepseek-ai/dsh-session')
|
||||
const handle = await ctx.agents.create({ sessionId: SessionId('s1'), meta: { cwd: sessionDir }, agentOptions: { provider: 'mock', model: 'mock' } })
|
||||
handle.agent.followup([{ type: 'text', text: 'go' }])
|
||||
handle.agent.followup({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } })
|
||||
await waitForIdle(ctx, handle.agent)
|
||||
expect(existsSync(marker)).toBe(true)
|
||||
expect(readFileSync(marker, 'utf8').trim().endsWith(sessionDir.split('/').pop()!)).toBe(true)
|
||||
|
||||
Reference in New Issue
Block a user