principles-disciple 1.83.0 → 1.84.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/hooks/prompt.ts +4 -5
- package/src/index.ts +62 -43
- package/src/utils/workspace-resolver.ts +250 -2
- package/tests/core/workspace-guidance-migrator.test.ts +49 -2
- package/tests/evolution-worker-slimming.test.ts +323 -0
- package/tests/hook-workspace-nextaction-contract.test.ts +32 -24
- package/tests/integration/mvp-surface-registry-guard.test.ts +12 -3
- package/tests/utils/hook-workspace-resolver.test.ts +347 -0
|
@@ -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
|
+
});
|
|
@@ -7,36 +7,44 @@ const INDEX_TS = fs.readFileSync(
|
|
|
7
7
|
'utf-8',
|
|
8
8
|
);
|
|
9
9
|
|
|
10
|
+
const WORKSPACE_RESOLVER_TS = fs.readFileSync(
|
|
11
|
+
path.resolve(__dirname, '../src/utils/workspace-resolver.ts'),
|
|
12
|
+
'utf-8',
|
|
13
|
+
);
|
|
14
|
+
|
|
10
15
|
describe('Hook workspace resolution NextAction contract', () => {
|
|
11
|
-
|
|
12
|
-
/
|
|
13
|
-
|
|
14
|
-
];
|
|
15
|
-
|
|
16
|
-
it('does not claim PD_WORKSPACE_DIR env var as recovery in NextAction', () => {
|
|
17
|
-
const matches = INDEX_TS.match(/NextAction:[^`]*PD_WORKSPACE_DIR/g);
|
|
18
|
-
expect(matches).toBeNull();
|
|
16
|
+
it('no stale HOOK_WORKSPACE_RESOLUTION_NEXT_ACTION constant remains', () => {
|
|
17
|
+
const constantMatch = INDEX_TS.match(/HOOK_WORKSPACE_RESOLUTION_NEXT_ACTION/);
|
|
18
|
+
expect(constantMatch).toBeNull();
|
|
19
19
|
});
|
|
20
20
|
|
|
21
|
-
it('
|
|
22
|
-
const
|
|
23
|
-
|
|
21
|
+
it('resolveHookWorkspaceDir failure result includes PD canonical config in nextAction', () => {
|
|
22
|
+
const nextActionMatch = WORKSPACE_RESOLVER_TS.match(
|
|
23
|
+
/nextAction:\s*'([^']+)'/s,
|
|
24
|
+
);
|
|
25
|
+
expect(nextActionMatch).not.toBeNull();
|
|
26
|
+
const nextAction = nextActionMatch![1];
|
|
27
|
+
expect(nextAction).toContain('PD_WORKSPACE_DIR');
|
|
28
|
+
expect(nextAction).toContain('principles-disciple.json');
|
|
24
29
|
});
|
|
25
30
|
|
|
26
|
-
it('all hook failure
|
|
27
|
-
const
|
|
28
|
-
expect(
|
|
29
|
-
expect(
|
|
31
|
+
it('all hook failure paths use resolveHookWorkspaceDir with structured nextAction', () => {
|
|
32
|
+
const hookUsages = INDEX_TS.match(/resolveHookWorkspaceDir\(/g);
|
|
33
|
+
expect(hookUsages).not.toBeNull();
|
|
34
|
+
expect(hookUsages!.length).toBeGreaterThanOrEqual(6);
|
|
35
|
+
|
|
36
|
+
const wsResultOkChecks = INDEX_TS.match(/!wsResult\.ok/g);
|
|
37
|
+
expect(wsResultOkChecks).not.toBeNull();
|
|
38
|
+
expect(wsResultOkChecks!.length).toBeGreaterThanOrEqual(6);
|
|
39
|
+
|
|
40
|
+
const nextActionRefs = INDEX_TS.match(/wsResult\.nextAction/g);
|
|
41
|
+
expect(nextActionRefs).not.toBeNull();
|
|
42
|
+
expect(nextActionRefs!.length).toBeGreaterThanOrEqual(6);
|
|
30
43
|
});
|
|
31
44
|
|
|
32
|
-
it('
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
);
|
|
36
|
-
expect(constantMatch).not.toBeNull();
|
|
37
|
-
const constantValue = constantMatch![1];
|
|
38
|
-
for (const pattern of FORBIDDEN_NEXT_ACTION_PATTERNS) {
|
|
39
|
-
expect(pattern.test(constantValue)).toBe(false);
|
|
40
|
-
}
|
|
45
|
+
it('resolveHookWorkspaceDir failure result has reason and nextAction fields', () => {
|
|
46
|
+
expect(WORKSPACE_RESOLVER_TS).toContain("reason: 'workspace_dir_unresolvable'");
|
|
47
|
+
expect(WORKSPACE_RESOLVER_TS).toContain('nextAction:');
|
|
48
|
+
expect(WORKSPACE_RESOLVER_TS).toContain('PD_WORKSPACE_DIR');
|
|
41
49
|
});
|
|
42
50
|
});
|
|
@@ -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
|
|