Files
gemini-cli/packages/core/src/hooks/runtimeHooks.test.ts

142 lines
3.5 KiB
TypeScript

/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { HookSystem } from './hookSystem.js';
import { Config } from '../config/config.js';
import { HookType, HookEventName, ConfigSource } from './types.js';
import * as os from 'node:os';
import * as path from 'node:path';
import * as fs from 'node:fs';
// Mock console methods
vi.stubGlobal('console', {
log: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
debug: vi.fn(),
});
describe('Runtime Hooks', () => {
let hookSystem: HookSystem;
let config: Config;
beforeEach(() => {
vi.resetAllMocks();
const testDir = path.join(os.tmpdir(), 'test-runtime-hooks');
fs.mkdirSync(testDir, { recursive: true });
config = new Config({
model: 'gemini-3-flash-preview',
targetDir: testDir,
sessionId: 'test-session',
debugMode: false,
cwd: testDir,
});
// Stub getMessageBus
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(config as any).getMessageBus = () => undefined;
hookSystem = new HookSystem(config);
});
it('should register a runtime hook', async () => {
await hookSystem.initialize();
const action = vi.fn().mockResolvedValue(undefined);
hookSystem.registerHook(
{
type: HookType.Runtime,
name: 'test-hook',
action,
},
HookEventName.BeforeTool,
{ matcher: 'TestTool' },
);
const hooks = hookSystem.getAllHooks();
expect(hooks).toHaveLength(1);
expect(hooks[0].config.name).toBe('test-hook');
expect(hooks[0].source).toBe(ConfigSource.Runtime);
});
it('should execute a runtime hook', async () => {
await hookSystem.initialize();
const action = vi.fn().mockImplementation(async () => ({
decision: 'allow',
systemMessage: 'Hook ran',
}));
hookSystem.registerHook(
{
type: HookType.Runtime,
name: 'test-hook',
action,
},
HookEventName.BeforeTool,
{ matcher: 'TestTool' },
);
const result = await hookSystem
.getEventHandler()
.fireBeforeToolEvent('TestTool', { foo: 'bar' });
expect(action).toHaveBeenCalled();
expect(action.mock.calls[0][0]).toMatchObject({
tool_name: 'TestTool',
tool_input: { foo: 'bar' },
hook_event_name: 'BeforeTool',
});
expect(result.finalOutput?.systemMessage).toBe('Hook ran');
});
it('should handle runtime hook errors', async () => {
await hookSystem.initialize();
const action = vi.fn().mockRejectedValue(new Error('Hook failed'));
hookSystem.registerHook(
{
type: HookType.Runtime,
name: 'fail-hook',
action,
},
HookEventName.BeforeTool,
{ matcher: 'TestTool' },
);
// Should not throw, but handle error gracefully
await hookSystem.getEventHandler().fireBeforeToolEvent('TestTool', {});
expect(action).toHaveBeenCalled();
});
it('should preserve runtime hooks across re-initialization', async () => {
await hookSystem.initialize();
hookSystem.registerHook(
{
type: HookType.Runtime,
name: 'persist-hook',
action: async () => {},
},
HookEventName.BeforeTool,
{ matcher: 'TestTool' },
);
expect(hookSystem.getAllHooks()).toHaveLength(1);
// Re-initialize
await hookSystem.initialize();
expect(hookSystem.getAllHooks()).toHaveLength(1);
expect(hookSystem.getAllHooks()[0].config.name).toBe('persist-hook');
});
});