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

205 lines
5.4 KiB
TypeScript

/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, it, expect, vi, beforeEach } from 'vitest';
import * as fs from 'node:fs';
import { TrustedHooksManager } from './trustedHooks.js';
import { Storage } from '../config/storage.js';
import { HookEventName, HookType, type HookDefinition } from './types.js';
vi.mock('node:fs');
vi.mock('../config/storage.js');
vi.mock('../utils/debugLogger.js', () => ({
debugLogger: {
warn: vi.fn(),
error: vi.fn(),
log: vi.fn(),
debug: vi.fn(),
},
}));
describe('TrustedHooksManager', () => {
beforeEach(() => {
vi.resetAllMocks();
vi.mocked(Storage.getGlobalGeminiDir).mockReturnValue('/mock/home/.gemini');
});
describe('initialization', () => {
it('should load existing trusted hooks', () => {
const existingData = {
'/project/a': ['hook1:cmd1'],
};
vi.mocked(fs.existsSync).mockReturnValue(true);
vi.mocked(fs.readFileSync).mockReturnValue(JSON.stringify(existingData));
const manager = new TrustedHooksManager();
const untrusted = manager.getUntrustedHooks('/project/a', {
[HookEventName.BeforeTool]: [
{
hooks: [{ type: HookType.Command, command: 'cmd1', name: 'hook1' }],
},
],
});
expect(untrusted).toHaveLength(0);
});
it('should handle missing config file', () => {
vi.mocked(fs.existsSync).mockReturnValue(false);
const manager = new TrustedHooksManager();
const untrusted = manager.getUntrustedHooks('/project/a', {
[HookEventName.BeforeTool]: [
{
hooks: [{ type: HookType.Command, command: 'cmd1', name: 'hook1' }],
},
],
});
expect(untrusted).toEqual(['hook1']);
});
});
describe('getUntrustedHooks', () => {
it('should return names of untrusted hooks', () => {
vi.mocked(fs.existsSync).mockReturnValue(false);
const manager = new TrustedHooksManager();
const projectHooks = {
[HookEventName.BeforeTool]: [
{
hooks: [
{
name: 'trusted-hook',
type: HookType.Command,
command: 'cmd1',
} as const,
{
name: 'new-hook',
type: HookType.Command,
command: 'cmd2',
} as const,
],
},
],
};
// Initially both are untrusted
expect(manager.getUntrustedHooks('/project', projectHooks)).toEqual([
'trusted-hook',
'new-hook',
]);
// Trust one
manager.trustHooks('/project', {
[HookEventName.BeforeTool]: [
{
hooks: [
{
name: 'trusted-hook',
type: HookType.Command,
command: 'cmd1',
} as const,
],
},
],
});
// Only the other one is untrusted
expect(manager.getUntrustedHooks('/project', projectHooks)).toEqual([
'new-hook',
]);
});
it('should use command if name is missing', () => {
vi.mocked(fs.existsSync).mockReturnValue(false);
const manager = new TrustedHooksManager();
const projectHooks = {
[HookEventName.BeforeTool]: [
{
hooks: [{ type: HookType.Command, command: './script.sh' }],
},
],
};
expect(
manager.getUntrustedHooks(
'/project',
projectHooks as Partial<Record<HookEventName, HookDefinition[]>>,
),
).toEqual(['./script.sh']);
});
it('should detect change in command as untrusted', () => {
vi.mocked(fs.existsSync).mockReturnValue(false);
const manager = new TrustedHooksManager();
const originalHook = {
[HookEventName.BeforeTool]: [
{
hooks: [
{ name: 'my-hook', type: HookType.Command, command: 'old-cmd' },
],
},
],
};
const updatedHook = {
[HookEventName.BeforeTool]: [
{
hooks: [
{ name: 'my-hook', type: HookType.Command, command: 'new-cmd' },
],
},
],
};
manager.trustHooks(
'/project',
originalHook as Partial<Record<HookEventName, HookDefinition[]>>,
);
expect(
manager.getUntrustedHooks(
'/project',
updatedHook as Partial<Record<HookEventName, HookDefinition[]>>,
),
).toEqual(['my-hook']);
});
});
describe('persistence', () => {
it('should save to file when trusting hooks', () => {
vi.mocked(fs.existsSync).mockReturnValue(false);
const manager = new TrustedHooksManager();
manager.trustHooks('/project', {
[HookEventName.BeforeTool]: [
{
hooks: [{ name: 'hook1', type: HookType.Command, command: 'cmd1' }],
},
],
});
expect(fs.writeFileSync).toHaveBeenCalledWith(
expect.stringContaining('trusted_hooks.json'),
expect.stringContaining('hook1:cmd1'),
);
});
it('should create directory if missing on save', () => {
vi.mocked(fs.existsSync).mockReturnValue(false);
const manager = new TrustedHooksManager();
manager.trustHooks('/project', {});
expect(fs.mkdirSync).toHaveBeenCalledWith(expect.any(String), {
recursive: true,
});
});
});
});