principles-disciple 1.10.0 → 1.12.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/CHANGELOG.md +26 -0
- package/README.md +66 -0
- package/esbuild.config.js +1 -1
- package/openclaw.plugin.json +4 -4
- package/package.json +2 -3
- package/run-nocturnal.mjs +30 -0
- package/scripts/db-migrate.mjs +170 -0
- package/scripts/sync-plugin.mjs +94 -5
- package/src/commands/archive-impl.ts +136 -0
- package/src/commands/capabilities.ts +4 -2
- package/src/commands/context.ts +5 -1
- package/src/commands/disable-impl.ts +151 -0
- package/src/commands/evolution-status.ts +64 -19
- package/src/commands/export.ts +8 -6
- package/src/commands/focus.ts +8 -20
- package/src/commands/nocturnal-review.ts +5 -7
- package/src/commands/nocturnal-rollout.ts +1 -12
- package/src/commands/nocturnal-train.ts +17 -42
- package/src/commands/pain.ts +10 -5
- package/src/commands/principle-rollback.ts +4 -2
- package/src/commands/promote-impl.ts +274 -0
- package/src/commands/rollback-impl.ts +234 -0
- package/src/commands/rollback.ts +6 -3
- package/src/commands/samples.ts +2 -0
- package/src/commands/thinking-os.ts +3 -4
- package/src/commands/workflow-debug.ts +2 -1
- package/src/config/errors.ts +1 -0
- package/src/core/AGENTS.md +34 -0
- package/src/core/adaptive-thresholds.ts +4 -3
- package/src/core/code-implementation-storage.ts +241 -0
- package/src/core/config.ts +5 -2
- package/src/core/control-ui-db.ts +29 -10
- package/src/core/detection-funnel.ts +12 -7
- package/src/core/diagnostician-task-store.ts +156 -0
- package/src/core/dictionary.ts +4 -4
- package/src/core/empathy-keyword-matcher.ts +7 -3
- package/src/core/empathy-types.ts +13 -2
- package/src/core/event-log.ts +14 -6
- package/src/core/evolution-engine.ts +27 -31
- package/src/core/evolution-logger.ts +3 -2
- package/src/core/evolution-reducer.ts +83 -31
- package/src/core/evolution-types.ts +10 -0
- package/src/core/external-training-contract.ts +1 -0
- package/src/core/focus-history.ts +38 -24
- package/src/core/hygiene/tracker.ts +10 -6
- package/src/core/init.ts +5 -2
- package/src/core/migration.ts +3 -3
- package/src/core/model-deployment-registry.ts +6 -4
- package/src/core/model-training-registry.ts +5 -3
- package/src/core/nocturnal-arbiter.ts +13 -14
- package/src/core/nocturnal-artifact-lineage.ts +117 -0
- package/src/core/nocturnal-artificer.ts +257 -0
- package/src/core/nocturnal-candidate-scoring.ts +4 -2
- package/src/core/nocturnal-compliance.ts +67 -19
- package/src/core/nocturnal-dataset.ts +95 -2
- package/src/core/nocturnal-executability.ts +2 -3
- package/src/core/nocturnal-export.ts +6 -3
- package/src/core/nocturnal-rule-implementation-validator.ts +245 -0
- package/src/core/nocturnal-trajectory-extractor.ts +10 -3
- package/src/core/nocturnal-trinity.ts +300 -57
- package/src/core/pain-context-extractor.ts +29 -15
- package/src/core/pain.ts +7 -5
- package/src/core/path-resolver.ts +16 -15
- package/src/core/paths.ts +2 -1
- package/src/core/pd-task-reconciler.ts +463 -0
- package/src/core/pd-task-service.ts +42 -0
- package/src/core/pd-task-store.ts +77 -0
- package/src/core/pd-task-types.ts +128 -0
- package/src/core/principle-internalization/deprecated-readiness.ts +91 -0
- package/src/core/principle-internalization/internalization-routing-policy.ts +208 -0
- package/src/core/principle-internalization/lifecycle-metrics.ts +149 -0
- package/src/core/principle-internalization/lifecycle-read-model.ts +243 -0
- package/src/core/principle-internalization/lifecycle-refresh.ts +11 -0
- package/src/core/principle-internalization/principle-lifecycle-service.ts +167 -0
- package/src/core/principle-training-state.ts +95 -370
- package/src/core/principle-tree-ledger.ts +713 -0
- package/src/core/profile.ts +3 -1
- package/src/core/promotion-gate.ts +14 -18
- package/src/core/replay-engine.ts +562 -0
- package/src/core/risk-calculator.ts +6 -4
- package/src/core/rule-host-helpers.ts +39 -0
- package/src/core/rule-host-types.ts +82 -0
- package/src/core/rule-host.ts +245 -0
- package/src/core/rule-implementation-runtime.ts +38 -0
- package/src/core/schema/db-types.ts +16 -0
- package/src/core/schema/index.ts +26 -0
- package/src/core/schema/migration-runner.ts +207 -0
- package/src/core/schema/migrations/001-init-trajectory.ts +211 -0
- package/src/core/schema/migrations/002-init-central.ts +122 -0
- package/src/core/schema/migrations/003-init-workflow.ts +55 -0
- package/src/core/schema/migrations/004-add-thinking-and-gfi.ts +74 -0
- package/src/core/schema/migrations/index.ts +31 -0
- package/src/core/schema/schema-definitions.ts +650 -0
- package/src/core/session-tracker.ts +6 -4
- package/src/core/shadow-observation-registry.ts +6 -3
- package/src/core/system-logger.ts +2 -2
- package/src/core/thinking-models.ts +182 -46
- package/src/core/thinking-os-parser.ts +164 -0
- package/src/core/training-program.ts +7 -7
- package/src/core/trajectory.ts +42 -36
- package/src/core/workspace-context.ts +77 -11
- package/src/core/workspace-dir-validation.ts +152 -0
- package/src/hooks/AGENTS.md +31 -0
- package/src/hooks/bash-risk.ts +3 -1
- package/src/hooks/edit-verification.ts +9 -5
- package/src/hooks/gate-block-helper.ts +5 -1
- package/src/hooks/gate.ts +152 -5
- package/src/hooks/gfi-gate.ts +9 -2
- package/src/hooks/lifecycle-routing.ts +124 -0
- package/src/hooks/lifecycle.ts +12 -12
- package/src/hooks/llm.ts +17 -109
- package/src/hooks/message-sanitize.ts +5 -3
- package/src/hooks/pain.ts +19 -15
- package/src/hooks/progressive-trust-gate.ts +7 -1
- package/src/hooks/prompt.ts +169 -60
- package/src/hooks/subagent.ts +5 -4
- package/src/hooks/thinking-checkpoint.ts +2 -0
- package/src/hooks/trajectory-collector.ts +15 -12
- package/src/http/principles-console-route.ts +31 -68
- package/src/i18n/commands.ts +2 -2
- package/src/index.ts +126 -40
- package/src/service/central-database.ts +131 -43
- package/src/service/central-health-service.ts +47 -0
- package/src/service/central-overview-service.ts +135 -0
- package/src/service/central-sync-service.ts +87 -0
- package/src/service/control-ui-query-service.ts +46 -36
- package/src/service/event-log-auditor.ts +261 -0
- package/src/service/evolution-query-service.ts +23 -22
- package/src/service/evolution-worker.ts +565 -261
- package/src/service/health-query-service.ts +213 -36
- package/src/service/nocturnal-runtime.ts +8 -4
- package/src/service/nocturnal-service.ts +499 -59
- package/src/service/nocturnal-target-selector.ts +5 -7
- package/src/service/runtime-summary-service.ts +2 -1
- package/src/service/subagent-workflow/deep-reflect-workflow-manager.ts +25 -336
- package/src/service/subagent-workflow/dynamic-timeout.ts +30 -0
- package/src/service/subagent-workflow/empathy-observer-workflow-manager.ts +48 -386
- package/src/service/subagent-workflow/index.ts +2 -0
- package/src/service/subagent-workflow/nocturnal-workflow-manager.ts +155 -285
- package/src/service/subagent-workflow/runtime-direct-driver.ts +114 -16
- package/src/service/subagent-workflow/subagent-error-utils.ts +25 -0
- package/src/service/subagent-workflow/types.ts +9 -4
- package/src/service/subagent-workflow/workflow-manager-base.ts +573 -0
- package/src/service/subagent-workflow/workflow-store.ts +71 -11
- package/src/service/trajectory-service.ts +2 -1
- package/src/tools/critique-prompt.ts +1 -1
- package/src/tools/deep-reflect.ts +175 -209
- package/src/tools/model-index.ts +2 -1
- package/src/types/event-types.ts +2 -2
- package/src/types/principle-tree-schema.ts +29 -23
- package/src/utils/file-lock.ts +5 -3
- package/src/utils/io.ts +5 -2
- package/src/utils/nlp.ts +5 -46
- package/src/utils/node-vm-polyfill.ts +11 -0
- package/src/utils/plugin-logger.ts +2 -0
- package/src/utils/retry.ts +572 -0
- package/src/utils/subagent-probe.ts +1 -1
- package/templates/langs/en/core/AGENTS.md +0 -13
- package/templates/langs/en/core/SOUL.md +1 -31
- package/templates/langs/en/core/TOOLS.md +0 -4
- package/templates/langs/en/principles/THINKING_OS.md +64 -0
- package/templates/langs/en/skills/admin/SKILL.md +0 -1
- package/templates/langs/en/skills/evolution-framework-update/SKILL.md +1 -1
- package/templates/langs/en/skills/pd-diagnostician/SKILL.md +18 -5
- package/templates/langs/zh/core/AGENTS.md +0 -22
- package/templates/langs/zh/core/SOUL.md +1 -31
- package/templates/langs/zh/core/TOOLS.md +0 -4
- package/templates/langs/zh/principles/THINKING_OS.md +64 -0
- package/templates/langs/zh/skills/admin/SKILL.md +0 -1
- package/templates/langs/zh/skills/evolution-framework-update/SKILL.md +1 -1
- package/templates/langs/zh/skills/pd-diagnostician/SKILL.md +25 -4
- package/tests/commands/evolution-status.test.ts +119 -0
- package/tests/commands/implementation-lifecycle.test.ts +362 -0
- package/tests/core/code-implementation-storage.test.ts +398 -0
- package/tests/core/evolution-reducer.detector-metadata.test.ts +28 -28
- package/tests/core/nocturnal-artifact-lineage.test.ts +53 -0
- package/tests/core/nocturnal-artificer.test.ts +241 -0
- package/tests/core/nocturnal-compliance-p-principles.test.ts +133 -0
- package/tests/core/nocturnal-rule-implementation-validator.test.ts +127 -0
- package/tests/core/pd-task-store.test.ts +126 -0
- package/tests/core/principle-internalization/deprecated-readiness.test.ts +193 -0
- package/tests/core/principle-internalization/internalization-routing-policy.test.ts +212 -0
- package/tests/core/principle-internalization/lifecycle-metrics.test.ts +350 -0
- package/tests/core/principle-internalization/principle-lifecycle-service.test.ts +211 -0
- package/tests/core/principle-training-state.test.ts +228 -1
- package/tests/core/principle-tree-ledger.test.ts +423 -0
- package/tests/core/regression-v1-9-1.test.ts +265 -0
- package/tests/core/replay-engine.test.ts +234 -0
- package/tests/core/rule-host-helpers.test.ts +120 -0
- package/tests/core/rule-host.test.ts +389 -0
- package/tests/core/rule-implementation-runtime.test.ts +64 -0
- package/tests/core/workspace-context.test.ts +53 -0
- package/tests/core/workspace-dir-validation.test.ts +272 -0
- package/tests/hooks/gate-rule-host-pipeline.test.ts +385 -0
- package/tests/hooks/pain.test.ts +74 -10
- package/tests/hooks/prompt.test.ts +63 -1
- package/tests/integration/principle-lifecycle.e2e.test.ts +197 -0
- package/tests/integration/tool-hooks-workspace-dir.e2e.test.ts +211 -0
- package/tests/service/data-endpoints-regression.test.ts +834 -0
- package/tests/service/evolution-worker.test.ts +0 -123
- package/tests/service/nocturnal-service-code-candidate.test.ts +330 -0
- package/tests/utils/nlp.test.ts +1 -19
- package/tests/utils/retry.test.ts +327 -0
- package/ui/src/App.tsx +1 -1
- package/ui/src/api.ts +4 -0
- package/ui/src/charts.tsx +366 -0
- package/ui/src/components/WorkspaceConfig.tsx +107 -75
- package/ui/src/i18n/ui.ts +55 -22
- package/ui/src/pages/OverviewPage.tsx +441 -81
- package/ui/src/styles.css +43 -0
- package/ui/src/types.ts +17 -1
- package/src/agents/nocturnal-dreamer.md +0 -152
- package/src/agents/nocturnal-philosopher.md +0 -138
- package/src/agents/nocturnal-reflector.md +0 -126
- package/src/agents/nocturnal-scribe.md +0 -164
- package/templates/workspace/.principles/00-kernel.md +0 -51
- package/templates/workspace/.principles/DECISION_POLICY.json +0 -44
- package/templates/workspace/.principles/PRINCIPLES.md +0 -20
- package/templates/workspace/.principles/PROFILE.json +0 -54
- package/templates/workspace/.principles/PROFILE.schema.json +0 -56
- package/templates/workspace/.principles/THINKING_OS.md +0 -64
- package/templates/workspace/.principles/THINKING_OS_ARCHIVE.md +0 -7
- package/templates/workspace/.principles/THINKING_OS_CANDIDATES.md +0 -9
- package/templates/workspace/.principles/models/_INDEX.md +0 -27
- package/templates/workspace/.principles/models/first_principles.md +0 -62
- package/templates/workspace/.principles/models/marketing_4p.md +0 -52
- package/templates/workspace/.principles/models/porter_five.md +0 -63
- package/templates/workspace/.principles/models/swot.md +0 -60
- package/templates/workspace/.principles/models/user_story_map.md +0 -63
- package/templates/workspace/.state/WORKBOARD.json +0 -4
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for workspace-dir-validation.ts
|
|
3
|
+
*
|
|
4
|
+
* Tests the core validation logic and 3-tier fallback strategy
|
|
5
|
+
* for resolving correct workspaceDir in tool hooks.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
9
|
+
import * as os from 'os';
|
|
10
|
+
import { validateWorkspaceDir, resolveValidWorkspaceDir, logWorkspaceDirHealth } from '../../src/core/workspace-dir-validation.js';
|
|
11
|
+
|
|
12
|
+
const homeDir = os.homedir();
|
|
13
|
+
|
|
14
|
+
describe('validateWorkspaceDir', () => {
|
|
15
|
+
it('should return error for undefined', () => {
|
|
16
|
+
const result = validateWorkspaceDir(undefined);
|
|
17
|
+
expect(result).toBe('workspaceDir is undefined/null');
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('should return error for null (treated as undefined)', () => {
|
|
21
|
+
const result = validateWorkspaceDir(null as unknown as string);
|
|
22
|
+
expect(result).toBe('workspaceDir is undefined/null');
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('should return error for empty string (treated as falsy)', () => {
|
|
26
|
+
const result = validateWorkspaceDir('');
|
|
27
|
+
// Empty string is falsy, so it's treated like undefined
|
|
28
|
+
expect(result).toContain('undefined/null');
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('should return error for root directory', () => {
|
|
32
|
+
const result = validateWorkspaceDir('/');
|
|
33
|
+
expect(result).toContain('root or empty');
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('should return error for home directory', () => {
|
|
37
|
+
const result = validateWorkspaceDir(homeDir);
|
|
38
|
+
expect(result).toContain('equals home directory');
|
|
39
|
+
expect(result).toContain(homeDir);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('should return error for home directory with trailing slash', () => {
|
|
43
|
+
const result = validateWorkspaceDir(`${homeDir}/`);
|
|
44
|
+
expect(result).toContain('home directory');
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('should return null (valid) for a proper workspace path', () => {
|
|
48
|
+
const result = validateWorkspaceDir('/home/user/projects/my-workspace');
|
|
49
|
+
expect(result).toBeNull();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('should return null (valid) for a nested workspace path', () => {
|
|
53
|
+
const result = validateWorkspaceDir('/home/user/.openclaw/workspace-main');
|
|
54
|
+
expect(result).toBeNull();
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('should return null (valid) for temp directory', () => {
|
|
58
|
+
const result = validateWorkspaceDir('/tmp/test-workspace');
|
|
59
|
+
expect(result).toBeNull();
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('should return null (valid) for Windows-style path', () => {
|
|
63
|
+
// On Linux, Windows paths are just regular paths
|
|
64
|
+
const result = validateWorkspaceDir('C:\\Users\\test\\workspace');
|
|
65
|
+
expect(result).toBeNull();
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
describe('resolveValidWorkspaceDir', () => {
|
|
70
|
+
const mockLogger = {
|
|
71
|
+
warn: vi.fn(),
|
|
72
|
+
error: vi.fn(),
|
|
73
|
+
info: vi.fn(),
|
|
74
|
+
debug: vi.fn(),
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const mockApi = {
|
|
78
|
+
runtime: {
|
|
79
|
+
agent: {
|
|
80
|
+
resolveAgentWorkspaceDir: vi.fn(),
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
config: {},
|
|
84
|
+
resolvePath: vi.fn(),
|
|
85
|
+
logger: mockLogger,
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
beforeEach(() => {
|
|
89
|
+
vi.clearAllMocks();
|
|
90
|
+
mockApi.resolvePath.mockReturnValue('/default/workspace');
|
|
91
|
+
mockApi.runtime.agent.resolveAgentWorkspaceDir.mockReturnValue('/resolved/from/agent');
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
afterEach(() => {
|
|
95
|
+
vi.resetAllMocks();
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('should return ctx.workspaceDir when valid', () => {
|
|
99
|
+
const ctx = { workspaceDir: '/valid/workspace', agentId: 'agent-1' };
|
|
100
|
+
const result = resolveValidWorkspaceDir(ctx, mockApi as any, { source: 'test' });
|
|
101
|
+
|
|
102
|
+
expect(result).toBe('/valid/workspace');
|
|
103
|
+
expect(mockLogger.warn).not.toHaveBeenCalled();
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('should warn and continue when ctx.workspaceDir is home directory', () => {
|
|
107
|
+
const ctx = { workspaceDir: homeDir, agentId: 'agent-1' };
|
|
108
|
+
const result = resolveValidWorkspaceDir(ctx, mockApi as any, { source: 'test' });
|
|
109
|
+
|
|
110
|
+
// Should fall back to agentId resolution
|
|
111
|
+
expect(result).toBe('/resolved/from/agent');
|
|
112
|
+
expect(mockLogger.warn).toHaveBeenCalled();
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('should try agentId resolution when ctx.workspaceDir is undefined', () => {
|
|
116
|
+
const ctx = { workspaceDir: undefined, agentId: 'agent-1' };
|
|
117
|
+
const result = resolveValidWorkspaceDir(ctx, mockApi as any, { source: 'test' });
|
|
118
|
+
|
|
119
|
+
expect(mockApi.runtime.agent.resolveAgentWorkspaceDir).toHaveBeenCalledWith(mockApi.config, 'agent-1');
|
|
120
|
+
expect(result).toBe('/resolved/from/agent');
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('should fallback to resolvePath when agentId is undefined', () => {
|
|
124
|
+
const ctx = { workspaceDir: undefined, agentId: undefined };
|
|
125
|
+
const result = resolveValidWorkspaceDir(ctx, mockApi as any, { source: 'test' });
|
|
126
|
+
|
|
127
|
+
expect(mockApi.resolvePath).toHaveBeenCalledWith('.');
|
|
128
|
+
expect(result).toBe('/default/workspace');
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('should fallback to resolvePath when agentId resolution fails', () => {
|
|
132
|
+
mockApi.runtime.agent.resolveAgentWorkspaceDir.mockImplementation(() => {
|
|
133
|
+
throw new Error('Agent not found');
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
const ctx = { workspaceDir: undefined, agentId: 'unknown-agent' };
|
|
137
|
+
const result = resolveValidWorkspaceDir(ctx, mockApi as any, { source: 'test' });
|
|
138
|
+
|
|
139
|
+
expect(mockApi.resolvePath).toHaveBeenCalledWith('.');
|
|
140
|
+
expect(result).toBe('/default/workspace');
|
|
141
|
+
expect(mockLogger.warn).toHaveBeenCalledWith(expect.stringContaining('failed to resolve from agentId'));
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('should fallback to resolvePath when agentId resolution returns home directory', () => {
|
|
145
|
+
mockApi.runtime.agent.resolveAgentWorkspaceDir.mockReturnValue(homeDir);
|
|
146
|
+
|
|
147
|
+
const ctx = { workspaceDir: undefined, agentId: 'agent-1' };
|
|
148
|
+
const result = resolveValidWorkspaceDir(ctx, mockApi as any, { source: 'test' });
|
|
149
|
+
|
|
150
|
+
expect(mockApi.resolvePath).toHaveBeenCalledWith('.');
|
|
151
|
+
expect(result).toBe('/default/workspace');
|
|
152
|
+
expect(mockLogger.warn).toHaveBeenCalledWith(expect.stringContaining('invalid'));
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('should warn when all fallbacks fail', () => {
|
|
156
|
+
mockApi.resolvePath.mockReturnValue(homeDir);
|
|
157
|
+
|
|
158
|
+
const ctx = { workspaceDir: undefined, agentId: undefined };
|
|
159
|
+
const result = resolveValidWorkspaceDir(ctx, mockApi as any, { source: 'test' });
|
|
160
|
+
|
|
161
|
+
expect(result).toBe(homeDir);
|
|
162
|
+
expect(mockLogger.warn).toHaveBeenCalledWith(expect.stringContaining('FINAL FALLBACK'));
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it('should use custom onWarning callback', () => {
|
|
166
|
+
const customWarning = vi.fn();
|
|
167
|
+
|
|
168
|
+
const ctx = { workspaceDir: homeDir, agentId: undefined };
|
|
169
|
+
mockApi.resolvePath.mockReturnValue('/valid/fallback');
|
|
170
|
+
|
|
171
|
+
const result = resolveValidWorkspaceDir(ctx, mockApi as any, {
|
|
172
|
+
source: 'test',
|
|
173
|
+
onWarning: customWarning,
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
expect(customWarning).toHaveBeenCalled();
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it('should use default source "unknown" when not provided', () => {
|
|
180
|
+
const ctx = { workspaceDir: homeDir, agentId: undefined };
|
|
181
|
+
mockApi.resolvePath.mockReturnValue('/valid/fallback');
|
|
182
|
+
|
|
183
|
+
resolveValidWorkspaceDir(ctx, mockApi as any);
|
|
184
|
+
|
|
185
|
+
expect(mockLogger.warn).toHaveBeenCalledWith(expect.stringContaining('unknown:'));
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
describe('logWorkspaceDirHealth', () => {
|
|
190
|
+
const mockLogger = {
|
|
191
|
+
warn: vi.fn(),
|
|
192
|
+
error: vi.fn(),
|
|
193
|
+
info: vi.fn(),
|
|
194
|
+
debug: vi.fn(),
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
const mockApi = {
|
|
198
|
+
runtime: {
|
|
199
|
+
agent: {
|
|
200
|
+
resolveAgentWorkspaceDir: vi.fn().mockReturnValue('/valid/workspace'),
|
|
201
|
+
},
|
|
202
|
+
},
|
|
203
|
+
config: {},
|
|
204
|
+
resolvePath: vi.fn().mockReturnValue('/valid/workspace'),
|
|
205
|
+
logger: mockLogger,
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
beforeEach(() => {
|
|
209
|
+
vi.clearAllMocks();
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it('should log info when workspaceDir is valid', () => {
|
|
213
|
+
const ctx = { workspaceDir: '/valid/workspace' };
|
|
214
|
+
logWorkspaceDirHealth(ctx, 'startup', mockApi as any);
|
|
215
|
+
|
|
216
|
+
expect(mockLogger.info).toHaveBeenCalledWith(expect.stringContaining('✓'));
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it('should log error when workspaceDir is invalid', () => {
|
|
220
|
+
mockApi.resolvePath.mockReturnValue(homeDir);
|
|
221
|
+
|
|
222
|
+
const ctx = { workspaceDir: undefined };
|
|
223
|
+
logWorkspaceDirHealth(ctx, 'startup', mockApi as any);
|
|
224
|
+
|
|
225
|
+
expect(mockLogger.error).toHaveBeenCalledWith(expect.stringContaining(homeDir));
|
|
226
|
+
});
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
describe('Integration: 3-tier fallback chain', () => {
|
|
230
|
+
const mockLogger = {
|
|
231
|
+
warn: vi.fn(),
|
|
232
|
+
error: vi.fn(),
|
|
233
|
+
info: vi.fn(),
|
|
234
|
+
debug: vi.fn(),
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
it('should follow the complete fallback chain: ctx.workspaceDir -> agentId -> resolvePath', () => {
|
|
238
|
+
const mockApi = {
|
|
239
|
+
runtime: {
|
|
240
|
+
agent: {
|
|
241
|
+
resolveAgentWorkspaceDir: vi.fn().mockReturnValue('/workspace/from-agent'),
|
|
242
|
+
},
|
|
243
|
+
},
|
|
244
|
+
config: {},
|
|
245
|
+
resolvePath: vi.fn().mockReturnValue('/workspace/from-resolvePath'),
|
|
246
|
+
logger: mockLogger,
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
// Case 1: ctx.workspaceDir is valid - use it directly
|
|
250
|
+
const ctx1 = { workspaceDir: '/workspace/from-ctx', agentId: 'agent-1' };
|
|
251
|
+
const result1 = resolveValidWorkspaceDir(ctx1, mockApi as any, { source: 'test' });
|
|
252
|
+
expect(result1).toBe('/workspace/from-ctx');
|
|
253
|
+
expect(mockApi.runtime.agent.resolveAgentWorkspaceDir).not.toHaveBeenCalled();
|
|
254
|
+
|
|
255
|
+
vi.clearAllMocks();
|
|
256
|
+
|
|
257
|
+
// Case 2: ctx.workspaceDir is invalid, agentId resolution works
|
|
258
|
+
const ctx2 = { workspaceDir: homeDir, agentId: 'agent-1' };
|
|
259
|
+
const result2 = resolveValidWorkspaceDir(ctx2, mockApi as any, { source: 'test' });
|
|
260
|
+
expect(result2).toBe('/workspace/from-agent');
|
|
261
|
+
expect(mockApi.runtime.agent.resolveAgentWorkspaceDir).toHaveBeenCalled();
|
|
262
|
+
|
|
263
|
+
vi.clearAllMocks();
|
|
264
|
+
|
|
265
|
+
// Case 3: ctx.workspaceDir is invalid, agentId resolution returns invalid, use resolvePath
|
|
266
|
+
mockApi.runtime.agent.resolveAgentWorkspaceDir.mockReturnValue(homeDir);
|
|
267
|
+
const ctx3 = { workspaceDir: homeDir, agentId: 'agent-1' };
|
|
268
|
+
const result3 = resolveValidWorkspaceDir(ctx3, mockApi as any, { source: 'test' });
|
|
269
|
+
expect(result3).toBe('/workspace/from-resolvePath');
|
|
270
|
+
expect(mockApi.resolvePath).toHaveBeenCalledWith('.');
|
|
271
|
+
});
|
|
272
|
+
});
|
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gate Rule Host Pipeline Integration Tests
|
|
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.
|
|
6
|
+
*
|
|
7
|
+
* 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
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
17
|
+
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
|
+
import * as sessionTracker from '../../src/core/session-tracker.js';
|
|
22
|
+
import * as evolutionEngine from '../../src/core/evolution-engine.js';
|
|
23
|
+
|
|
24
|
+
// Mock fs
|
|
25
|
+
vi.mock('fs');
|
|
26
|
+
|
|
27
|
+
// Mock workspace context
|
|
28
|
+
vi.mock('../../src/core/workspace-context.js');
|
|
29
|
+
|
|
30
|
+
// Mock session tracker
|
|
31
|
+
vi.mock('../../src/core/session-tracker.js', () => ({
|
|
32
|
+
getSession: vi.fn(() => ({ currentGfi: 0 })),
|
|
33
|
+
trackBlock: vi.fn(),
|
|
34
|
+
hasRecentThinking: vi.fn(() => false),
|
|
35
|
+
}));
|
|
36
|
+
|
|
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(() => []),
|
|
63
|
+
}));
|
|
64
|
+
|
|
65
|
+
import { RuleHost } from '../../src/core/rule-host.js';
|
|
66
|
+
import * as sessionTrackerModule from '../../src/core/session-tracker.js';
|
|
67
|
+
import * as evolutionEngineModule from '../../src/core/evolution-engine.js';
|
|
68
|
+
|
|
69
|
+
const MockedRuleHost = vi.mocked(RuleHost);
|
|
70
|
+
|
|
71
|
+
const mockEvolution = {
|
|
72
|
+
getTier: vi.fn().mockReturnValue(3),
|
|
73
|
+
getPoints: vi.fn().mockReturnValue(200),
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
describe('Gate Rule Host Pipeline Integration', () => {
|
|
77
|
+
const workspaceDir = '/mock/workspace';
|
|
78
|
+
const sessionId = 'test-session-rh';
|
|
79
|
+
|
|
80
|
+
const mockConfig = {
|
|
81
|
+
get: vi.fn().mockImplementation((key: string) => {
|
|
82
|
+
if (key === 'trust') return {
|
|
83
|
+
limits: { stage_2_max_lines: 50, stage_3_max_lines: 300 }
|
|
84
|
+
};
|
|
85
|
+
if (key === 'gfi_gate') return {
|
|
86
|
+
enabled: true,
|
|
87
|
+
thresholds: { low_risk_block: 70, high_risk_block: 40 },
|
|
88
|
+
bash_safe_patterns: ['^(ls|dir|pwd)$'],
|
|
89
|
+
bash_dangerous_patterns: ['rm\\s+-rf'],
|
|
90
|
+
};
|
|
91
|
+
return undefined;
|
|
92
|
+
})
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const mockEventLog = {
|
|
96
|
+
recordGateBlock: vi.fn(),
|
|
97
|
+
recordPlanApproval: vi.fn(),
|
|
98
|
+
recordGateBypass: vi.fn(),
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const mockTrajectory = {
|
|
102
|
+
recordGateBlock: vi.fn(),
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const mockWctx = {
|
|
106
|
+
workspaceDir,
|
|
107
|
+
stateDir: '/mock/state',
|
|
108
|
+
config: mockConfig,
|
|
109
|
+
eventLog: mockEventLog,
|
|
110
|
+
trajectory: mockTrajectory,
|
|
111
|
+
evolution: mockEvolution,
|
|
112
|
+
resolve: vi.fn().mockImplementation((key: string) => {
|
|
113
|
+
if (key === 'PROFILE') return path.join(workspaceDir, '.principles', 'PROFILE.json');
|
|
114
|
+
if (key === 'PLAN') return path.join(workspaceDir, 'PLAN.md');
|
|
115
|
+
if (key === 'STATE_DIR') return path.join(workspaceDir, '.state');
|
|
116
|
+
if (typeof key === 'string' && !key.includes(':')) {
|
|
117
|
+
return path.join(workspaceDir, key);
|
|
118
|
+
}
|
|
119
|
+
return key;
|
|
120
|
+
}),
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
beforeEach(() => {
|
|
124
|
+
vi.clearAllMocks();
|
|
125
|
+
vi.useFakeTimers();
|
|
126
|
+
// Reset the shared evaluate mock to default (returns undefined)
|
|
127
|
+
_mockEvaluate = vi.fn().mockReturnValue(undefined);
|
|
128
|
+
MockedRuleHost.mockImplementation(function(this: any, _stateDir: string) {
|
|
129
|
+
this.evaluate = _mockEvaluate;
|
|
130
|
+
});
|
|
131
|
+
vi.mocked(WorkspaceContext.fromHookContext).mockReturnValue(mockWctx as any);
|
|
132
|
+
vi.mocked(sessionTrackerModule.getSession).mockReturnValue({ currentGfi: 0 } as any);
|
|
133
|
+
vi.mocked(sessionTrackerModule.trackBlock).mockImplementation(() => {});
|
|
134
|
+
vi.mocked(evolutionEngineModule.getEvolutionEngine).mockReturnValue(mockEvolution);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
afterEach(() => {
|
|
138
|
+
vi.useRealTimers();
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Helper: create a standard write event
|
|
143
|
+
*/
|
|
144
|
+
function makeWriteEvent(overrides?: Partial<any>) {
|
|
145
|
+
return {
|
|
146
|
+
toolName: 'write',
|
|
147
|
+
params: {
|
|
148
|
+
file_path: 'src/test.ts',
|
|
149
|
+
content: 'const x = 1;',
|
|
150
|
+
},
|
|
151
|
+
...overrides,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Helper: set up fs mocks for a profile with progressive gate enabled
|
|
157
|
+
*/
|
|
158
|
+
function setupProfileMock(profileOverrides?: Record<string, unknown>) {
|
|
159
|
+
vi.mocked(fs.existsSync).mockImplementation((p: any) => {
|
|
160
|
+
if (typeof p === 'string' && p.includes('PROFILE.json')) return true;
|
|
161
|
+
return false;
|
|
162
|
+
});
|
|
163
|
+
vi.mocked(fs.readFileSync).mockImplementation((p: any) => {
|
|
164
|
+
if (typeof p === 'string' && p.includes('PROFILE.json')) {
|
|
165
|
+
return JSON.stringify({
|
|
166
|
+
risk_paths: [],
|
|
167
|
+
progressive_gate: { enabled: true },
|
|
168
|
+
edit_verification: { enabled: true },
|
|
169
|
+
...profileOverrides,
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
return '';
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
177
|
+
// TEST 1: When GFI blocks, Rule Host is never called
|
|
178
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
179
|
+
it('should not call Rule Host when GFI gate blocks', () => {
|
|
180
|
+
// Set high GFI to trigger GFI block
|
|
181
|
+
vi.mocked(sessionTrackerModule.getSession).mockReturnValue({ currentGfi: 85 } as any);
|
|
182
|
+
setupProfileMock();
|
|
183
|
+
|
|
184
|
+
const result = handleBeforeToolCall(makeWriteEvent() as any, { workspaceDir, sessionId } as any);
|
|
185
|
+
|
|
186
|
+
// GFI should block
|
|
187
|
+
expect(result).toBeDefined();
|
|
188
|
+
expect(result?.block).toBe(true);
|
|
189
|
+
expect(result?.blockReason).toContain('GFI');
|
|
190
|
+
|
|
191
|
+
// RuleHost constructor should NOT have been called
|
|
192
|
+
// (GFI returns before reaching Rule Host evaluation)
|
|
193
|
+
expect(MockedRuleHost).not.toHaveBeenCalled();
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
197
|
+
// TEST 2: When Rule Host blocks, Progressive Gate is never called
|
|
198
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
199
|
+
it('should not reach Progressive Gate when Rule Host blocks', () => {
|
|
200
|
+
setupProfileMock();
|
|
201
|
+
|
|
202
|
+
// Mock RuleHost.evaluate to return a block
|
|
203
|
+
_mockEvaluate = vi.fn().mockReturnValue({
|
|
204
|
+
decision: 'block',
|
|
205
|
+
matched: true,
|
|
206
|
+
reason: 'Rule Host test block',
|
|
207
|
+
});
|
|
208
|
+
MockedRuleHost.mockImplementation(function(this: any, _stateDir: string) {
|
|
209
|
+
this.evaluate = _mockEvaluate;
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
const result = handleBeforeToolCall(makeWriteEvent() as any, { workspaceDir, sessionId } as any);
|
|
213
|
+
|
|
214
|
+
// Rule Host should block
|
|
215
|
+
expect(result).toBeDefined();
|
|
216
|
+
expect(result?.block).toBe(true);
|
|
217
|
+
|
|
218
|
+
// The block should come from rule-host
|
|
219
|
+
expect(mockEventLog.recordGateBlock).toHaveBeenCalledWith(
|
|
220
|
+
sessionId,
|
|
221
|
+
expect.objectContaining({ blockSource: 'rule-host' })
|
|
222
|
+
);
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
226
|
+
// TEST 3: When Rule Host returns undefined, Progressive Gate runs normally
|
|
227
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
228
|
+
it('should continue to Progressive Gate when Rule Host returns undefined', () => {
|
|
229
|
+
setupProfileMock();
|
|
230
|
+
|
|
231
|
+
// Mock RuleHost.evaluate to return undefined (no active implementations)
|
|
232
|
+
_mockEvaluate = vi.fn().mockReturnValue(undefined);
|
|
233
|
+
MockedRuleHost.mockImplementation(function(this: any, _stateDir: string) {
|
|
234
|
+
this.evaluate = _mockEvaluate;
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
const result = handleBeforeToolCall(makeWriteEvent() as any, { workspaceDir, sessionId } as any);
|
|
238
|
+
|
|
239
|
+
// Should pass through (no block from any gate)
|
|
240
|
+
expect(result).toBeUndefined();
|
|
241
|
+
|
|
242
|
+
// Rule Host was called
|
|
243
|
+
expect(_mockEvaluate).toHaveBeenCalled();
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
247
|
+
// TEST 4: When Rule Host throws, gate continues to Progressive Gate (D-08)
|
|
248
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
249
|
+
it('should continue to Progressive Gate when Rule Host throws (D-08)', () => {
|
|
250
|
+
setupProfileMock();
|
|
251
|
+
|
|
252
|
+
// Mock RuleHost.evaluate to throw
|
|
253
|
+
_mockEvaluate = vi.fn().mockImplementation(() => {
|
|
254
|
+
throw new Error('Host internal error');
|
|
255
|
+
});
|
|
256
|
+
MockedRuleHost.mockImplementation(function(this: any, _stateDir: string) {
|
|
257
|
+
this.evaluate = _mockEvaluate;
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
const result = handleBeforeToolCall(makeWriteEvent() as any, { workspaceDir, sessionId } as any);
|
|
261
|
+
|
|
262
|
+
// Should pass through (host error is caught, degrades to Progressive Gate)
|
|
263
|
+
// Progressive Gate will pass for this low-risk operation
|
|
264
|
+
expect(result).toBeUndefined();
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
268
|
+
// TEST 5: Block result uses blockSource='rule-host'
|
|
269
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
270
|
+
it('should use blockSource=rule-host for Rule Host blocks', () => {
|
|
271
|
+
setupProfileMock();
|
|
272
|
+
|
|
273
|
+
_mockEvaluate = vi.fn().mockReturnValue({
|
|
274
|
+
decision: 'block',
|
|
275
|
+
matched: true,
|
|
276
|
+
reason: 'Dangerous file modification',
|
|
277
|
+
});
|
|
278
|
+
MockedRuleHost.mockImplementation(function(this: any, _stateDir: string) {
|
|
279
|
+
this.evaluate = _mockEvaluate;
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
const result = handleBeforeToolCall(makeWriteEvent() as any, { workspaceDir, sessionId } as any);
|
|
283
|
+
|
|
284
|
+
expect(result?.block).toBe(true);
|
|
285
|
+
|
|
286
|
+
// Verify recordGateBlockAndReturn was called with blockSource='rule-host'
|
|
287
|
+
expect(mockEventLog.recordGateBlock).toHaveBeenCalledWith(
|
|
288
|
+
sessionId,
|
|
289
|
+
expect.objectContaining({
|
|
290
|
+
blockSource: 'rule-host',
|
|
291
|
+
reason: 'Dangerous file modification',
|
|
292
|
+
})
|
|
293
|
+
);
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
297
|
+
// TEST 5b: requireApproval result uses blockSource='rule-host' with reason
|
|
298
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
299
|
+
it('should use blockSource=rule-host with approval prefix for requireApproval', () => {
|
|
300
|
+
setupProfileMock();
|
|
301
|
+
|
|
302
|
+
_mockEvaluate = vi.fn().mockReturnValue({
|
|
303
|
+
decision: 'requireApproval',
|
|
304
|
+
matched: true,
|
|
305
|
+
reason: 'High-risk path requires approval',
|
|
306
|
+
});
|
|
307
|
+
MockedRuleHost.mockImplementation(function(this: any, _stateDir: string) {
|
|
308
|
+
this.evaluate = _mockEvaluate;
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
const result = handleBeforeToolCall(makeWriteEvent() as any, { workspaceDir, sessionId } as any);
|
|
312
|
+
|
|
313
|
+
expect(result?.block).toBe(true);
|
|
314
|
+
expect(mockEventLog.recordGateBlock).toHaveBeenCalledWith(
|
|
315
|
+
sessionId,
|
|
316
|
+
expect.objectContaining({
|
|
317
|
+
blockSource: 'rule-host',
|
|
318
|
+
reason: expect.stringContaining('[Rule Host] Approval required'),
|
|
319
|
+
})
|
|
320
|
+
);
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
324
|
+
// TEST 6: Existing gate flow still works when no active implementations exist
|
|
325
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
326
|
+
it('should allow operation through existing gate flow with no active implementations', () => {
|
|
327
|
+
setupProfileMock();
|
|
328
|
+
|
|
329
|
+
// Default mock: RuleHost.evaluate returns undefined (already set in beforeEach)
|
|
330
|
+
const result = handleBeforeToolCall(makeWriteEvent() as any, { workspaceDir, sessionId } as any);
|
|
331
|
+
|
|
332
|
+
// Should pass all gates — no block
|
|
333
|
+
expect(result).toBeUndefined();
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
it('should block with GFI even when Rule Host would allow', () => {
|
|
337
|
+
// High GFI triggers GFI block
|
|
338
|
+
vi.mocked(sessionTrackerModule.getSession).mockReturnValue({ currentGfi: 85 } as any);
|
|
339
|
+
setupProfileMock();
|
|
340
|
+
|
|
341
|
+
// Even if RuleHost would allow, GFI blocks first
|
|
342
|
+
const result = handleBeforeToolCall(makeWriteEvent() as any, { workspaceDir, sessionId } as any);
|
|
343
|
+
|
|
344
|
+
expect(result?.block).toBe(true);
|
|
345
|
+
expect(result?.blockReason).toContain('GFI');
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
it('should allow edit when oldText matches (full pipeline with Rule Host)', () => {
|
|
349
|
+
const fileContent = 'const x = 1;\n';
|
|
350
|
+
const editEvent = {
|
|
351
|
+
toolName: 'edit',
|
|
352
|
+
params: {
|
|
353
|
+
file_path: 'src/example.ts',
|
|
354
|
+
oldText: 'const x = 1;',
|
|
355
|
+
newText: 'const x = 2;',
|
|
356
|
+
},
|
|
357
|
+
};
|
|
358
|
+
|
|
359
|
+
setupProfileMock();
|
|
360
|
+
|
|
361
|
+
vi.mocked(fs.readFileSync).mockImplementation((p: any) => {
|
|
362
|
+
if (typeof p === 'string' && p.includes('PROFILE.json')) {
|
|
363
|
+
return JSON.stringify({
|
|
364
|
+
risk_paths: [],
|
|
365
|
+
progressive_gate: { enabled: true },
|
|
366
|
+
edit_verification: { enabled: true },
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
if (typeof p === 'string' && p.includes('example.ts')) {
|
|
370
|
+
return fileContent;
|
|
371
|
+
}
|
|
372
|
+
return '';
|
|
373
|
+
});
|
|
374
|
+
vi.mocked(fs.statSync).mockReturnValue({ size: 1000 } as any);
|
|
375
|
+
vi.mocked(fs.existsSync).mockImplementation((p: any) => {
|
|
376
|
+
if (typeof p === 'string' && p.includes('PROFILE.json')) return true;
|
|
377
|
+
if (typeof p === 'string' && p.includes('example.ts')) return true;
|
|
378
|
+
return false;
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
const result = handleBeforeToolCall(editEvent as any, { workspaceDir, sessionId } as any);
|
|
382
|
+
|
|
383
|
+
expect(result).toBeUndefined();
|
|
384
|
+
});
|
|
385
|
+
});
|