Download integration-tests/hooks-agent-flow.test.ts from SaylorTwift/gemini-cli: direct link, hf CLI and curl.
- Browser
- Download file 10.6 kB
-
https://huggingface.co/SaylorTwift/gemini-cli/resolve/main/integration-tests/hooks-agent-flow.test.ts
- Command line
-
hf download hf://SaylorTwift/gemini-cli/integration-tests/hooks-agent-flow.test.ts
-
curl -L -o hooks-agent-flow.test.ts https://huggingface.co/SaylorTwift/gemini-cli/resolve/main/integration-tests/hooks-agent-flow.test.ts
10.6 kB
| /** | |
| * @license | |
| * Copyright 2025 Google LLC | |
| * SPDX-License-Identifier: Apache-2.0 | |
| */ | |
| import { describe, it, expect, beforeEach, afterEach } from 'vitest'; | |
| import { TestRig, normalizePath } from './test-helper.js'; | |
| import { join } from 'node:path'; | |
| import { writeFileSync } from 'node:fs'; | |
| describe('Hooks Agent Flow', () => { | |
| let rig: TestRig; | |
| beforeEach(() => { | |
| rig = new TestRig(); | |
| }); | |
| afterEach(async () => { | |
| if (rig) { | |
| await rig.cleanup(); | |
| } | |
| }); | |
| describe('BeforeAgent Hooks', () => { | |
| it('should inject additional context via BeforeAgent hook', async () => { | |
| await rig.setup('should inject additional context via BeforeAgent hook', { | |
| fakeResponsesPath: join( | |
| import.meta.dirname, | |
| 'hooks-agent-flow.responses', | |
| ), | |
| }); | |
| const hookScript = ` | |
| try { | |
| const output = { | |
| decision: "allow", | |
| hookSpecificOutput: { | |
| hookEventName: "BeforeAgent", | |
| additionalContext: "SYSTEM INSTRUCTION: This is injected context." | |
| } | |
| }; | |
| process.stdout.write(JSON.stringify(output)); | |
| } catch (e) { | |
| console.error('Failed to write stdout:', e); | |
| process.exit(1); | |
| } | |
| console.error('DEBUG: BeforeAgent hook executed'); | |
| `; | |
| const scriptPath = join(rig.testDir!, 'before_agent_context.cjs'); | |
| writeFileSync(scriptPath, hookScript); | |
| await rig.setup('should inject additional context via BeforeAgent hook', { | |
| settings: { | |
| hooksConfig: { | |
| enabled: true, | |
| }, | |
| hooks: { | |
| BeforeAgent: [ | |
| { | |
| hooks: [ | |
| { | |
| type: 'command', | |
| command: `node "${scriptPath}"`, | |
| timeout: 5000, | |
| }, | |
| ], | |
| }, | |
| ], | |
| }, | |
| }, | |
| }); | |
| await rig.run({ args: 'Hello test' }); | |
| // Verify hook execution and telemetry | |
| const hookTelemetryFound = await rig.waitForTelemetryEvent('hook_call'); | |
| expect(hookTelemetryFound).toBeTruthy(); | |
| const hookLogs = rig.readHookLogs(); | |
| const beforeAgentLog = hookLogs.find( | |
| (log) => log.hookCall.hook_event_name === 'BeforeAgent', | |
| ); | |
| expect(beforeAgentLog).toBeDefined(); | |
| expect(beforeAgentLog?.hookCall.stdout).toContain('injected context'); | |
| expect(beforeAgentLog?.hookCall.stdout).toContain('"decision":"allow"'); | |
| expect(beforeAgentLog?.hookCall.stdout).toContain( | |
| 'SYSTEM INSTRUCTION: This is injected context.', | |
| ); | |
| }); | |
| }); | |
| describe('AfterAgent Hooks', () => { | |
| it('should receive prompt and response in AfterAgent hook', async () => { | |
| await rig.setup('should receive prompt and response in AfterAgent hook', { | |
| fakeResponsesPath: join( | |
| import.meta.dirname, | |
| 'hooks-agent-flow.responses', | |
| ), | |
| }); | |
| const hookScript = ` | |
| const fs = require('fs'); | |
| try { | |
| const input = fs.readFileSync(0, 'utf-8'); | |
| console.error('DEBUG: AfterAgent hook input received'); | |
| process.stdout.write("Received Input: " + input); | |
| } catch (err) { | |
| console.error('Hook Failed:', err); | |
| process.exit(1); | |
| } | |
| `; | |
| const scriptPath = rig.createScript('after_agent_verify.cjs', hookScript); | |
| rig.setup('should receive prompt and response in AfterAgent hook', { | |
| settings: { | |
| hooksConfig: { | |
| enabled: true, | |
| }, | |
| hooks: { | |
| AfterAgent: [ | |
| { | |
| hooks: [ | |
| { | |
| type: 'command', | |
| command: normalizePath(`node "${scriptPath}"`)!, | |
| timeout: 5000, | |
| }, | |
| ], | |
| }, | |
| ], | |
| }, | |
| }, | |
| }); | |
| await rig.run({ args: 'Hello validation' }); | |
| const hookTelemetryFound = await rig.waitForTelemetryEvent('hook_call'); | |
| expect(hookTelemetryFound).toBeTruthy(); | |
| const hookLogs = rig.readHookLogs(); | |
| const afterAgentLog = hookLogs.find( | |
| (log) => log.hookCall.hook_event_name === 'AfterAgent', | |
| ); | |
| expect(afterAgentLog).toBeDefined(); | |
| // Verify the hook stdout contains the input we echoed which proves the | |
| // hook received the prompt and response | |
| expect(afterAgentLog?.hookCall.stdout).toContain('Received Input'); | |
| expect(afterAgentLog?.hookCall.stdout).toContain('Hello validation'); | |
| // The fake response contains "Hello World" | |
| expect(afterAgentLog?.hookCall.stdout).toContain('Hello World'); | |
| }); | |
| it('should process clearContext in AfterAgent hook output', async () => { | |
| rig.setup('should process clearContext in AfterAgent hook output', { | |
| fakeResponsesPath: join( | |
| import.meta.dirname, | |
| 'hooks-system.after-agent.responses', | |
| ), | |
| }); | |
| // BeforeModel hook to track message counts across LLM calls | |
| const messageCountFile = join(rig.testDir!, 'message-counts.json'); | |
| const escapedPath = JSON.stringify(messageCountFile); | |
| const beforeModelScript = ` | |
| const fs = require('fs'); | |
| const input = JSON.parse(fs.readFileSync(0, 'utf-8')); | |
| const messageCount = input.llm_request?.contents?.length || 0; | |
| let counts = []; | |
| try { counts = JSON.parse(fs.readFileSync(${escapedPath}, 'utf-8')); } catch (e) {} | |
| counts.push(messageCount); | |
| fs.writeFileSync(${escapedPath}, JSON.stringify(counts)); | |
| console.log(JSON.stringify({ decision: 'allow' })); | |
| `; | |
| const beforeModelScriptPath = rig.createScript( | |
| 'before_model_counter.cjs', | |
| beforeModelScript, | |
| ); | |
| const afterAgentScript = ` | |
| const fs = require('fs'); | |
| const input = JSON.parse(fs.readFileSync(0, 'utf-8')); | |
| if (input.stop_hook_active) { | |
| // Retry turn: allow execution to proceed (breaks the loop) | |
| console.log(JSON.stringify({ decision: 'allow' })); | |
| } else { | |
| // First call: block and clear context to trigger the retry | |
| console.log(JSON.stringify({ | |
| decision: 'block', | |
| reason: 'Security policy triggered', | |
| hookSpecificOutput: { | |
| hookEventName: 'AfterAgent', | |
| clearContext: true | |
| } | |
| })); | |
| } | |
| `; | |
| const afterAgentScriptPath = rig.createScript( | |
| 'after_agent_clear.cjs', | |
| afterAgentScript, | |
| ); | |
| rig.setup('should process clearContext in AfterAgent hook output', { | |
| settings: { | |
| hooksConfig: { | |
| enabled: true, | |
| }, | |
| hooks: { | |
| BeforeModel: [ | |
| { | |
| hooks: [ | |
| { | |
| type: 'command', | |
| command: normalizePath(`node "${beforeModelScriptPath}"`)!, | |
| timeout: 5000, | |
| }, | |
| ], | |
| }, | |
| ], | |
| AfterAgent: [ | |
| { | |
| hooks: [ | |
| { | |
| type: 'command', | |
| command: normalizePath(`node "${afterAgentScriptPath}"`)!, | |
| timeout: 5000, | |
| }, | |
| ], | |
| }, | |
| ], | |
| }, | |
| }, | |
| }); | |
| const result = await rig.run({ args: 'Hello test' }); | |
| const hookTelemetryFound = await rig.waitForTelemetryEvent('hook_call'); | |
| expect(hookTelemetryFound).toBeTruthy(); | |
| const hookLogs = rig.readHookLogs(); | |
| const afterAgentLog = hookLogs.find( | |
| (log) => log.hookCall.hook_event_name === 'AfterAgent', | |
| ); | |
| expect(afterAgentLog).toBeDefined(); | |
| expect(afterAgentLog?.hookCall.stdout).toContain('clearContext'); | |
| expect(afterAgentLog?.hookCall.stdout).toContain('true'); | |
| expect(result).toContain('Security policy triggered'); | |
| // Verify context was cleared: second call should not have more messages than first | |
| const countsRaw = rig.readFile('message-counts.json'); | |
| const counts = JSON.parse(countsRaw) as number[]; | |
| expect(counts.length).toBeGreaterThanOrEqual(2); | |
| expect(counts[1]).toBeLessThanOrEqual(counts[0]); | |
| }); | |
| }); | |
| describe('Multi-step Loops', () => { | |
| it('should fire BeforeAgent and AfterAgent exactly once per turn despite tool calls', async () => { | |
| await rig.setup( | |
| 'should fire BeforeAgent and AfterAgent exactly once per turn despite tool calls', | |
| { | |
| fakeResponsesPath: join( | |
| import.meta.dirname, | |
| 'hooks-agent-flow-multistep.responses', | |
| ), | |
| }, | |
| ); | |
| // Create script files for hooks | |
| const baPath = rig.createScript( | |
| 'ba_fired.cjs', | |
| "console.log('BeforeAgent Fired');", | |
| ); | |
| const aaPath = rig.createScript( | |
| 'aa_fired.cjs', | |
| "console.log('AfterAgent Fired');", | |
| ); | |
| await rig.setup( | |
| 'should fire BeforeAgent and AfterAgent exactly once per turn despite tool calls', | |
| { | |
| settings: { | |
| hooksConfig: { | |
| enabled: true, | |
| }, | |
| hooks: { | |
| BeforeAgent: [ | |
| { | |
| hooks: [ | |
| { | |
| type: 'command', | |
| command: normalizePath(`node "${baPath}"`)!, | |
| timeout: 5000, | |
| }, | |
| ], | |
| }, | |
| ], | |
| AfterAgent: [ | |
| { | |
| hooks: [ | |
| { | |
| type: 'command', | |
| command: normalizePath(`node "${aaPath}"`)!, | |
| timeout: 5000, | |
| }, | |
| ], | |
| }, | |
| ], | |
| }, | |
| }, | |
| }, | |
| ); | |
| await rig.run({ args: 'Do a multi-step task' }); | |
| const hookLogs = rig.readHookLogs(); | |
| const beforeAgentLogs = hookLogs.filter( | |
| (log) => log.hookCall.hook_event_name === 'BeforeAgent', | |
| ); | |
| const afterAgentLogs = hookLogs.filter( | |
| (log) => log.hookCall.hook_event_name === 'AfterAgent', | |
| ); | |
| expect(beforeAgentLogs).toHaveLength(1); | |
| expect(afterAgentLogs).toHaveLength(1); | |
| const afterAgentLog = afterAgentLogs[0]; | |
| expect(afterAgentLog).toBeDefined(); | |
| expect(afterAgentLog?.hookCall.stdout).toContain('AfterAgent Fired'); | |
| }); | |
| }); | |
| }); | |