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,405 +1,230 @@
1
1
  /**
2
- * Gate Rule Host Pipeline Integration Tests
2
+ * Gate Rule Host Only - Pipeline Integration Tests
3
3
  *
4
- * PURPOSE: Verify that the Rule Host is correctly wired into the gate chain
5
- * between GFI and Progressive Gate, with correct ordering and behavior.
4
+ * PURPOSE: Verify gate.ts with Rule Host Only (no hardcoded gates).
6
5
  *
7
6
  * Tests:
8
- * 1. When GFI blocks, Rule Host is never called
9
- * 2. When Rule Host blocks, Progressive Gate is never called
10
- * 3. When Rule Host returns undefined (no active implementations), Progressive Gate runs normally
11
- * 4. When Rule Host throws, gate continues to Progressive Gate (D-08)
12
- * 5. Block result uses blockSource='rule-host'
13
- * 6. Existing gate flow still works when no active implementations exist
7
+ * 1. Rule Host blocks operation block result with blockSource='rule-host'
8
+ * 2. Rule Host allow (no match) operation passes
9
+ * 3. Rule Host throws degrades conservatively, allows operation
10
+ * 4. Rule Host requireApproval records event, does not block
11
+ * 5. Non-target tools (read) → pass through early
14
12
  */
15
13
 
16
- import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
14
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
17
15
  import { handleBeforeToolCall } from '../../src/hooks/gate.js';
18
- import * as fs from 'fs';
19
- import * as path from 'path';
20
- import { WorkspaceContext } from '../../src/core/workspace-context.js';
21
16
  import * as sessionTracker from '../../src/core/session-tracker.js';
22
17
  import * as evolutionEngine from '../../src/core/evolution-engine.js';
23
18
 
24
- // Mock fs
25
- vi.mock('fs');
19
+ const workspaceDir = '/mock/workspace';
20
+ const sessionId = 'test-session-rh';
26
21
 
27
- // Mock workspace context
28
- vi.mock('../../src/core/workspace-context.js');
22
+ const mockEvolution = {
23
+ getTier: vi.fn().mockReturnValue(3),
24
+ getPoints: vi.fn().mockReturnValue(200),
25
+ };
29
26
 
30
- // Mock session tracker
31
27
  vi.mock('../../src/core/session-tracker.js', () => ({
32
28
  getSession: vi.fn(() => ({ currentGfi: 0 })),
33
29
  trackBlock: vi.fn(),
34
30
  hasRecentThinking: vi.fn(() => false),
35
31
  }));
36
32
 
37
- // Mock evolution engine
38
- vi.mock('../../src/core/evolution-engine.js', async () => {
39
- const actual = await vi.importActual('../../src/core/evolution-engine.js');
40
- return {
41
- ...actual,
42
- checkEvolutionGate: vi.fn(() => ({ allowed: true, currentTier: 'SEED' })),
43
- getEvolutionEngine: vi.fn(),
44
- };
45
- });
46
-
47
- // Mock Rule Host module — controls RuleHost.evaluate behavior
48
- // Use a shared mutable evaluate mock that tests can override
49
- let _mockEvaluate: ReturnType<typeof vi.fn> = vi.fn().mockReturnValue(undefined);
50
-
51
- vi.mock('../../src/core/rule-host.js', () => {
52
- return {
53
- RuleHost: vi.fn(function(this: any, _stateDir: string) {
54
- this.evaluate = _mockEvaluate;
55
- }),
56
- };
57
- });
58
-
59
- // Mock ledger to avoid file reads
60
- vi.mock('../../src/core/principle-tree-ledger.js', () => ({
61
- loadLedger: vi.fn(),
62
- listImplementationsByLifecycleState: vi.fn(() => []),
33
+ vi.mock('../../src/core/evolution-engine.js', () => ({
34
+ getEvolutionEngine: vi.fn(() => mockEvolution),
63
35
  }));
64
36
 
65
- // Shared mock instance — exposed as module-level so test assertions can reference it
66
- const _mockEventLogInstance = {
67
- recordGateBlock: vi.fn(),
68
- recordPlanApproval: vi.fn(),
69
- recordGateBypass: vi.fn(),
37
+ const mockEventLogInstance = {
38
+ recordRuleHostEvaluated: vi.fn(),
70
39
  recordRuleEnforced: vi.fn(),
40
+ recordRuleHostBlocked: vi.fn(),
71
41
  recordRuleHostRequireApproval: vi.fn(),
72
- recordRuleHostEvaluated: vi.fn(),
73
- recordPainSignal: vi.fn(),
74
42
  };
75
43
  vi.mock('../../src/core/event-log.js', () => ({
76
- EventLogService: { get: vi.fn(() => _mockEventLogInstance) },
77
- EventLog: {},
44
+ EventLogService: { get: vi.fn(() => mockEventLogInstance) },
78
45
  }));
79
- // Export so test assertions can use vi.mocked() on the instance
80
- export { _mockEventLogInstance };
81
-
82
- import { RuleHost } from '../../src/core/rule-host.js';
83
- import { EventLogService } from '../../src/core/event-log.js';
84
- import * as sessionTrackerModule from '../../src/core/session-tracker.js';
85
- import * as evolutionEngineModule from '../../src/core/evolution-engine.js';
86
46
 
87
- const MockedRuleHost = vi.mocked(RuleHost);
88
-
89
- const mockEvolution = {
90
- getTier: vi.fn().mockReturnValue(3),
91
- getPoints: vi.fn().mockReturnValue(200),
92
- };
93
-
94
- describe('Gate Rule Host Pipeline Integration', () => {
95
- const workspaceDir = '/mock/workspace';
96
- const sessionId = 'test-session-rh';
47
+ let _mockEvaluate = vi.fn().mockReturnValue(undefined);
48
+ vi.mock('../../src/core/rule-host.js', () => ({
49
+ RuleHost: vi.fn(function(this: any, _stateDir: string, _logger: any) {
50
+ this.evaluate = _mockEvaluate;
51
+ }),
52
+ }));
97
53
 
98
- const mockConfig = {
99
- get: vi.fn().mockImplementation((key: string) => {
100
- if (key === 'trust') return {
101
- limits: { stage_2_max_lines: 50, stage_3_max_lines: 300 }
102
- };
103
- if (key === 'gfi_gate') return {
104
- enabled: true,
105
- thresholds: { low_risk_block: 70, high_risk_block: 40 },
106
- bash_safe_patterns: ['^(ls|dir|pwd)$'],
107
- bash_dangerous_patterns: ['rm\\s+-rf'],
108
- };
109
- return undefined;
110
- })
111
- };
112
-
113
- const mockEventLog = {
114
- recordGateBlock: vi.fn(),
115
- recordPlanApproval: vi.fn(),
116
- recordGateBypass: vi.fn(),
117
- recordRuleEnforced: vi.fn(),
118
- recordRuleHostRequireApproval: vi.fn(),
119
- };
120
-
121
- const mockTrajectory = {
122
- recordGateBlock: vi.fn(),
123
- };
124
-
125
- const mockWctx = {
126
- workspaceDir,
127
- stateDir: '/mock/state',
128
- config: mockConfig,
129
- eventLog: mockEventLog,
130
- trajectory: mockTrajectory,
131
- evolution: mockEvolution,
132
- resolve: vi.fn().mockImplementation((key: string) => {
133
- if (key === 'PROFILE') return path.join(workspaceDir, '.principles', 'PROFILE.json');
134
- if (key === 'PLAN') return path.join(workspaceDir, 'PLAN.md');
135
- if (key === 'STATE_DIR') return path.join(workspaceDir, '.state');
136
- if (typeof key === 'string' && !key.includes(':')) {
137
- return path.join(workspaceDir, key);
138
- }
139
- return key;
140
- }),
141
- };
54
+ vi.mock('../../src/core/principle-tree-ledger.js', () => ({
55
+ loadLedger: vi.fn(),
56
+ listImplementationsByLifecycleState: vi.fn(() => []),
57
+ }));
142
58
 
59
+ describe('Gate Rule Host Only Pipeline', () => {
143
60
  beforeEach(() => {
144
61
  vi.clearAllMocks();
145
- vi.useFakeTimers();
146
- // Reset the shared evaluate mock to default (returns undefined)
147
62
  _mockEvaluate = vi.fn().mockReturnValue(undefined);
148
- MockedRuleHost.mockImplementation(function(this: any, _stateDir: string) {
149
- this.evaluate = _mockEvaluate;
150
- });
151
- vi.mocked(WorkspaceContext.fromHookContext).mockReturnValue(mockWctx as any);
152
- vi.mocked(sessionTrackerModule.getSession).mockReturnValue({ currentGfi: 0 } as any);
153
- vi.mocked(sessionTrackerModule.trackBlock).mockImplementation(() => {});
154
- vi.mocked(evolutionEngineModule.getEvolutionEngine).mockReturnValue(mockEvolution);
155
63
  });
156
64
 
157
- afterEach(() => {
158
- vi.useRealTimers();
159
- });
65
+ describe('Rule Host blocks', () => {
66
+ it('should block with blockSource=rule-host when Rule Host returns block', () => {
67
+ _mockEvaluate = vi.fn().mockReturnValue({
68
+ decision: 'block',
69
+ matched: true,
70
+ reason: 'Dangerous git force-push detected',
71
+ ruleId: 'R_001',
72
+ principleId: 'P_001',
73
+ });
74
+
75
+ const event = {
76
+ toolName: 'bash',
77
+ params: { command: 'git push --force' },
78
+ };
160
79
 
161
- /**
162
- * Helper: create a standard write event
163
- */
164
- function makeWriteEvent(overrides?: Partial<any>) {
165
- return {
166
- toolName: 'write',
167
- params: {
168
- file_path: 'src/test.ts',
169
- content: 'const x = 1;',
170
- },
171
- ...overrides,
172
- };
173
- }
174
-
175
- /**
176
- * Helper: set up fs mocks for a profile with progressive gate enabled
177
- */
178
- function setupProfileMock(profileOverrides?: Record<string, unknown>) {
179
- vi.mocked(fs.existsSync).mockImplementation((p: any) => {
180
- if (typeof p === 'string' && p.includes('PROFILE.json')) return true;
181
- return false;
182
- });
183
- vi.mocked(fs.readFileSync).mockImplementation((p: any) => {
184
- if (typeof p === 'string' && p.includes('PROFILE.json')) {
185
- return JSON.stringify({
186
- risk_paths: [],
187
- progressive_gate: { enabled: true },
188
- edit_verification: { enabled: true },
189
- ...profileOverrides,
190
- });
191
- }
192
- return '';
193
- });
194
- }
195
-
196
- // ═══════════════════════════════════════════════════════════════════════════
197
- // TEST 1: When GFI blocks, Rule Host is never called
198
- // ═══════════════════════════════════════════════════════════════════════════
199
- it('should not call Rule Host when GFI gate blocks', () => {
200
- // Set high GFI to trigger GFI block
201
- vi.mocked(sessionTrackerModule.getSession).mockReturnValue({ currentGfi: 85 } as any);
202
- setupProfileMock();
203
-
204
- const result = handleBeforeToolCall(makeWriteEvent() as any, { workspaceDir, sessionId } as any);
205
-
206
- // GFI should block
207
- expect(result).toBeDefined();
208
- expect(result?.block).toBe(true);
209
- expect(result?.blockReason).toContain('GFI');
210
-
211
- // RuleHost constructor should NOT have been called
212
- // (GFI returns before reaching Rule Host evaluation)
213
- expect(MockedRuleHost).not.toHaveBeenCalled();
214
- });
80
+ const result = handleBeforeToolCall(event as any, { workspaceDir, sessionId } as any);
215
81
 
216
- // ═══════════════════════════════════════════════════════════════════════════
217
- // TEST 2: When Rule Host blocks, Progressive Gate is never called
218
- // ═══════════════════════════════════════════════════════════════════════════
219
- it('should not reach Progressive Gate when Rule Host blocks', () => {
220
- setupProfileMock();
221
-
222
- // Mock RuleHost.evaluate to return a block
223
- _mockEvaluate = vi.fn().mockReturnValue({
224
- decision: 'block',
225
- matched: true,
226
- reason: 'Rule Host test block',
227
- });
228
- MockedRuleHost.mockImplementation(function(this: any, _stateDir: string) {
229
- this.evaluate = _mockEvaluate;
82
+ expect(result).toBeDefined();
83
+ expect(result?.block).toBe(true);
84
+ expect(result?.blockReason).toContain('Dangerous git force-push detected');
230
85
  });
231
86
 
232
- const result = handleBeforeToolCall(makeWriteEvent() as any, { workspaceDir, sessionId } as any);
87
+ it('should record rulehost_blocked event when Rule Host blocks', () => {
88
+ _mockEvaluate = vi.fn().mockReturnValue({
89
+ decision: 'block',
90
+ matched: true,
91
+ reason: 'High-risk path',
92
+ ruleId: 'R_002',
93
+ });
94
+
95
+ const event = {
96
+ toolName: 'write',
97
+ params: { file_path: 'src/danger.ts', content: 'bad' },
98
+ };
233
99
 
234
- // Rule Host should block
235
- expect(result).toBeDefined();
236
- expect(result?.block).toBe(true);
100
+ handleBeforeToolCall(event as any, { workspaceDir, sessionId } as any);
237
101
 
238
- // The block should come from rule-host
239
- expect(mockEventLog.recordGateBlock).toHaveBeenCalledWith(
240
- sessionId,
241
- expect.objectContaining({ blockSource: 'rule-host' })
242
- );
102
+ expect(mockEventLogInstance.recordRuleHostBlocked).toHaveBeenCalledWith(
103
+ expect.objectContaining({
104
+ toolName: 'write',
105
+ ruleId: 'R_002',
106
+ })
107
+ );
108
+ });
243
109
  });
244
110
 
245
- // ═══════════════════════════════════════════════════════════════════════════
246
- // TEST 3: When Rule Host returns undefined, Progressive Gate runs normally
247
- // ═══════════════════════════════════════════════════════════════════════════
248
- it('should continue to Progressive Gate when Rule Host returns undefined', () => {
249
- setupProfileMock();
111
+ describe('Rule Host allows', () => {
112
+ it('should allow when Rule Host returns undefined (no match)', () => {
113
+ _mockEvaluate = vi.fn().mockReturnValue(undefined);
250
114
 
251
- // Mock RuleHost.evaluate to return undefined (no active implementations)
252
- _mockEvaluate = vi.fn().mockReturnValue(undefined);
253
- MockedRuleHost.mockImplementation(function(this: any, _stateDir: string) {
254
- this.evaluate = _mockEvaluate;
255
- });
115
+ const event = {
116
+ toolName: 'write',
117
+ params: { file_path: 'src/safe.ts', content: 'const x = 1' },
118
+ };
119
+
120
+ const result = handleBeforeToolCall(event as any, { workspaceDir, sessionId } as any);
256
121
 
257
- const result = handleBeforeToolCall(makeWriteEvent() as any, { workspaceDir, sessionId } as any);
122
+ expect(result).toBeUndefined();
123
+ });
258
124
 
259
- // Should pass through (no block from any gate)
260
- expect(result).toBeUndefined();
125
+ it('should record rulehost_evaluated even when no match', () => {
126
+ _mockEvaluate = vi.fn().mockReturnValue(undefined);
261
127
 
262
- // Rule Host was called
263
- expect(_mockEvaluate).toHaveBeenCalled();
264
- });
128
+ const event = {
129
+ toolName: 'edit',
130
+ params: { file_path: 'src/config.ts', oldText: 'x', newText: 'y' },
131
+ };
265
132
 
266
- // ═══════════════════════════════════════════════════════════════════════════
267
- // TEST 4: When Rule Host throws, gate continues to Progressive Gate (D-08)
268
- // ═══════════════════════════════════════════════════════════════════════════
269
- it('should continue to Progressive Gate when Rule Host throws (D-08)', () => {
270
- setupProfileMock();
133
+ handleBeforeToolCall(event as any, { workspaceDir, sessionId } as any);
271
134
 
272
- // Mock RuleHost.evaluate to throw
273
- _mockEvaluate = vi.fn().mockImplementation(() => {
274
- throw new Error('Host internal error');
275
- });
276
- MockedRuleHost.mockImplementation(function(this: any, _stateDir: string) {
277
- this.evaluate = _mockEvaluate;
135
+ expect(mockEventLogInstance.recordRuleHostEvaluated).toHaveBeenCalledWith(
136
+ expect.objectContaining({
137
+ toolName: 'edit',
138
+ matched: false,
139
+ decision: 'allow',
140
+ })
141
+ );
278
142
  });
143
+ });
279
144
 
280
- const result = handleBeforeToolCall(makeWriteEvent() as any, { workspaceDir, sessionId } as any);
145
+ describe('Rule Host degradation', () => {
146
+ it('should allow operation when Rule Host throws (conservative degradation)', () => {
147
+ _mockEvaluate = vi.fn().mockImplementation(() => {
148
+ throw new Error('Host internal error');
149
+ });
281
150
 
282
- // Should pass through (host error is caught, degrades to Progressive Gate)
283
- // Progressive Gate will pass for this low-risk operation
284
- expect(result).toBeUndefined();
285
- });
151
+ const event = {
152
+ toolName: 'bash',
153
+ params: { command: 'ls -la' },
154
+ };
286
155
 
287
- // ═══════════════════════════════════════════════════════════════════════════
288
- // TEST 5: Block result uses blockSource='rule-host'
289
- // ═══════════════════════════════════════════════════════════════════════════
290
- it('should use blockSource=rule-host for Rule Host blocks', () => {
291
- setupProfileMock();
156
+ const result = handleBeforeToolCall(event as any, { workspaceDir, sessionId } as any);
292
157
 
293
- _mockEvaluate = vi.fn().mockReturnValue({
294
- decision: 'block',
295
- matched: true,
296
- reason: 'Dangerous file modification',
297
- });
298
- MockedRuleHost.mockImplementation(function(this: any, _stateDir: string) {
299
- this.evaluate = _mockEvaluate;
158
+ expect(result).toBeUndefined();
300
159
  });
160
+ });
301
161
 
302
- const result = handleBeforeToolCall(makeWriteEvent() as any, { workspaceDir, sessionId } as any);
162
+ describe('Rule Host requireApproval', () => {
163
+ it('should not block when Rule Host returns requireApproval', () => {
164
+ _mockEvaluate = vi.fn().mockReturnValue({
165
+ decision: 'requireApproval',
166
+ matched: true,
167
+ reason: 'High-risk operation needs approval',
168
+ ruleId: 'R_003',
169
+ });
170
+
171
+ const event = {
172
+ toolName: 'bash',
173
+ params: { command: 'rm -rf node_modules' },
174
+ };
303
175
 
304
- expect(result?.block).toBe(true);
176
+ const result = handleBeforeToolCall(event as any, { workspaceDir, sessionId } as any);
305
177
 
306
- // Verify recordGateBlockAndReturn was called with blockSource='rule-host'
307
- expect(mockEventLog.recordGateBlock).toHaveBeenCalledWith(
308
- sessionId,
309
- expect.objectContaining({
310
- blockSource: 'rule-host',
311
- reason: 'Dangerous file modification',
312
- })
313
- );
178
+ expect(result).toBeUndefined();
179
+ expect(mockEventLogInstance.recordRuleEnforced).toHaveBeenCalledWith(
180
+ expect.objectContaining({ enforcement: 'requireApproval' })
181
+ );
182
+ });
314
183
  });
315
184
 
316
- // ═══════════════════════════════════════════════════════════════════════════
317
- // TEST 5b: requireApproval result uses blockSource='rule-host' with reason
318
- // ═══════════════════════════════════════════════════════════════════════════
319
- it('should use blockSource=rule-host with approval prefix for requireApproval', () => {
320
- setupProfileMock();
321
-
322
- _mockEvaluate = vi.fn().mockReturnValue({
323
- decision: 'requireApproval',
324
- matched: true,
325
- reason: 'High-risk path requires approval',
326
- });
327
- MockedRuleHost.mockImplementation(function(this: any, _stateDir: string) {
328
- this.evaluate = _mockEvaluate;
329
- });
185
+ describe('Early return for non-target tools', () => {
186
+ it('should allow read tool without calling Rule Host', () => {
187
+ const event = {
188
+ toolName: 'read',
189
+ params: { file_path: 'src/readonly.ts' },
190
+ };
330
191
 
331
- const result = handleBeforeToolCall(makeWriteEvent() as any, { workspaceDir, sessionId } as any);
192
+ const result = handleBeforeToolCall(event as any, { workspaceDir, sessionId } as any);
332
193
 
333
- expect(result?.block).toBeUndefined();
334
- // requireApproval records events but does not block — the operation proceeds
335
- // to the Progressive Trust Gate for further evaluation.
336
- // recordRuleEnforced takes a single object arg (no sessionId).
337
- expect(_mockEventLogInstance.recordRuleEnforced).toHaveBeenCalledWith(
338
- expect.objectContaining({ enforcement: 'requireApproval' })
339
- );
340
- expect(_mockEventLogInstance.recordGateBlock).not.toHaveBeenCalled();
341
- });
194
+ expect(result).toBeUndefined();
195
+ expect(_mockEvaluate).not.toHaveBeenCalled();
196
+ });
342
197
 
343
- // ═══════════════════════════════════════════════════════════════════════════
344
- // TEST 6: Existing gate flow still works when no active implementations exist
345
- // ═══════════════════════════════════════════════════════════════════════════
346
- it('should allow operation through existing gate flow with no active implementations', () => {
347
- setupProfileMock();
198
+ it('should allow agent tool without calling Rule Host when no workspace', () => {
199
+ const event = {
200
+ toolName: 'agent',
201
+ params: { task: 'do something' },
202
+ };
348
203
 
349
- // Default mock: RuleHost.evaluate returns undefined (already set in beforeEach)
350
- const result = handleBeforeToolCall(makeWriteEvent() as any, { workspaceDir, sessionId } as any);
204
+ const result = handleBeforeToolCall(event as any, { sessionId } as any);
351
205
 
352
- // Should pass all gates — no block
353
- expect(result).toBeUndefined();
206
+ expect(result).toBeUndefined();
207
+ expect(_mockEvaluate).not.toHaveBeenCalled();
208
+ });
354
209
  });
355
210
 
356
- it('should block with GFI even when Rule Host would allow', () => {
357
- // High GFI triggers GFI block
358
- vi.mocked(sessionTrackerModule.getSession).mockReturnValue({ currentGfi: 85 } as any);
359
- setupProfileMock();
211
+ describe('Session GFI context', () => {
212
+ it('should pass current GFI to Rule Host', () => {
213
+ _mockEvaluate = vi.fn().mockReturnValue(undefined);
214
+ vi.mocked(sessionTracker.getSession).mockReturnValue({ currentGfi: 75 } as any);
360
215
 
361
- // Even if RuleHost would allow, GFI blocks first
362
- const result = handleBeforeToolCall(makeWriteEvent() as any, { workspaceDir, sessionId } as any);
216
+ const event = {
217
+ toolName: 'write',
218
+ params: { file_path: 'src/test.ts', content: 'x' },
219
+ };
363
220
 
364
- expect(result?.block).toBe(true);
365
- expect(result?.blockReason).toContain('GFI');
366
- });
221
+ handleBeforeToolCall(event as any, { workspaceDir, sessionId } as any);
367
222
 
368
- it('should allow edit when oldText matches (full pipeline with Rule Host)', () => {
369
- const fileContent = 'const x = 1;\n';
370
- const editEvent = {
371
- toolName: 'edit',
372
- params: {
373
- file_path: 'src/example.ts',
374
- oldText: 'const x = 1;',
375
- newText: 'const x = 2;',
376
- },
377
- };
378
-
379
- setupProfileMock();
380
-
381
- vi.mocked(fs.readFileSync).mockImplementation((p: any) => {
382
- if (typeof p === 'string' && p.includes('PROFILE.json')) {
383
- return JSON.stringify({
384
- risk_paths: [],
385
- progressive_gate: { enabled: true },
386
- edit_verification: { enabled: true },
387
- });
388
- }
389
- if (typeof p === 'string' && p.includes('example.ts')) {
390
- return fileContent;
391
- }
392
- return '';
393
- });
394
- vi.mocked(fs.statSync).mockReturnValue({ size: 1000 } as any);
395
- vi.mocked(fs.existsSync).mockImplementation((p: any) => {
396
- if (typeof p === 'string' && p.includes('PROFILE.json')) return true;
397
- if (typeof p === 'string' && p.includes('example.ts')) return true;
398
- return false;
223
+ expect(_mockEvaluate).toHaveBeenCalledWith(
224
+ expect.objectContaining({
225
+ session: expect.objectContaining({ currentGfi: 75 }),
226
+ })
227
+ );
399
228
  });
400
-
401
- const result = handleBeforeToolCall(editEvent as any, { workspaceDir, sessionId } as any);
402
-
403
- expect(result).toBeUndefined();
404
229
  });
405
230
  });
@@ -70,7 +70,11 @@ const noopLogger: PluginLogger = {
70
70
  afterEach(() => {
71
71
  vi.restoreAllMocks();
72
72
  for (const dir of tempDirs.splice(0)) {
73
- fs.rmSync(dir, { recursive: true, force: true });
73
+ try {
74
+ fs.rmSync(dir, { recursive: true, force: true });
75
+ } catch {
76
+ // On Windows, temp dirs may be held open — ignore cleanup errors
77
+ }
74
78
  }
75
79
  });
76
80