principles-disciple 1.62.0 → 1.64.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.
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/src/commands/evolution-status.ts +32 -21
- package/src/core/paths.ts +1 -0
- package/src/core/workflow-funnel-loader.ts +36 -5
- package/src/hooks/gate-block-helper.ts +1 -1
- package/src/hooks/gate.ts +27 -205
- package/src/service/runtime-summary-service.ts +5 -1
- package/templates/langs/en/skills/pd-pain-signal/SKILL.md +14 -14
- package/templates/langs/zh/skills/pd-pain-signal/SKILL.md +14 -15
- package/tests/core/workflow-funnel-loader.test.ts +866 -0
- package/tests/hooks/gate-rule-host-pipeline.test.ts +159 -334
- package/tests/service/cooldown-strategy.test.ts +1 -0
- package/tests/service/evolution-worker.compilation-backfill.test.ts +5 -1
- package/src/hooks/bash-risk.ts +0 -175
- package/src/hooks/edit-verification.ts +0 -302
- package/src/hooks/gfi-gate.ts +0 -186
- package/src/hooks/progressive-trust-gate.ts +0 -183
- package/src/hooks/thinking-checkpoint.ts +0 -76
- package/tests/hooks/bash-risk-integration.test.ts +0 -137
- package/tests/hooks/bash-risk.test.ts +0 -81
- package/tests/hooks/edit-verification.test.ts +0 -678
- package/tests/hooks/gate-edit-verification-p1.test.ts +0 -632
- package/tests/hooks/gate-pipeline-integration.test.ts +0 -404
- package/tests/hooks/gate.test.ts +0 -271
- package/tests/hooks/gfi-gate-unit.test.ts +0 -422
- package/tests/hooks/gfi-gate.test.ts +0 -669
- package/tests/hooks/thinking-gate.test.ts +0 -313
|
@@ -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
|
|
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.
|
|
9
|
-
* 2.
|
|
10
|
-
* 3.
|
|
11
|
-
* 4.
|
|
12
|
-
* 5.
|
|
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
|
|
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
|
-
|
|
25
|
-
|
|
19
|
+
const workspaceDir = '/mock/workspace';
|
|
20
|
+
const sessionId = 'test-session-rh';
|
|
26
21
|
|
|
27
|
-
|
|
28
|
-
vi.
|
|
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
|
-
|
|
38
|
-
vi.
|
|
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
|
-
|
|
66
|
-
|
|
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(() =>
|
|
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
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
-
|
|
158
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
235
|
-
expect(result).toBeDefined();
|
|
236
|
-
expect(result?.block).toBe(true);
|
|
100
|
+
handleBeforeToolCall(event as any, { workspaceDir, sessionId } as any);
|
|
237
101
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
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
|
-
|
|
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
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
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
|
-
|
|
122
|
+
expect(result).toBeUndefined();
|
|
123
|
+
});
|
|
258
124
|
|
|
259
|
-
|
|
260
|
-
|
|
125
|
+
it('should record rulehost_evaluated even when no match', () => {
|
|
126
|
+
_mockEvaluate = vi.fn().mockReturnValue(undefined);
|
|
261
127
|
|
|
262
|
-
|
|
263
|
-
|
|
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
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
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
|
-
|
|
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
|
-
|
|
283
|
-
|
|
284
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
176
|
+
const result = handleBeforeToolCall(event as any, { workspaceDir, sessionId } as any);
|
|
305
177
|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
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
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
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
|
-
|
|
192
|
+
const result = handleBeforeToolCall(event as any, { workspaceDir, sessionId } as any);
|
|
332
193
|
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
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
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
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
|
-
|
|
350
|
-
const result = handleBeforeToolCall(makeWriteEvent() as any, { workspaceDir, sessionId } as any);
|
|
204
|
+
const result = handleBeforeToolCall(event as any, { sessionId } as any);
|
|
351
205
|
|
|
352
|
-
|
|
353
|
-
|
|
206
|
+
expect(result).toBeUndefined();
|
|
207
|
+
expect(_mockEvaluate).not.toHaveBeenCalled();
|
|
208
|
+
});
|
|
354
209
|
});
|
|
355
210
|
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
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
|
-
|
|
362
|
-
|
|
216
|
+
const event = {
|
|
217
|
+
toolName: 'write',
|
|
218
|
+
params: { file_path: 'src/test.ts', content: 'x' },
|
|
219
|
+
};
|
|
363
220
|
|
|
364
|
-
|
|
365
|
-
expect(result?.blockReason).toContain('GFI');
|
|
366
|
-
});
|
|
221
|
+
handleBeforeToolCall(event as any, { workspaceDir, sessionId } as any);
|
|
367
222
|
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
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
|
});
|
|
@@ -60,6 +60,7 @@ describe('cooldown-strategy', () => {
|
|
|
60
60
|
|
|
61
61
|
it('independent state per task kind', async () => {
|
|
62
62
|
await recordPersistentFailure(tmpDir, 'sleep_reflection');
|
|
63
|
+
await new Promise((r) => setTimeout(r, 10)); // ensure distinct timestamps
|
|
63
64
|
await recordPersistentFailure(tmpDir, 'keyword_optimization');
|
|
64
65
|
const state = await readState(tmpDir);
|
|
65
66
|
|
|
@@ -70,7 +70,11 @@ const noopLogger: PluginLogger = {
|
|
|
70
70
|
afterEach(() => {
|
|
71
71
|
vi.restoreAllMocks();
|
|
72
72
|
for (const dir of tempDirs.splice(0)) {
|
|
73
|
-
|
|
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
|
|