|
|
|
|
@@ -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)
|
|
|
|
|
|