principles-disciple 1.83.0 → 1.83.1
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/hooks/prompt.ts +4 -5
- package/src/index.ts +5 -4
- package/tests/core/workspace-guidance-migrator.test.ts +49 -2
- package/tests/evolution-worker-slimming.test.ts +323 -0
- package/tests/integration/mvp-surface-registry-guard.test.ts +12 -3
package/openclaw.plugin.json
CHANGED
package/package.json
CHANGED
package/src/hooks/prompt.ts
CHANGED
|
@@ -192,7 +192,7 @@ export function loadContextInjectionConfig(workspaceDir: string): ContextInjecti
|
|
|
192
192
|
const raw = cachedReadFile(profilePath);
|
|
193
193
|
if (raw) {
|
|
194
194
|
const profile = JSON.parse(raw);
|
|
195
|
-
if (profile.contextInjection) {
|
|
195
|
+
if (profile && typeof profile === 'object' && profile.contextInjection && typeof profile.contextInjection === 'object') {
|
|
196
196
|
const contextInjection = profile.contextInjection as Partial<ContextInjectionConfig>;
|
|
197
197
|
return {
|
|
198
198
|
...defaultContextConfig,
|
|
@@ -360,6 +360,9 @@ export async function handleBeforePromptBuild(
|
|
|
360
360
|
}
|
|
361
361
|
|
|
362
362
|
// ──── 1. prependSystemContext: Minimal Agent Identity ────
|
|
363
|
+
// EvolutionWorker-era INTERNAL SYSTEM LAYOUT removed per PRI-294.
|
|
364
|
+
// The EVOLUTION_WORKER PathResolver key and system layout reference are
|
|
365
|
+
// not MVP-Core; agents discover what they need via tool calls.
|
|
363
366
|
prependSystemContext = `## 【AGENT IDENTITY】
|
|
364
367
|
|
|
365
368
|
You are a **self-evolving AI agent** powered by Principles Disciple.
|
|
@@ -377,10 +380,6 @@ You are a **self-evolving AI agent** powered by Principles Disciple.
|
|
|
377
380
|
- Use the current session for the normal user reply.
|
|
378
381
|
- Use sessions_send for cross-session messaging.
|
|
379
382
|
- Use agents_list / sessions_list for peer-agent or peer-session orchestration.
|
|
380
|
-
|
|
381
|
-
## 🔧 INTERNAL SYSTEM LAYOUT
|
|
382
|
-
- Your core plugin logic is rooted at: ${PathResolver.getExtensionRoot() || 'EXTENSION_ROOT (unresolved)'}
|
|
383
|
-
- If you need self-inspection, prioritize the worker entry pointed by PathResolver key: EVOLUTION_WORKER
|
|
384
383
|
`;
|
|
385
384
|
|
|
386
385
|
// ──── 2. Empathy Observer Spawn (async sidecar)
|
package/src/index.ts
CHANGED
|
@@ -563,11 +563,12 @@ const plugin = {
|
|
|
563
563
|
return handleAfterCompaction(event, { ...ctx, workspaceDir });
|
|
564
564
|
}));
|
|
565
565
|
|
|
566
|
-
// ── Service
|
|
566
|
+
// ── Service Registration (surface-guarded) ──
|
|
567
|
+
// PRI-294: EvolutionWorker service registration removed — it starts via
|
|
568
|
+
// before_prompt_build hook gate, not via api.registerService. The surface
|
|
569
|
+
// guard already prevents registration when disabled (enabledByDefault=false).
|
|
570
|
+
// Dead pre-assignment of EvolutionWorkerService.api removed.
|
|
567
571
|
try {
|
|
568
|
-
EvolutionWorkerService.api = api;
|
|
569
|
-
const guardedEvolutionWorker = guardService('service:evolution-worker', EvolutionWorkerService, api.logger);
|
|
570
|
-
if (guardedEvolutionWorker) api.registerService(guardedEvolutionWorker);
|
|
571
572
|
const guardedCorrectionObserver = guardService('service:correction-observer', CorrectionObserverService, api.logger);
|
|
572
573
|
if (guardedCorrectionObserver) api.registerService(guardedCorrectionObserver);
|
|
573
574
|
const guardedTrajectory = guardService('service:trajectory', TrajectoryService, api.logger);
|
|
@@ -13,6 +13,15 @@ const mockFs = {
|
|
|
13
13
|
|
|
14
14
|
vi.mock('fs', () => mockFs);
|
|
15
15
|
|
|
16
|
+
// Mock heavy core module to avoid slow re-imports and to control staleness detection
|
|
17
|
+
const mockMigrateWorkspaceGuidance = vi.fn<(content: string, relativePath: string) => { changed: boolean; migrated: string }>();
|
|
18
|
+
const mockContainsStalePlanMdGuidance = vi.fn<(content: string, relativePath: string) => boolean>();
|
|
19
|
+
|
|
20
|
+
vi.mock('@principles/core/runtime-v2', () => ({
|
|
21
|
+
migrateWorkspaceGuidance: (...args: unknown[]) => mockMigrateWorkspaceGuidance(...(args as [string, string])),
|
|
22
|
+
containsStalePlanMdGuidance: (...args: unknown[]) => mockContainsStalePlanMdGuidance(...(args as [string, string])),
|
|
23
|
+
}));
|
|
24
|
+
|
|
16
25
|
const WORKSPACE_GUIDANCE_MIGRATOR_PATH = '../../src/core/workspace-guidance-migrator.js';
|
|
17
26
|
|
|
18
27
|
describe('workspace-guidance-migrator', () => {
|
|
@@ -41,6 +50,13 @@ describe('workspace-guidance-migrator', () => {
|
|
|
41
50
|
mockFs.writeFileSync.mockReturnValue(undefined);
|
|
42
51
|
mockFs.readdirSync.mockReturnValue([]);
|
|
43
52
|
|
|
53
|
+
// Default: content is NOT stale
|
|
54
|
+
mockContainsStalePlanMdGuidance.mockReturnValue(false);
|
|
55
|
+
mockMigrateWorkspaceGuidance.mockImplementation((content: string) => ({
|
|
56
|
+
changed: false,
|
|
57
|
+
migrated: content,
|
|
58
|
+
}));
|
|
59
|
+
|
|
44
60
|
const module = await import(WORKSPACE_GUIDANCE_MIGRATOR_PATH);
|
|
45
61
|
migrateStaleWorkspaceGuidance = module.migrateStaleWorkspaceGuidance;
|
|
46
62
|
});
|
|
@@ -63,6 +79,7 @@ describe('workspace-guidance-migrator', () => {
|
|
|
63
79
|
it('skips files with no stale guidance', () => {
|
|
64
80
|
mockFs.existsSync.mockReturnValue(true);
|
|
65
81
|
mockFs.readFileSync.mockReturnValue('# Clean AGENTS.md\nNo stale references here.');
|
|
82
|
+
// Default: containsStalePlanMdGuidance returns false
|
|
66
83
|
|
|
67
84
|
const result = migrateStaleWorkspaceGuidance(mockApi, '/workspace');
|
|
68
85
|
|
|
@@ -76,6 +93,14 @@ describe('workspace-guidance-migrator', () => {
|
|
|
76
93
|
mockFs.readFileSync.mockReturnValue(
|
|
77
94
|
'# Agent Instructions\nPhysical interception ensures safety.',
|
|
78
95
|
);
|
|
96
|
+
// First call (AGENTS.md) is stale, second (MEMORY.md) is not
|
|
97
|
+
mockContainsStalePlanMdGuidance
|
|
98
|
+
.mockReturnValueOnce(true)
|
|
99
|
+
.mockReturnValueOnce(false);
|
|
100
|
+
mockMigrateWorkspaceGuidance.mockImplementation((content: string) => ({
|
|
101
|
+
changed: true,
|
|
102
|
+
migrated: content.replace('Physical interception', 'MIGRATED'),
|
|
103
|
+
}));
|
|
79
104
|
|
|
80
105
|
const result = migrateStaleWorkspaceGuidance(mockApi, '/workspace');
|
|
81
106
|
|
|
@@ -88,6 +113,11 @@ describe('workspace-guidance-migrator', () => {
|
|
|
88
113
|
mockFs.readFileSync.mockReturnValue(
|
|
89
114
|
'# Agent Instructions\nPhysical interception ensures safety.',
|
|
90
115
|
);
|
|
116
|
+
mockContainsStalePlanMdGuidance.mockReturnValue(true);
|
|
117
|
+
mockMigrateWorkspaceGuidance.mockImplementation((content: string) => ({
|
|
118
|
+
changed: true,
|
|
119
|
+
migrated: content.replace('Physical interception', 'MIGRATED'),
|
|
120
|
+
}));
|
|
91
121
|
|
|
92
122
|
migrateStaleWorkspaceGuidance(mockApi, '/workspace');
|
|
93
123
|
|
|
@@ -102,6 +132,11 @@ describe('workspace-guidance-migrator', () => {
|
|
|
102
132
|
mockFs.readFileSync.mockReturnValue(
|
|
103
133
|
'# Agent Instructions\nPhysical interception ensures safety.',
|
|
104
134
|
);
|
|
135
|
+
mockContainsStalePlanMdGuidance.mockReturnValue(true);
|
|
136
|
+
mockMigrateWorkspaceGuidance.mockImplementation((content: string) => ({
|
|
137
|
+
changed: true,
|
|
138
|
+
migrated: content.replace('Physical interception', 'MIGRATED'),
|
|
139
|
+
}));
|
|
105
140
|
|
|
106
141
|
migrateStaleWorkspaceGuidance(mockApi, '/workspace');
|
|
107
142
|
|
|
@@ -126,6 +161,11 @@ describe('workspace-guidance-migrator', () => {
|
|
|
126
161
|
const originalContent = '# Agent Instructions\nPhysical interception ensures safety.';
|
|
127
162
|
mockFs.existsSync.mockReturnValue(true);
|
|
128
163
|
mockFs.readFileSync.mockReturnValue(originalContent);
|
|
164
|
+
mockContainsStalePlanMdGuidance.mockReturnValue(true);
|
|
165
|
+
mockMigrateWorkspaceGuidance.mockImplementation((content: string) => ({
|
|
166
|
+
changed: true,
|
|
167
|
+
migrated: content.replace('Physical interception', 'MIGRATED'),
|
|
168
|
+
}));
|
|
129
169
|
|
|
130
170
|
let callCount = 0;
|
|
131
171
|
mockFs.writeFileSync.mockImplementation((path: string, content: string) => {
|
|
@@ -154,8 +194,9 @@ describe('workspace-guidance-migrator', () => {
|
|
|
154
194
|
});
|
|
155
195
|
|
|
156
196
|
it('discovers skill files in .principles/skills directory', () => {
|
|
197
|
+
const skillsPattern = path.join('.principles', 'skills');
|
|
157
198
|
mockFs.existsSync.mockImplementation((p: string) => {
|
|
158
|
-
if (String(p).includes(
|
|
199
|
+
if (String(p).includes(skillsPattern)) return true;
|
|
159
200
|
return false;
|
|
160
201
|
});
|
|
161
202
|
mockFs.readdirSync.mockReturnValue([
|
|
@@ -165,6 +206,11 @@ describe('workspace-guidance-migrator', () => {
|
|
|
165
206
|
mockFs.readFileSync.mockReturnValue(
|
|
166
207
|
'Ensure `PLAN.md` contains `## Target Files` heading.',
|
|
167
208
|
);
|
|
209
|
+
mockContainsStalePlanMdGuidance.mockReturnValue(true);
|
|
210
|
+
mockMigrateWorkspaceGuidance.mockImplementation((content: string) => ({
|
|
211
|
+
changed: true,
|
|
212
|
+
migrated: content.replace('## Target Files', '## Targets'),
|
|
213
|
+
}));
|
|
168
214
|
|
|
169
215
|
const result = migrateStaleWorkspaceGuidance(mockApi, '/workspace');
|
|
170
216
|
|
|
@@ -182,8 +228,9 @@ describe('workspace-guidance-migrator', () => {
|
|
|
182
228
|
});
|
|
183
229
|
|
|
184
230
|
it('handles skills directory read error gracefully', () => {
|
|
231
|
+
const skillsPattern = path.join('.principles', 'skills');
|
|
185
232
|
mockFs.existsSync.mockImplementation((p: string) => {
|
|
186
|
-
if (String(p).includes(
|
|
233
|
+
if (String(p).includes(skillsPattern)) return true;
|
|
187
234
|
return false;
|
|
188
235
|
});
|
|
189
236
|
mockFs.readdirSync.mockImplementation(() => {
|
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PRI-294: Split EvolutionWorker-era services into MVP hook adapters.
|
|
3
|
+
*
|
|
4
|
+
* Tests prove:
|
|
5
|
+
* 1. All guardHook/guardService surface IDs in index.ts exist in the registry.
|
|
6
|
+
* 2. CorrectionObserver starts independently of EvolutionWorker.
|
|
7
|
+
* 3. Core hooks are enabled; non-core hooks are disabled by default.
|
|
8
|
+
* 4. EvolutionWorker-era prompt injection has been removed.
|
|
9
|
+
* 5. EvolutionWorker service is NOT registered via api.registerService.
|
|
10
|
+
*
|
|
11
|
+
* ERR-024: dead validators/services must not appear as protection if not wired.
|
|
12
|
+
* ERR-025: tests must exercise production hook/service paths.
|
|
13
|
+
* ERR-027: registry/docs must match actual runtime surface.
|
|
14
|
+
*/
|
|
15
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
16
|
+
import * as fs from 'fs';
|
|
17
|
+
import * as path from 'path';
|
|
18
|
+
import * as os from 'os';
|
|
19
|
+
import * as yaml from 'js-yaml';
|
|
20
|
+
import { PLUGIN_SURFACE_REGISTRY, computeEffectiveFlags, DEFAULT_FEATURE_FLAGS } from '@principles/core/runtime-v2';
|
|
21
|
+
|
|
22
|
+
// ── Helpers ──
|
|
23
|
+
|
|
24
|
+
function createTempWorkspace(): string {
|
|
25
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'pd-slimming-'));
|
|
26
|
+
fs.mkdirSync(path.join(dir, '.pd'), { recursive: true });
|
|
27
|
+
fs.mkdirSync(path.join(dir, '.state'), { recursive: true });
|
|
28
|
+
return dir;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function writeFeatureFlags(workspaceDir: string, flags: Record<string, unknown>): void {
|
|
32
|
+
const configPath = path.join(workspaceDir, '.pd', 'feature-flags.yaml');
|
|
33
|
+
const content = yaml.dump(flags, { schema: yaml.JSON_SCHEMA });
|
|
34
|
+
fs.writeFileSync(configPath, content, 'utf8');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function createMockLogger() {
|
|
38
|
+
return {
|
|
39
|
+
info: vi.fn(),
|
|
40
|
+
warn: vi.fn(),
|
|
41
|
+
error: vi.fn(),
|
|
42
|
+
debug: vi.fn(),
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Mock dependencies for service start tests
|
|
47
|
+
vi.mock('../src/core/dictionary-service.js', () => ({
|
|
48
|
+
DictionaryService: { get: vi.fn(() => ({ flush: vi.fn() })) },
|
|
49
|
+
}));
|
|
50
|
+
|
|
51
|
+
vi.mock('../src/core/session-tracker.js', () => ({
|
|
52
|
+
initPersistence: vi.fn(),
|
|
53
|
+
flushAllSessions: vi.fn(),
|
|
54
|
+
listSessions: vi.fn(() => []),
|
|
55
|
+
}));
|
|
56
|
+
|
|
57
|
+
vi.mock('../src/core/workspace-context.js', () => {
|
|
58
|
+
const mockCtx = {
|
|
59
|
+
stateDir: '',
|
|
60
|
+
workspaceDir: '',
|
|
61
|
+
config: { get: vi.fn() },
|
|
62
|
+
eventLog: { recordHookExecution: vi.fn() },
|
|
63
|
+
dictionary: { flush: vi.fn() },
|
|
64
|
+
resolve: vi.fn((key: string) => `/mock/${key}`),
|
|
65
|
+
trajectory: null,
|
|
66
|
+
};
|
|
67
|
+
return {
|
|
68
|
+
WorkspaceContext: {
|
|
69
|
+
fromHookContext: vi.fn(() => mockCtx),
|
|
70
|
+
clearCache: vi.fn(),
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
// Import after mocks
|
|
76
|
+
import { loadFeatureFlagFromWorkspace, shouldStartEvolutionWorker, shouldStartCorrectionObserver } from '../src/index.js';
|
|
77
|
+
import { CorrectionObserverService } from '../src/service/correction-observer-service.js';
|
|
78
|
+
|
|
79
|
+
// ── 1. Surface Registry Coverage Audit ──
|
|
80
|
+
|
|
81
|
+
describe('PRI-294: Surface registry coverage audit', () => {
|
|
82
|
+
// Surface IDs actually used in index.ts guardHook/guardService calls
|
|
83
|
+
const USED_SURFACE_IDS = [
|
|
84
|
+
'hook:before_prompt_build',
|
|
85
|
+
'hook:before_tool_call',
|
|
86
|
+
'hook:after_tool_call',
|
|
87
|
+
'hook:llm_output',
|
|
88
|
+
'hook:after_tool_call.trajectory',
|
|
89
|
+
'hook:llm_output.trajectory',
|
|
90
|
+
'hook:subagent_spawning',
|
|
91
|
+
'hook:subagent_ended',
|
|
92
|
+
'hook:before_reset',
|
|
93
|
+
'hook:before_compaction',
|
|
94
|
+
'hook:after_compaction',
|
|
95
|
+
// Services registered via guardService
|
|
96
|
+
'service:correction-observer',
|
|
97
|
+
'service:trajectory',
|
|
98
|
+
'service:pd-task',
|
|
99
|
+
'service:central-sync',
|
|
100
|
+
];
|
|
101
|
+
|
|
102
|
+
for (const surfaceId of USED_SURFACE_IDS) {
|
|
103
|
+
it(`used surface '${surfaceId}' exists in registry`, () => {
|
|
104
|
+
const entry = PLUGIN_SURFACE_REGISTRY.find(s => s.id === surfaceId);
|
|
105
|
+
expect(entry).toBeDefined();
|
|
106
|
+
expect(entry!.id).toBe(surfaceId);
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
it('all used surface IDs are accounted for (no orphan surfaces)', () => {
|
|
111
|
+
const registeredIds = new Set(PLUGIN_SURFACE_REGISTRY.map(s => s.id));
|
|
112
|
+
const usedButNotRegistered = USED_SURFACE_IDS.filter(id => !registeredIds.has(id));
|
|
113
|
+
expect(usedButNotRegistered).toEqual([]);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('registry has no unexpected surfaces beyond what index.ts uses', () => {
|
|
117
|
+
const usedSet = new Set(USED_SURFACE_IDS);
|
|
118
|
+
// These are used but not directly via guardHook/guardService in the main hook path
|
|
119
|
+
const additionallyRegistered = [
|
|
120
|
+
'service:evolution-worker', // Previously registered, now removed per PRI-294
|
|
121
|
+
'startup:workspace-init',
|
|
122
|
+
'startup:evolution-worker',
|
|
123
|
+
'startup:correction-observer',
|
|
124
|
+
];
|
|
125
|
+
const allowedIds = new Set([...usedSet, ...additionallyRegistered]);
|
|
126
|
+
const unaccounted = PLUGIN_SURFACE_REGISTRY
|
|
127
|
+
.map(s => s.id)
|
|
128
|
+
.filter(id => !allowedIds.has(id));
|
|
129
|
+
// Unaccounted surfaces should be empty — every registry entry must trace to a caller
|
|
130
|
+
expect(unaccounted).toEqual([]);
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
// ── 2. CorrectionObserver Independence ──
|
|
135
|
+
|
|
136
|
+
describe('PRI-294: CorrectionObserver independence from EvolutionWorker', () => {
|
|
137
|
+
let workspaceDir: string;
|
|
138
|
+
|
|
139
|
+
beforeEach(() => {
|
|
140
|
+
workspaceDir = createTempWorkspace();
|
|
141
|
+
CorrectionObserverService.stop?.({ logger: createMockLogger() });
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
afterEach(() => {
|
|
145
|
+
CorrectionObserverService.stop?.({ logger: createMockLogger() });
|
|
146
|
+
try {
|
|
147
|
+
fs.rmSync(workspaceDir, { recursive: true, force: true });
|
|
148
|
+
} catch {
|
|
149
|
+
// best-effort
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it('CorrectionObserver starts when EvolutionWorker is disabled (default)', () => {
|
|
154
|
+
const logger = createMockLogger();
|
|
155
|
+
|
|
156
|
+
// Verify EvolutionWorker is disabled
|
|
157
|
+
const ewGate = shouldStartEvolutionWorker(workspaceDir, logger);
|
|
158
|
+
expect(ewGate.shouldStart).toBe(false);
|
|
159
|
+
|
|
160
|
+
// Verify CorrectionObserver is enabled
|
|
161
|
+
const coGate = shouldStartCorrectionObserver(workspaceDir, logger);
|
|
162
|
+
expect(coGate.shouldStart).toBe(true);
|
|
163
|
+
expect(coGate.disabledInfo).toBeNull();
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('CorrectionObserver starts when EvolutionWorker is explicitly enabled', () => {
|
|
167
|
+
writeFeatureFlags(workspaceDir, {
|
|
168
|
+
evolution_worker: { enabled: true },
|
|
169
|
+
});
|
|
170
|
+
const logger = createMockLogger();
|
|
171
|
+
|
|
172
|
+
const ewGate = shouldStartEvolutionWorker(workspaceDir, logger);
|
|
173
|
+
expect(ewGate.shouldStart).toBe(true);
|
|
174
|
+
|
|
175
|
+
const coGate = shouldStartCorrectionObserver(workspaceDir, logger);
|
|
176
|
+
expect(coGate.shouldStart).toBe(true);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it('CorrectionObserver can be independently disabled', () => {
|
|
180
|
+
writeFeatureFlags(workspaceDir, {
|
|
181
|
+
correction_observer: { enabled: false },
|
|
182
|
+
});
|
|
183
|
+
const logger = createMockLogger();
|
|
184
|
+
|
|
185
|
+
const coGate = shouldStartCorrectionObserver(workspaceDir, logger);
|
|
186
|
+
expect(coGate.shouldStart).toBe(false);
|
|
187
|
+
expect(coGate.disabledInfo).not.toBeNull();
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
// ── 3. Core vs Non-Core Surface Defaults ──
|
|
192
|
+
|
|
193
|
+
describe('PRI-294: MVP core hooks enabled, non-core disabled', () => {
|
|
194
|
+
const CORE_HOOKS = [
|
|
195
|
+
'hook:before_prompt_build',
|
|
196
|
+
'hook:before_tool_call',
|
|
197
|
+
'hook:after_tool_call',
|
|
198
|
+
'hook:llm_output',
|
|
199
|
+
];
|
|
200
|
+
|
|
201
|
+
const QUIET_HOOKS = [
|
|
202
|
+
'hook:after_tool_call.trajectory',
|
|
203
|
+
'hook:llm_output.trajectory',
|
|
204
|
+
'hook:subagent_spawning',
|
|
205
|
+
'hook:subagent_ended',
|
|
206
|
+
'hook:before_reset',
|
|
207
|
+
'hook:before_compaction',
|
|
208
|
+
'hook:after_compaction',
|
|
209
|
+
];
|
|
210
|
+
|
|
211
|
+
for (const surfaceId of CORE_HOOKS) {
|
|
212
|
+
it(`core hook '${surfaceId}' is enabled by default`, () => {
|
|
213
|
+
const entry = PLUGIN_SURFACE_REGISTRY.find(s => s.id === surfaceId);
|
|
214
|
+
expect(entry).toBeDefined();
|
|
215
|
+
expect(entry!.category).toBe('core');
|
|
216
|
+
expect(entry!.enabledByDefault).toBe(true);
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
for (const surfaceId of QUIET_HOOKS) {
|
|
221
|
+
it(`quiet hook '${surfaceId}' is disabled by default`, () => {
|
|
222
|
+
const entry = PLUGIN_SURFACE_REGISTRY.find(s => s.id === surfaceId);
|
|
223
|
+
expect(entry).toBeDefined();
|
|
224
|
+
expect(entry!.category).toBe('quiet');
|
|
225
|
+
expect(entry!.enabledByDefault).toBe(false);
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
it('CorrectionObserver service surface is core and enabled', () => {
|
|
230
|
+
const entry = PLUGIN_SURFACE_REGISTRY.find(s => s.id === 'service:correction-observer');
|
|
231
|
+
expect(entry).toBeDefined();
|
|
232
|
+
expect(entry!.category).toBe('core');
|
|
233
|
+
expect(entry!.enabledByDefault).toBe(true);
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
it('EvolutionWorker service surface is quiet and disabled', () => {
|
|
237
|
+
const entry = PLUGIN_SURFACE_REGISTRY.find(s => s.id === 'service:evolution-worker');
|
|
238
|
+
expect(entry).toBeDefined();
|
|
239
|
+
expect(entry!.category).toBe('quiet');
|
|
240
|
+
expect(entry!.enabledByDefault).toBe(false);
|
|
241
|
+
expect(entry!.disabledReason).toBeDefined();
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it('all disabled surfaces have disabledReason (ERR-002 observability)', () => {
|
|
245
|
+
const disabledWithoutReason = PLUGIN_SURFACE_REGISTRY.filter(
|
|
246
|
+
s => !s.enabledByDefault && s.category !== 'core' && !s.disabledReason,
|
|
247
|
+
);
|
|
248
|
+
expect(disabledWithoutReason).toEqual([]);
|
|
249
|
+
});
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
// ── 4. EvolutionWorker-era prompt injection removed ──
|
|
253
|
+
|
|
254
|
+
describe('PRI-294: EvolutionWorker-era prompt injection removed', () => {
|
|
255
|
+
it('prompt.ts does not inject EVOLUTION_WORKER key into agent prompt', async () => {
|
|
256
|
+
const promptPath = path.resolve(__dirname, '../src/hooks/prompt.ts');
|
|
257
|
+
const src = fs.readFileSync(promptPath, 'utf-8');
|
|
258
|
+
// Check that the EVOLUTION_WORKER PathResolver key is not injected into
|
|
259
|
+
// the prependSystemContext template string (the runtime prompt).
|
|
260
|
+
// Comments referencing EVOLUTION_WORKER for documentation are fine.
|
|
261
|
+
// Match: inside template literal interpolation that resolves EVOLUTION_WORKER
|
|
262
|
+
expect(src).not.toMatch(/\$\{.*EVOLUTION_WORKER.*\}/);
|
|
263
|
+
// Also check no template literal contains "PathResolver key: EVOLUTION_WORKER"
|
|
264
|
+
expect(src).not.toContain('PathResolver key: EVOLUTION_WORKER');
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it('prompt.ts does not inject INTERNAL SYSTEM LAYOUT section into prompt', async () => {
|
|
268
|
+
const promptPath = path.resolve(__dirname, '../src/hooks/prompt.ts');
|
|
269
|
+
const src = fs.readFileSync(promptPath, 'utf-8');
|
|
270
|
+
// The INTERNAL SYSTEM LAYOUT section was EvolutionWorker-era injection.
|
|
271
|
+
// Verify the actual rendered section header is gone — only comments should remain.
|
|
272
|
+
// Remove all single-line comments, then check no INTERNAL SYSTEM LAYOUT in code.
|
|
273
|
+
const withoutComments = src.replace(/\/\/.*$/gm, '');
|
|
274
|
+
expect(withoutComments).not.toContain('INTERNAL SYSTEM LAYOUT');
|
|
275
|
+
});
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
// ── 5. EvolutionWorker NOT registered via api.registerService ──
|
|
279
|
+
|
|
280
|
+
describe('PRI-294: EvolutionWorker not registered as service', () => {
|
|
281
|
+
it('index.ts does not register EvolutionWorker via api.registerService', async () => {
|
|
282
|
+
const indexPath = path.resolve(__dirname, '../src/index.ts');
|
|
283
|
+
const src = fs.readFileSync(indexPath, 'utf-8');
|
|
284
|
+
// Should NOT have guardService('service:evolution-worker', ...)
|
|
285
|
+
expect(src).not.toMatch(/guardService\(\s*['"]service:evolution-worker['"]/);
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
it('index.ts does not pre-assign EvolutionWorkerService.api outside gate', async () => {
|
|
289
|
+
const indexPath = path.resolve(__dirname, '../src/index.ts');
|
|
290
|
+
const src = fs.readFileSync(indexPath, 'utf-8');
|
|
291
|
+
// The only EvolutionWorkerService.api assignment should be inside the gate
|
|
292
|
+
const apiAssignments = src.match(/EvolutionWorkerService\.api\s*=\s*api/g) ?? [];
|
|
293
|
+
// Should be exactly one: inside the shouldStartEvolutionWorker gate
|
|
294
|
+
expect(apiAssignments.length).toBe(1);
|
|
295
|
+
});
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
// ── 6. Feature flag registry completeness ──
|
|
299
|
+
|
|
300
|
+
describe('PRI-294: Feature flag registry matches surface registry', () => {
|
|
301
|
+
it('evolution_worker flag exists in DEFAULT_FEATURE_FLAGS as quiet+disabled', () => {
|
|
302
|
+
const flag = DEFAULT_FEATURE_FLAGS.find(f => f.id === 'evolution_worker');
|
|
303
|
+
expect(flag).toBeDefined();
|
|
304
|
+
expect(flag!.category).toBe('quiet');
|
|
305
|
+
expect(flag!.enabled).toBe(false);
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
it('correction_observer flag exists in DEFAULT_FEATURE_FLAGS', () => {
|
|
309
|
+
const flag = DEFAULT_FEATURE_FLAGS.find(f => f.id === 'correction_observer');
|
|
310
|
+
expect(flag).toBeDefined();
|
|
311
|
+
expect(flag!.enabled).toBe(true);
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
it('nocturnal and idle_trigger are gone flags', () => {
|
|
315
|
+
const nocturnal = DEFAULT_FEATURE_FLAGS.find(f => f.id === 'nocturnal');
|
|
316
|
+
expect(nocturnal).toBeDefined();
|
|
317
|
+
expect(nocturnal!.category).toBe('gone');
|
|
318
|
+
|
|
319
|
+
const idle = DEFAULT_FEATURE_FLAGS.find(f => f.id === 'idle_trigger');
|
|
320
|
+
expect(idle).toBeDefined();
|
|
321
|
+
expect(idle!.category).toBe('gone');
|
|
322
|
+
});
|
|
323
|
+
});
|
|
@@ -200,12 +200,21 @@ describe('MVP Surface Registry Guard (PRI-289)', () => {
|
|
|
200
200
|
expect(unclassified).toEqual([]);
|
|
201
201
|
});
|
|
202
202
|
|
|
203
|
-
it('
|
|
203
|
+
it('registered service IDs match expected MVP set', () => {
|
|
204
|
+
// Quiet/disabled services (e.g. evolution-worker per PRI-294) exist in the
|
|
205
|
+
// registry but are NOT registered via guardService in index.ts.
|
|
204
206
|
const source = read('packages/openclaw-plugin/src/index.ts');
|
|
205
207
|
const registrations = extractServiceRegistrations(source);
|
|
206
208
|
|
|
207
|
-
const
|
|
208
|
-
|
|
209
|
+
const registeredIds = registrations.map(r => r.surfaceId).sort();
|
|
210
|
+
const expectedIds = [
|
|
211
|
+
'service:central-sync',
|
|
212
|
+
'service:correction-observer',
|
|
213
|
+
'service:pd-task',
|
|
214
|
+
'service:trajectory',
|
|
215
|
+
].sort();
|
|
216
|
+
|
|
217
|
+
expect(registeredIds).toEqual(expectedIds);
|
|
209
218
|
});
|
|
210
219
|
});
|
|
211
220
|
|