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,404 +0,0 @@
1
- /**
2
- * Gate Pipeline Integration Tests
3
- *
4
- * PURPOSE: Prove that gate.ts has ONE authoritative orchestration path.
5
- *
6
- * These tests verify:
7
- * 1. Progressive gate enabled + edit verification required => verification still runs
8
- * 2. Progressive gate enabled + GFI block => block still uses the same persistence path
9
- * 3. Progressive gate enabled + thinking checkpoint => checkpoint still participates in default flow
10
- * 4. Gate block has ONE authoritative persistence implementation
11
- */
12
-
13
- import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
14
- import { handleBeforeToolCall } from '../../src/hooks/gate.js';
15
- import * as fs from 'fs';
16
- import * as path from 'path';
17
- import { WorkspaceContext } from '../../src/core/workspace-context.js';
18
- import * as sessionTracker from '../../src/core/session-tracker.js';
19
- import * as evolutionEngine from '../../src/core/evolution-engine.js';
20
-
21
- vi.mock('fs');
22
- vi.mock('../../src/core/workspace-context.js');
23
- vi.mock('../../src/core/session-tracker.js', () => ({
24
- getSession: vi.fn(() => ({ currentGfi: 0 })),
25
- trackBlock: vi.fn(),
26
- hasRecentThinking: vi.fn(() => false),
27
- }));
28
- vi.mock('../../src/core/evolution-engine.js', async () => {
29
- const actual = await vi.importActual('../../src/core/evolution-engine.js');
30
- return {
31
- ...actual,
32
- checkEvolutionGate: vi.fn(() => ({ allowed: true, currentTier: 'SEED' })),
33
- getEvolutionEngine: vi.fn(),
34
- };
35
- });
36
-
37
- const mockEvolution = {
38
- getTier: vi.fn().mockReturnValue(3),
39
- getPoints: vi.fn().mockReturnValue(200),
40
- };
41
-
42
- const mockedHasRecentThinking = vi.mocked(sessionTracker.hasRecentThinking);
43
-
44
- describe('Gate Pipeline Integration - Single Authoritative Path', () => {
45
- const workspaceDir = '/mock/workspace';
46
- const sessionId = 'test-session-123';
47
-
48
- const mockConfig = {
49
- get: vi.fn().mockImplementation((key) => {
50
- if (key === 'trust') return {
51
- limits: { stage_2_max_lines: 50, stage_3_max_lines: 300 }
52
- };
53
- if (key === 'gfi_gate') return {
54
- enabled: true,
55
- thresholds: { low_risk_block: 70, high_risk_block: 40 },
56
- bash_safe_patterns: ['^(ls|dir|pwd)$'],
57
- bash_dangerous_patterns: ['rm\s+-rf'],
58
- };
59
- return undefined;
60
- })
61
- };
62
-
63
- const mockEventLog = {
64
- recordGateBlock: vi.fn(),
65
- recordPlanApproval: vi.fn(),
66
- recordGateBypass: vi.fn(),
67
- };
68
-
69
- const mockTrajectory = {
70
- recordGateBlock: vi.fn(),
71
- };
72
-
73
- const mockWctx = {
74
- workspaceDir,
75
- stateDir: '/mock/state',
76
- config: mockConfig,
77
- eventLog: mockEventLog,
78
- trajectory: mockTrajectory,
79
- evolution: mockEvolution,
80
- resolve: vi.fn().mockImplementation((key) => {
81
- if (key === 'PROFILE') return path.join(workspaceDir, '.principles', 'PROFILE.json');
82
- if (key === 'PLAN') return path.join(workspaceDir, 'PLAN.md');
83
- if (key === 'STATE_DIR') return path.join(workspaceDir, '.state');
84
- if (typeof key === 'string' && !key.includes(':')) {
85
- return path.join(workspaceDir, key);
86
- }
87
- return key;
88
- }),
89
- };
90
-
91
- beforeEach(() => {
92
- vi.clearAllMocks();
93
- vi.useFakeTimers();
94
- vi.mocked(WorkspaceContext.fromHookContext).mockReturnValue(mockWctx as any);
95
- vi.mocked(sessionTracker.getSession).mockReturnValue({ currentGfi: 0 } as any);
96
- vi.mocked(sessionTracker.trackBlock).mockImplementation(() => {});
97
- vi.mocked(evolutionEngine.getEvolutionEngine).mockReturnValue(mockEvolution);
98
- });
99
-
100
- afterEach(() => {
101
- vi.useRealTimers();
102
- });
103
-
104
- // ═══════════════════════════════════════════════════════════════════════════
105
- // TEST 1: Progressive gate enabled + edit verification => verification runs
106
- // ═══════════════════════════════════════════════════════════════════════════
107
- describe('Edit Verification Integration', () => {
108
- it('should run edit verification when progressive gate is enabled and operation is allowed', () => {
109
- const fileContent = 'const x = 1;\n';
110
- const editEvent = {
111
- toolName: 'edit',
112
- params: {
113
- file_path: 'src/example.ts',
114
- oldText: 'wrong text that does not exist', // This should trigger edit verification
115
- newText: 'const x = 2;',
116
- },
117
- };
118
-
119
- // Progressive gate enabled
120
- vi.mocked(fs.existsSync).mockReturnValue(true);
121
- vi.mocked(fs.readFileSync).mockImplementation((p: any) => {
122
- if (typeof p === 'string' && p.includes('PROFILE.json')) {
123
- return JSON.stringify({
124
- risk_paths: [],
125
- progressive_gate: { enabled: true }, // ENABLED
126
- edit_verification: { enabled: true },
127
- });
128
- }
129
- if (typeof p === 'string' && p.includes('example.ts')) {
130
- return fileContent;
131
- }
132
- return '';
133
- });
134
- vi.mocked(fs.statSync).mockReturnValue({ size: 1000 } as any);
135
-
136
- const result = handleBeforeToolCall(editEvent as any, { workspaceDir, sessionId } as any);
137
-
138
- // EXPECTED: Edit verification should run and block because oldText doesn't match
139
- // CURRENT BEHAVIOR (BUG): progressive gate returns early, edit verification never runs
140
- expect(result).toBeDefined();
141
- expect(result?.block).toBe(true);
142
- expect(result?.blockReason).toContain('P-03'); // Matches [P-03 Violation] or [P-03 Error]
143
- });
144
-
145
- it('should allow edit when oldText matches (progressive gate enabled)', () => {
146
- const fileContent = 'const x = 1;\n';
147
- const editEvent = {
148
- toolName: 'edit',
149
- params: {
150
- file_path: 'src/example.ts',
151
- oldText: 'const x = 1;', // Exact match (file has \n at end but we match without)
152
- newText: 'const x = 2;',
153
- },
154
- };
155
-
156
- vi.mocked(fs.existsSync).mockReturnValue(true);
157
- vi.mocked(fs.readFileSync).mockImplementation((p: any) => {
158
- if (typeof p === 'string' && p.includes('PROFILE.json')) {
159
- return JSON.stringify({
160
- risk_paths: [],
161
- progressive_gate: { enabled: true },
162
- edit_verification: { enabled: true },
163
- });
164
- }
165
- if (typeof p === 'string' && p.includes('example.ts')) {
166
- return fileContent;
167
- }
168
- return '';
169
- });
170
- vi.mocked(fs.statSync).mockReturnValue({ size: 1000 } as any);
171
-
172
- const result = handleBeforeToolCall(editEvent as any, { workspaceDir, sessionId } as any);
173
-
174
- // Should pass edit verification - fuzzy match should find the content
175
- // If exact match fails, fuzzy match (0.8 threshold) should pass
176
- expect(result).toBeUndefined();
177
- });
178
- });
179
-
180
- // ═══════════════════════════════════════════════════════════════════════════
181
- // TEST 2: GFI block uses single persistence path
182
- // ═══════════════════════════════════════════════════════════════════════════
183
- describe('GFI Gate Block Persistence', () => {
184
- it('should use single authoritative block path when GFI gate blocks', () => {
185
- vi.mocked(sessionTracker.getSession).mockReturnValue({ currentGfi: 85 } as any);
186
-
187
- const writeEvent = {
188
- toolName: 'write',
189
- params: {
190
- file_path: 'src/test.ts',
191
- content: 'new content',
192
- },
193
- };
194
-
195
- vi.mocked(fs.existsSync).mockReturnValue(true);
196
- vi.mocked(fs.readFileSync).mockImplementation((p: any) => {
197
- if (typeof p === 'string' && p.includes('PROFILE.json')) {
198
- return JSON.stringify({
199
- risk_paths: [],
200
- progressive_gate: { enabled: true },
201
- });
202
- }
203
- return '';
204
- });
205
-
206
- const result = handleBeforeToolCall(writeEvent as any, { workspaceDir, sessionId } as any);
207
-
208
- // EXPECTED: Block through GFI gate, with single persistence path
209
- expect(result).toBeDefined();
210
- expect(result?.block).toBe(true);
211
- expect(result?.blockReason).toContain('GFI');
212
-
213
- // CRITICAL: trackBlock should be called exactly once (single persistence)
214
- expect(sessionTracker.trackBlock).toHaveBeenCalledTimes(1);
215
- expect(sessionTracker.trackBlock).toHaveBeenCalledWith(sessionId);
216
-
217
- // EventLog should record the block
218
- expect(mockEventLog.recordGateBlock).toHaveBeenCalled();
219
- });
220
-
221
- it('should persist trajectory gate block with retry on failure', async () => {
222
- vi.mocked(sessionTracker.getSession).mockReturnValue({ currentGfi: 85 } as any);
223
-
224
- const writeEvent = {
225
- toolName: 'write',
226
- params: {
227
- file_path: 'src/test.ts',
228
- content: 'new content',
229
- },
230
- };
231
-
232
- // Make trajectory.recordGateBlock fail initially
233
- mockTrajectory.recordGateBlock.mockImplementation(() => {
234
- throw new Error('DB locked');
235
- });
236
-
237
- vi.mocked(fs.existsSync).mockReturnValue(true);
238
- vi.mocked(fs.readFileSync).mockImplementation((p: any) => {
239
- if (typeof p === 'string' && p.includes('PROFILE.json')) {
240
- return JSON.stringify({
241
- risk_paths: [],
242
- progressive_gate: { enabled: true },
243
- });
244
- }
245
- return '';
246
- });
247
-
248
- const result = handleBeforeToolCall(writeEvent as any, { workspaceDir, sessionId } as any);
249
-
250
- expect(result?.block).toBe(true);
251
-
252
- // Advance timers to trigger retry
253
- await vi.advanceTimersByTimeAsync(100);
254
-
255
- // Should have attempted trajectory persistence (with retry)
256
- expect(mockTrajectory.recordGateBlock).toHaveBeenCalled();
257
- });
258
- });
259
-
260
- // ═══════════════════════════════════════════════════════════════════════════
261
- // TEST 3: Thinking checkpoint participates in default flow
262
- // ═══════════════════════════════════════════════════════════════════════════
263
- describe('Thinking Checkpoint Integration', () => {
264
- it('should run thinking checkpoint before progressive gate (when enabled)', () => {
265
- // Setup: Enable thinking checkpoint
266
- const bashEvent = {
267
- toolName: 'run_shell_command',
268
- params: {
269
- command: 'rm -rf node_modules',
270
- },
271
- };
272
-
273
- vi.mocked(fs.existsSync).mockReturnValue(true);
274
- vi.mocked(fs.readFileSync).mockImplementation((p: any) => {
275
- if (typeof p === 'string' && p.includes('PROFILE.json')) {
276
- return JSON.stringify({
277
- risk_paths: [],
278
- progressive_gate: { enabled: true },
279
- thinking_checkpoint: {
280
- enabled: true,
281
- window_ms: 300000,
282
- high_risk_tools: ['run_shell_command', 'delete_file'],
283
- },
284
- });
285
- }
286
- return '';
287
- });
288
-
289
- // Mock session without recent thinking
290
- vi.mocked(sessionTracker.getSession).mockReturnValue({
291
- currentGfi: 0,
292
- lastThinkingAt: Date.now() - 600000, // 10 min ago
293
- } as any);
294
- mockedHasRecentThinking.mockReturnValue(false);
295
-
296
- const result = handleBeforeToolCall(bashEvent as any, { workspaceDir, sessionId } as any);
297
-
298
- // EXPECTED: Thinking checkpoint should block before progressive gate
299
- expect(result).toBeDefined();
300
- expect(result?.block).toBe(true);
301
- // Check for Chinese message (thinking-checkpoint.ts uses Chinese)
302
- expect(result?.blockReason).toContain('深度思考');
303
- });
304
-
305
- it('should allow operation after thinking checkpoint passes', () => {
306
- const bashEvent = {
307
- toolName: 'run_shell_command',
308
- params: {
309
- command: 'ls -la',
310
- },
311
- };
312
-
313
- vi.mocked(fs.existsSync).mockReturnValue(true);
314
- vi.mocked(fs.readFileSync).mockImplementation((p: any) => {
315
- if (typeof p === 'string' && p.includes('PROFILE.json')) {
316
- return JSON.stringify({
317
- risk_paths: [],
318
- progressive_gate: { enabled: true },
319
- thinking_checkpoint: {
320
- enabled: true,
321
- window_ms: 300000,
322
- high_risk_tools: ['run_shell_command', 'delete_file'],
323
- },
324
- });
325
- }
326
- return '';
327
- });
328
-
329
- // Mock session WITH recent thinking (checkpoint passes)
330
- vi.mocked(sessionTracker.getSession).mockReturnValue({
331
- currentGfi: 0,
332
- lastThinkingAt: Date.now() - 60000, // 1 min ago (within window)
333
- } as any);
334
- mockedHasRecentThinking.mockReturnValue(true);
335
-
336
- const result = handleBeforeToolCall(bashEvent as any, { workspaceDir, sessionId } as any);
337
-
338
- // Thinking checkpoint passes, no GFI concern, Stage 4 allows
339
- expect(result).toBeUndefined();
340
- });
341
- });
342
-
343
- // ═══════════════════════════════════════════════════════════════════════════
344
- // TEST 4: Single block persistence implementation
345
- // ═══════════════════════════════════════════════════════════════════════════
346
- describe('Single Block Persistence Implementation', () => {
347
- it('should have only ONE trackBlock call per gate block (no duplicate tracking)', () => {
348
- vi.mocked(sessionTracker.getSession).mockReturnValue({ currentGfi: 85 } as any);
349
-
350
- const writeEvent = {
351
- toolName: 'write',
352
- params: {
353
- file_path: 'src/test.ts',
354
- content: 'x'.repeat(100),
355
- },
356
- };
357
-
358
- vi.mocked(fs.existsSync).mockReturnValue(true);
359
- vi.mocked(fs.readFileSync).mockImplementation((p: any) => {
360
- if (typeof p === 'string' && p.includes('PROFILE.json')) {
361
- return JSON.stringify({
362
- risk_paths: [],
363
- progressive_gate: { enabled: true },
364
- });
365
- }
366
- return '';
367
- });
368
-
369
- handleBeforeToolCall(writeEvent as any, { workspaceDir, sessionId } as any);
370
-
371
- // CRITICAL: trackBlock should be called exactly ONCE
372
- // If called multiple times, there are multiple block implementations
373
- expect(sessionTracker.trackBlock).toHaveBeenCalledTimes(1);
374
- });
375
-
376
- it('should have only ONE eventLog.recordGateBlock call per gate block', () => {
377
- vi.mocked(sessionTracker.getSession).mockReturnValue({ currentGfi: 85 } as any);
378
-
379
- const writeEvent = {
380
- toolName: 'write',
381
- params: {
382
- file_path: 'src/test.ts',
383
- content: 'x'.repeat(100),
384
- },
385
- };
386
-
387
- vi.mocked(fs.existsSync).mockReturnValue(true);
388
- vi.mocked(fs.readFileSync).mockImplementation((p: any) => {
389
- if (typeof p === 'string' && p.includes('PROFILE.json')) {
390
- return JSON.stringify({
391
- risk_paths: [],
392
- progressive_gate: { enabled: true },
393
- });
394
- }
395
- return '';
396
- });
397
-
398
- handleBeforeToolCall(writeEvent as any, { workspaceDir, sessionId } as any);
399
-
400
- // Event log should record exactly once
401
- expect(mockEventLog.recordGateBlock).toHaveBeenCalledTimes(1);
402
- });
403
- });
404
- });
@@ -1,271 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach } from 'vitest';
2
- import { handleBeforeToolCall } from '../../src/hooks/gate.js';
3
- import { normalizeProfile, PROFILE_DEFAULTS } from '../../src/core/profile.js';
4
- import * as fs from 'fs';
5
- import * as path from 'path';
6
- import { WorkspaceContext } from '../../src/core/workspace-context.js';
7
- import * as riskCalculator from '../../src/core/risk-calculator.js';
8
- import * as evolutionEngine from '../../src/core/evolution-engine.js';
9
-
10
- vi.mock('fs');
11
- vi.mock('../../src/core/workspace-context.js');
12
- vi.mock('../../src/core/risk-calculator.js');
13
- vi.mock('../../src/core/evolution-engine.js');
14
-
15
- describe('Progressive Gate Hook', () => {
16
- const workspaceDir = '/mock/workspace';
17
-
18
- const mockTrust = {
19
- getScorecard: vi.fn(),
20
- getScore: vi.fn(),
21
- getStage: vi.fn(),
22
- };
23
-
24
- const mockConfig = {
25
- get: vi.fn().mockImplementation((key) => {
26
- if (key === 'trust') return {
27
- limits: { stage_2_max_lines: 10, stage_3_max_lines: 100 }
28
- };
29
- return undefined;
30
- })
31
- };
32
-
33
- const mockEventLog = {
34
- recordGateBlock: vi.fn(),
35
- recordPlanApproval: vi.fn(),
36
- };
37
-
38
- const mockWctx = {
39
- workspaceDir,
40
- stateDir: '/mock/state',
41
- trust: mockTrust,
42
- config: mockConfig,
43
- eventLog: mockEventLog,
44
- trajectory: {
45
- recordGateBlock: vi.fn(),
46
- },
47
- resolve: vi.fn().mockImplementation((key) => {
48
- if (key === 'PROFILE') return path.join(workspaceDir, '.principles', 'PROFILE.json');
49
- if (key === 'PLAN') return path.join(workspaceDir, 'PLAN.md');
50
- return '';
51
- }),
52
- };
53
-
54
- beforeEach(() => {
55
- vi.clearAllMocks();
56
- vi.mocked(WorkspaceContext.fromHookContext).mockReturnValue(mockWctx as any);
57
- vi.mocked(riskCalculator.assessRiskLevel).mockReturnValue('LOW');
58
- vi.mocked(riskCalculator.estimateLineChanges).mockReturnValue(1);
59
- });
60
-
61
- it('should block risk path writes at Seed tier (EP system)', () => {
62
- const mockCtx = { workspaceDir };
63
- const mockEvent = {
64
- toolName: 'write',
65
- params: { file_path: 'src/main.ts' }
66
- };
67
-
68
- mockTrust.getScore.mockReturnValue(25);
69
- mockTrust.getStage.mockReturnValue(1);
70
- vi.mocked(fs.existsSync).mockReturnValue(true);
71
- vi.mocked(fs.readFileSync).mockImplementation(() => {
72
- return JSON.stringify({ risk_paths: ['src/'], progressive_gate: { enabled: true } });
73
- });
74
- // EP system blocks risk path at Seed tier
75
- vi.mocked(evolutionEngine.checkEvolutionGate).mockReturnValue({
76
- allowed: false,
77
- currentTier: 1,
78
- reason: 'Seed tier cannot modify risk paths'
79
- });
80
-
81
- const result = handleBeforeToolCall(mockEvent as any, mockCtx as any);
82
-
83
- expect(result).toBeDefined();
84
- expect(result?.block).toBe(true);
85
- expect(result?.blockReason).toContain('risk path');
86
- });
87
-
88
- it('should block large writes at Seed tier (EP system)', () => {
89
- const mockCtx = { workspaceDir };
90
- const mockEvent = {
91
- toolName: 'write',
92
- params: { file_path: 'docs/readme.md' }
93
- };
94
-
95
- mockTrust.getScore.mockReturnValue(50);
96
- mockTrust.getStage.mockReturnValue(2);
97
- vi.mocked(riskCalculator.estimateLineChanges).mockReturnValue(200);
98
- vi.mocked(fs.existsSync).mockReturnValue(true);
99
- vi.mocked(fs.readFileSync).mockImplementation(() => {
100
- return JSON.stringify({ risk_paths: ['src/'], progressive_gate: { enabled: true } });
101
- });
102
- // EP system blocks 200 lines at Seed tier (limit is 150)
103
- vi.mocked(evolutionEngine.checkEvolutionGate).mockReturnValue({
104
- allowed: false,
105
- currentTier: 1,
106
- reason: 'Seed tier limit is 150 lines per write, requested 200'
107
- });
108
-
109
- const result = handleBeforeToolCall(mockEvent as any, mockCtx as any);
110
-
111
- expect(result).toBeDefined();
112
- expect(result?.block).toBe(true);
113
- expect(result?.blockReason).toContain('150');
114
- });
115
-
116
- it('should allow risk paths at Sapling tier (EP system)', () => {
117
- const mockCtx = { workspaceDir };
118
- const mockEvent = {
119
- toolName: 'write',
120
- params: { file_path: 'src/core.ts' }
121
- };
122
-
123
- mockTrust.getScore.mockReturnValue(70);
124
- mockTrust.getStage.mockReturnValue(3);
125
- vi.mocked(fs.existsSync).mockReturnValue(true);
126
- vi.mocked(fs.readFileSync).mockImplementation(() => {
127
- return JSON.stringify({ risk_paths: ['src/'], progressive_gate: { enabled: true } });
128
- });
129
- // EP system allows at Sapling tier (risk path allowed)
130
- vi.mocked(evolutionEngine.checkEvolutionGate).mockReturnValue({
131
- allowed: true,
132
- currentTier: 3,
133
- reason: ''
134
- });
135
-
136
- const result = handleBeforeToolCall(mockEvent as any, mockCtx as any);
137
-
138
- expect(result).toBeUndefined(); // Allowed
139
- });
140
-
141
- it('should allow everything for Forest tier (EP system)', () => {
142
- const mockCtx = { workspaceDir };
143
- const mockEvent = {
144
- toolName: 'write',
145
- params: { file_path: 'src/core.ts' }
146
- };
147
-
148
- mockTrust.getScore.mockReturnValue(90);
149
- mockTrust.getStage.mockReturnValue(4);
150
- vi.mocked(fs.existsSync).mockReturnValue(true);
151
- vi.mocked(fs.readFileSync).mockImplementation(() => {
152
- return JSON.stringify({ risk_paths: ['src/'], progressive_gate: { enabled: true } });
153
- });
154
- // EP system allows everything at Forest tier
155
- vi.mocked(evolutionEngine.checkEvolutionGate).mockReturnValue({
156
- allowed: true,
157
- currentTier: 5,
158
- reason: ''
159
- });
160
-
161
- const result = handleBeforeToolCall(mockEvent as any, mockCtx as any);
162
-
163
- expect(result).toBeUndefined(); // Allowed
164
- });
165
-
166
- it('should fall back to original logic if progressive gate is disabled', () => {
167
- const mockCtx = { workspaceDir };
168
- const mockEvent = {
169
- toolName: 'write',
170
- params: { file_path: 'src/db/user.ts' }
171
- };
172
-
173
- vi.mocked(fs.existsSync).mockReturnValue(true);
174
- vi.mocked(fs.readFileSync).mockImplementation((p: any) => {
175
- if (p.includes('PROFILE.json')) {
176
- return JSON.stringify({ risk_paths: ['src/db/'], gate: { require_plan_for_risk_paths: true }, progressive_gate: { enabled: false } });
177
- }
178
- if (p.includes('PLAN.md')) return 'STATUS: DRAFT\n';
179
- return '';
180
- });
181
-
182
- const result = handleBeforeToolCall(mockEvent as any, mockCtx as any);
183
-
184
- expect(result).toBeDefined();
185
- expect(result?.block).toBe(true);
186
- expect(result?.blockReason).toContain('No READY plan found');
187
- });
188
- });
189
-
190
- // ═══════════════════════════════════════════════════════════════════════════
191
- // Task 4: Default Values Consistency Tests
192
- // ═══════════════════════════════════════════════════════════════════════════
193
- describe('Gate Default Values Consistency', () => {
194
- /**
195
- * PURPOSE: Prove that gate.ts inline defaults match PROFILE_DEFAULTS.
196
- * If gate.ts has inline defaults that differ from normalizeProfile(),
197
- * this is a bug - the defaults should come from a single source of truth.
198
- *
199
- * IMPORTANT: gate.ts should use normalizeProfile({}) for defaults,
200
- * not maintain a separate inline object.
201
- */
202
-
203
- it('gate.ts uses PROFILE_DEFAULTS when PROFILE.json does not exist', () => {
204
- // When PROFILE.json doesn't exist, gate.ts should use defaults from normalizeProfile
205
- const expectedDefaults = normalizeProfile({});
206
-
207
- // Verify key defaults are correct
208
- expect(expectedDefaults.progressive_gate.enabled).toBe(true);
209
- expect(expectedDefaults.edit_verification.enabled).toBe(true);
210
- expect(expectedDefaults.thinking_checkpoint.enabled).toBe(false);
211
- expect(expectedDefaults.risk_paths).toEqual([]);
212
- expect(expectedDefaults.gate.require_plan_for_risk_paths).toBe(true);
213
- });
214
-
215
- it('gate.ts default behavior matches normalizeProfile({})', () => {
216
- const defaults = normalizeProfile({});
217
-
218
- // Test gate behavior with no PROFILE.json
219
- const mockCtx = { workspaceDir: '/test/workspace', sessionId: 'test-session' };
220
- const mockEvent = {
221
- toolName: 'write',
222
- params: { file_path: 'src/test.ts', content: 'test' }
223
- };
224
-
225
- vi.mocked(fs.existsSync).mockReturnValue(false); // No PROFILE.json
226
- vi.mocked(WorkspaceContext.fromHookContext).mockReturnValue({
227
- workspaceDir: '/test/workspace',
228
- trust: { getScore: () => 80, getStage: () => 4 },
229
- config: { get: () => undefined },
230
- eventLog: { recordGateBlock: () => {} },
231
- trajectory: { recordGateBlock: () => {} },
232
- resolve: (key: string) => `/test/workspace/.state/${key}.json`,
233
- } as any);
234
-
235
- // Should not throw, should use defaults
236
- const result = handleBeforeToolCall(mockEvent as any, mockCtx as any);
237
-
238
- // With defaults: progressive_gate.enabled=true, trust=Stage4, should allow
239
- // (Stage 4 bypasses progressive gate)
240
- expect(result).toBeUndefined();
241
- });
242
-
243
- it('gate.ts thinking_checkpoint default is OFF (false)', () => {
244
- const defaults = normalizeProfile({});
245
-
246
- // CRITICAL: thinking_checkpoint must default to false
247
- // Otherwise new users will be blocked by deep reflection requirement
248
- expect(defaults.thinking_checkpoint.enabled).toBe(false);
249
-
250
- // Verify gate.ts respects this default
251
- const mockCtx = { workspaceDir: '/test/workspace', sessionId: 'test-session' };
252
- const bashEvent = {
253
- toolName: 'run_shell_command',
254
- params: { command: 'ls -la' }
255
- };
256
-
257
- vi.mocked(fs.existsSync).mockReturnValue(false); // No PROFILE.json
258
- vi.mocked(WorkspaceContext.fromHookContext).mockReturnValue({
259
- workspaceDir: '/test/workspace',
260
- trust: { getScore: () => 80, getStage: () => 4 },
261
- config: { get: () => undefined },
262
- eventLog: { recordGateBlock: () => {} },
263
- trajectory: { recordGateBlock: () => {} },
264
- resolve: (key: string) => `/test/workspace/.state/${key}.json`,
265
- } as any);
266
-
267
- // Should NOT be blocked by thinking checkpoint (default OFF)
268
- const result = handleBeforeToolCall(bashEvent as any, mockCtx as any);
269
- expect(result).toBeUndefined();
270
- });
271
- });