File size: 3,591 Bytes
7a1ad33
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
/**
 * @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');
  });
});