mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-03-22 03:51:22 -07:00
205 lines
5.4 KiB
TypeScript
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,
|
|
});
|
|
});
|
|
});
|
|
});
|