principles-disciple 1.62.0 → 1.63.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,422 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach } from 'vitest';
2
- import { checkGfiGate } from '../../src/hooks/gfi-gate.js';
3
- import { WorkspaceContext } from '../../src/core/workspace-context.js';
4
- import * as sessionTracker from '../../src/core/session-tracker.js';
5
- import * as evolutionEngine from '../../src/core/evolution-engine.js';
6
- import { EvolutionTier } from '../../src/core/evolution-types.js';
7
-
8
- vi.mock('../../src/core/session-tracker.js');
9
- vi.mock('../../src/core/evolution-engine.js', async () => {
10
- const actual = await vi.importActual('../../src/core/evolution-engine.js');
11
- return {
12
- ...actual,
13
- getEvolutionEngine: vi.fn(),
14
- };
15
- });
16
-
17
- const mockEvolution = {
18
- getTier: vi.fn().mockReturnValue(EvolutionTier.Sapling),
19
- getPoints: vi.fn().mockReturnValue(200),
20
- getAvailablePoints: vi.fn().mockReturnValue(200),
21
- getTierDefinition: vi.fn().mockReturnValue({
22
- tier: EvolutionTier.Sapling,
23
- name: 'Sapling',
24
- requiredPoints: 200,
25
- permissions: { maxLinesPerWrite: 500, maxFilesPerTask: 10, allowRiskPath: true, allowSubagentSpawn: true }
26
- }),
27
- };
28
-
29
- describe('checkGfiGate', () => {
30
- const workspaceDir = '/mock/workspace';
31
-
32
- const mockWctx = {
33
- workspaceDir,
34
- evolution: mockEvolution,
35
- } as any;
36
-
37
- const mockLogger = {
38
- warn: vi.fn(),
39
- };
40
-
41
- const defaultConfig = {
42
- enabled: true,
43
- thresholds: {
44
- low_risk_block: 70,
45
- high_risk_block: 40,
46
- },
47
- large_change_lines: 50,
48
- ep_tier_multipliers: {
49
- '1': 0.5,
50
- '2': 0.75,
51
- '3': 1.0,
52
- '4': 1.5,
53
- },
54
- bash_safe_patterns: [
55
- '^(ls|dir|pwd|cat|echo)\\b',
56
- '^git\\s+(status|log)',
57
- ],
58
- bash_dangerous_patterns: [
59
- 'rm\\s+.*-rf',
60
- 'git\\s+push.*--force',
61
- ],
62
- };
63
-
64
- beforeEach(() => {
65
- vi.clearAllMocks();
66
- vi.mocked(sessionTracker.getSession).mockReturnValue({
67
- currentGfi: 0,
68
- } as any);
69
- mockEvolution.getTier.mockReturnValue(EvolutionTier.Sapling);
70
- mockEvolution.getPoints.mockReturnValue(200);
71
- mockEvolution.getAvailablePoints.mockReturnValue(200);
72
- mockEvolution.getTierDefinition.mockReturnValue({
73
- tier: EvolutionTier.Sapling,
74
- name: 'Sapling',
75
- requiredPoints: 200,
76
- permissions: { maxLinesPerWrite: 500, maxFilesPerTask: 10, allowRiskPath: true, allowSubagentSpawn: true }
77
- });
78
- vi.mocked(evolutionEngine.getEvolutionEngine).mockReturnValue(mockEvolution);
79
- });
80
-
81
- // ════════════════════════════════════════════════
82
- // Configuration
83
- // ════════════════════════════════════════════════
84
- describe('Configuration', () => {
85
- it('should return undefined when disabled', () => {
86
- const config = { ...defaultConfig, enabled: false };
87
- const event = {
88
- toolName: 'write',
89
- params: { file_path: 'test.txt' },
90
- } as any;
91
-
92
- const result = checkGfiGate(event, mockWctx, 'test-session', config, mockLogger);
93
-
94
- expect(result).toBeUndefined();
95
- });
96
-
97
- it('should return undefined when no sessionId', () => {
98
- const event = {
99
- toolName: 'write',
100
- params: { file_path: 'test.txt' },
101
- } as any;
102
-
103
- const result = checkGfiGate(event, mockWctx, undefined, defaultConfig, mockLogger);
104
-
105
- expect(result).toBeUndefined();
106
- });
107
-
108
- it('should return undefined when session not found', () => {
109
- vi.mocked(sessionTracker.getSession).mockReturnValue(undefined);
110
- const event = {
111
- toolName: 'write',
112
- params: { file_path: 'test.txt' },
113
- } as any;
114
-
115
- const result = checkGfiGate(event, mockWctx, 'test-session', defaultConfig, mockLogger);
116
-
117
- expect(result).toBeUndefined();
118
- });
119
- });
120
-
121
- // ════════════════════════════════════════════════
122
- // TIER 3: Bash commands
123
- // ════════════════════════════════════════════════
124
- describe('TIER 3: Bash commands', () => {
125
- describe('Safe commands', () => {
126
- it('should allow safe bash commands regardless of GFI', () => {
127
- vi.mocked(sessionTracker.getSession).mockReturnValue({ currentGfi: 95 } as any);
128
- const event = {
129
- toolName: 'bash',
130
- params: { command: 'git status' },
131
- } as any;
132
-
133
- const result = checkGfiGate(event, mockWctx, 'test-session', defaultConfig, mockLogger);
134
-
135
- expect(result).toBeUndefined();
136
- });
137
-
138
- it('should allow "ls -la"', () => {
139
- vi.mocked(sessionTracker.getSession).mockReturnValue({ currentGfi: 90 } as any);
140
- const event = {
141
- toolName: 'run_shell_command',
142
- params: { command: 'ls -la' },
143
- } as any;
144
-
145
- const result = checkGfiGate(event, mockWctx, 'test-session', defaultConfig, mockLogger);
146
-
147
- expect(result).toBeUndefined();
148
- });
149
- });
150
-
151
- describe('Dangerous commands', () => {
152
- it('should block dangerous commands regardless of GFI', () => {
153
- vi.mocked(sessionTracker.getSession).mockReturnValue({ currentGfi: 0 } as any);
154
- mockEvolution.getTier.mockReturnValue(4);
155
- const event = {
156
- toolName: 'bash',
157
- params: { command: 'rm -rf node_modules' },
158
- } as any;
159
-
160
- const result = checkGfiGate(event, mockWctx, 'test-session', defaultConfig, mockLogger);
161
-
162
- expect(result).toBeDefined();
163
- expect(result?.block).toBe(true);
164
- expect(result?.blockReason).toContain('危险命令');
165
- expect(mockLogger.warn).toHaveBeenCalledWith(expect.stringContaining('Dangerous bash command blocked'));
166
- });
167
-
168
- it('should block "git push --force"', () => {
169
- vi.mocked(sessionTracker.getSession).mockReturnValue({ currentGfi: 5 } as any);
170
- mockEvolution.getTier.mockReturnValue(4);
171
- const event = {
172
- toolName: 'bash',
173
- params: { command: 'git push origin main --force' },
174
- } as any;
175
-
176
- const result = checkGfiGate(event, mockWctx, 'test-session', defaultConfig, mockLogger);
177
-
178
- expect(result).toBeDefined();
179
- expect(result?.block).toBe(true);
180
- expect(result?.blockReason).toContain('危险命令');
181
- });
182
- });
183
-
184
- describe('Normal commands (GFI-based)', () => {
185
- it('should block normal bash when GFI exceeds threshold', () => {
186
- vi.mocked(sessionTracker.getSession).mockReturnValue({ currentGfi: 80 } as any);
187
- mockEvolution.getTier.mockReturnValue(3);
188
- const event = {
189
- toolName: 'bash',
190
- params: { command: 'npm install lodash' },
191
- } as any;
192
-
193
- const result = checkGfiGate(event, mockWctx, 'test-session', defaultConfig, mockLogger);
194
-
195
- expect(result).toBeDefined();
196
- expect(result?.block).toBe(true);
197
- expect(result?.blockReason).toContain('GFI');
198
- });
199
-
200
- it('should allow normal bash when GFI below threshold', () => {
201
- vi.mocked(sessionTracker.getSession).mockReturnValue({ currentGfi: 30 } as any);
202
- mockEvolution.getTier.mockReturnValue(3);
203
- const event = {
204
- toolName: 'bash',
205
- params: { command: 'npm install lodash' },
206
- } as any;
207
-
208
- const result = checkGfiGate(event, mockWctx, 'test-session', defaultConfig, mockLogger);
209
-
210
- expect(result).toBeUndefined();
211
- });
212
-
213
- it('should apply trust stage multiplier to bash threshold', () => {
214
- // Stage 1: threshold = 70 * 0.5 = 35
215
- vi.mocked(sessionTracker.getSession).mockReturnValue({ currentGfi: 40 } as any);
216
- mockEvolution.getTier.mockReturnValue(1);
217
- const event = {
218
- toolName: 'bash',
219
- params: { command: 'npm test' },
220
- } as any;
221
-
222
- const result = checkGfiGate(event, mockWctx, 'test-session', defaultConfig, mockLogger);
223
-
224
- expect(result).toBeDefined();
225
- expect(result?.block).toBe(true);
226
- });
227
- });
228
- });
229
-
230
- // ════════════════════════════════════════════════
231
- // TIER 2: High-risk tools
232
- // ════════════════════════════════════════════════
233
- describe('TIER 2: High-risk tools', () => {
234
- it('should block delete_file when GFI >= 40', () => {
235
- vi.mocked(sessionTracker.getSession).mockReturnValue({ currentGfi: 50 } as any);
236
- mockEvolution.getTier.mockReturnValue(3);
237
- const event = {
238
- toolName: 'delete_file',
239
- params: { file_path: 'temp/file.txt' },
240
- } as any;
241
-
242
- const result = checkGfiGate(event, mockWctx, 'test-session', defaultConfig, mockLogger);
243
-
244
- expect(result).toBeDefined();
245
- expect(result?.block).toBe(true);
246
- expect(result?.blockReason).toContain('GFI');
247
- });
248
-
249
- it('should allow delete_file when GFI < 40', () => {
250
- vi.mocked(sessionTracker.getSession).mockReturnValue({ currentGfi: 30 } as any);
251
- mockEvolution.getTier.mockReturnValue(3);
252
- const event = {
253
- toolName: 'delete_file',
254
- params: { file_path: 'temp/file.txt' },
255
- } as any;
256
-
257
- const result = checkGfiGate(event, mockWctx, 'test-session', defaultConfig, mockLogger);
258
-
259
- expect(result).toBeUndefined();
260
- });
261
-
262
- it('should block move_file when GFI >= 40', () => {
263
- vi.mocked(sessionTracker.getSession).mockReturnValue({ currentGfi: 55 } as any);
264
- mockEvolution.getTier.mockReturnValue(3);
265
- const event = {
266
- toolName: 'move_file',
267
- params: { source: 'old.ts', destination: 'new.ts' },
268
- } as any;
269
-
270
- const result = checkGfiGate(event, mockWctx, 'test-session', defaultConfig, mockLogger);
271
-
272
- expect(result).toBeDefined();
273
- expect(result?.block).toBe(true);
274
- });
275
-
276
- it('should apply trust stage multiplier to high-risk threshold', () => {
277
- // Stage 1: threshold = 40 * 0.5 = 20
278
- vi.mocked(sessionTracker.getSession).mockReturnValue({ currentGfi: 25 } as any);
279
- mockEvolution.getTier.mockReturnValue(1);
280
- const event = {
281
- toolName: 'delete_file',
282
- params: { file_path: 'test.txt' },
283
- } as any;
284
-
285
- const result = checkGfiGate(event, mockWctx, 'test-session', defaultConfig, mockLogger);
286
-
287
- expect(result).toBeDefined();
288
- expect(result?.block).toBe(true);
289
- });
290
- });
291
-
292
- // ════════════════════════════════════════════════
293
- // TIER 1: Low-risk write tools
294
- // ════════════════════════════════════════════════
295
- describe('TIER 1: Low-risk write tools', () => {
296
- it('should block write when GFI >= 70', () => {
297
- vi.mocked(sessionTracker.getSession).mockReturnValue({ currentGfi: 75 } as any);
298
- mockEvolution.getTier.mockReturnValue(3);
299
- const event = {
300
- toolName: 'write',
301
- params: { file_path: 'test.txt', content: 'hello' },
302
- } as any;
303
-
304
- const result = checkGfiGate(event, mockWctx, 'test-session', defaultConfig, mockLogger);
305
-
306
- expect(result).toBeDefined();
307
- expect(result?.block).toBe(true);
308
- expect(result?.blockReason).toContain('GFI');
309
- });
310
-
311
- it('should allow write when GFI < 70', () => {
312
- vi.mocked(sessionTracker.getSession).mockReturnValue({ currentGfi: 50 } as any);
313
- mockEvolution.getTier.mockReturnValue(3);
314
- const event = {
315
- toolName: 'write',
316
- params: { file_path: 'test.txt', content: 'hello' },
317
- } as any;
318
-
319
- const result = checkGfiGate(event, mockWctx, 'test-session', defaultConfig, mockLogger);
320
-
321
- expect(result).toBeUndefined();
322
- });
323
-
324
- it('should block edit when GFI >= 70', () => {
325
- vi.mocked(sessionTracker.getSession).mockReturnValue({ currentGfi: 80 } as any);
326
- mockEvolution.getTier.mockReturnValue(3);
327
- const event = {
328
- toolName: 'edit',
329
- params: { file_path: 'src/util.ts', oldText: 'foo', newText: 'bar' },
330
- } as any;
331
-
332
- const result = checkGfiGate(event, mockWctx, 'test-session', defaultConfig, mockLogger);
333
-
334
- expect(result).toBeDefined();
335
- expect(result?.block).toBe(true);
336
- });
337
-
338
- it('should apply trust stage multiplier to low-risk threshold', () => {
339
- // Stage 1: threshold = 70 * 0.5 = 35
340
- vi.mocked(sessionTracker.getSession).mockReturnValue({ currentGfi: 40 } as any);
341
- mockEvolution.getTier.mockReturnValue(1);
342
- const event = {
343
- toolName: 'write',
344
- params: { file_path: 'test.txt', content: 'hello' },
345
- } as any;
346
-
347
- const result = checkGfiGate(event, mockWctx, 'test-session', defaultConfig, mockLogger);
348
-
349
- expect(result).toBeDefined();
350
- expect(result?.block).toBe(true);
351
- });
352
- });
353
-
354
- // ════════════════════════════════════════════════
355
- // AGENT_TOOLS: Subagent spawn protection
356
- // ════════════════════════════════════════════════
357
- describe('AGENT_TOOLS: Subagent spawn', () => {
358
- it('should block sessions_spawn when GFI >= 90', () => {
359
- vi.mocked(sessionTracker.getSession).mockReturnValue({ currentGfi: 90 } as any);
360
- mockEvolution.getTier.mockReturnValue(3);
361
- const event = {
362
- toolName: 'sessions_spawn',
363
- params: { task: 'Analyze code' },
364
- } as any;
365
-
366
- const result = checkGfiGate(event, mockWctx, 'test-session', defaultConfig, mockLogger);
367
-
368
- expect(result).toBeDefined();
369
- expect(result?.block).toBe(true);
370
- expect(result?.blockReason).toContain('禁止派生子智能体');
371
- });
372
-
373
- it('should allow sessions_spawn when GFI < 90', () => {
374
- vi.mocked(sessionTracker.getSession).mockReturnValue({ currentGfi: 85 } as any);
375
- mockEvolution.getTier.mockReturnValue(3);
376
- const event = {
377
- toolName: 'sessions_spawn',
378
- params: { task: 'Analyze code' },
379
- } as any;
380
-
381
- const result = checkGfiGate(event, mockWctx, 'test-session', defaultConfig, mockLogger);
382
-
383
- expect(result).toBeUndefined();
384
- });
385
-
386
- it('should block agent spawn even at GFI=95 (critical threshold)', () => {
387
- vi.mocked(sessionTracker.getSession).mockReturnValue({ currentGfi: 95 } as any);
388
- mockEvolution.getTier.mockReturnValue(4);
389
- const event = {
390
- toolName: 'sessions_spawn',
391
- params: { task: 'Critical task' },
392
- } as any;
393
-
394
- const result = checkGfiGate(event, mockWctx, 'test-session', defaultConfig, mockLogger);
395
-
396
- expect(result).toBeDefined();
397
- expect(result?.block).toBe(true);
398
- });
399
- });
400
-
401
- // ════════════════════════════════════════════════
402
- // Large change threshold reduction
403
- // ════════════════════════════════════════════════
404
- describe('Large change reduction', () => {
405
- it('should lower threshold for large write operations', () => {
406
- vi.mocked(sessionTracker.getSession).mockReturnValue({ currentGfi: 55 } as any);
407
- mockEvolution.getTier.mockReturnValue(3);
408
- const event = {
409
- toolName: 'write',
410
- params: { file_path: 'large.ts', content: 'x\n'.repeat(120) },
411
- } as any;
412
-
413
- const result = checkGfiGate(event, mockWctx, 'test-session', defaultConfig, mockLogger);
414
-
415
- // 120 lines > 50 (large_change_lines)
416
- // threshold = 70 * (1 - min(120/200, 0.5)) = 70 * 0.7 = 49
417
- // GFI=55 > 49, should block
418
- expect(result).toBeDefined();
419
- expect(result?.block).toBe(true);
420
- });
421
- });
422
- });