principles-disciple 1.86.0 → 1.88.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.
@@ -6,11 +6,18 @@ import {
6
6
  guardService,
7
7
  getSurfaceIdForHook,
8
8
  getSurfaceIdForService,
9
+ __resetSurfaceGuardSkipLogStateForTests,
9
10
  } from '../../src/core/surface-guard.js';
10
11
  import { PLUGIN_SURFACE_REGISTRY } from '@principles/core/runtime-v2';
11
12
  import type { OpenClawPluginService } from '../../src/openclaw-sdk.js';
12
13
 
13
14
  describe('surface-guard', () => {
15
+ beforeEach(() => {
16
+ // Each test starts with a clean surface-guard skip log state so the
17
+ // first-fire assertions are deterministic (PRI-298).
18
+ __resetSurfaceGuardSkipLogStateForTests();
19
+ vi.clearAllMocks();
20
+ });
14
21
  describe('getSurfaceIdForHook', () => {
15
22
  it('generates correct surface id without label', () => {
16
23
  expect(getSurfaceIdForHook('before_tool_call')).toBe('hook:before_tool_call');
@@ -144,6 +151,39 @@ describe('surface-guard', () => {
144
151
  expect(mockHandler).not.toHaveBeenCalled();
145
152
  });
146
153
 
154
+ it('does not log at guardHook construction time (PRI-298 / chatgpt P2)', () => {
155
+ // Registering a guard for a quiet hook must not emit the SKIP line on
156
+ // its own; the log fires only when the returned no-op is actually
157
+ // invoked. Plugin startup that registers a dozen quiet hooks would
158
+ // otherwise log a dozen SKIP lines before any real traffic.
159
+ const mockLogger = { info: vi.fn(), debug: vi.fn() };
160
+ guardHook('hook:after_tool_call.trajectory', mockLogger, vi.fn());
161
+ expect(mockLogger.info).not.toHaveBeenCalled();
162
+ });
163
+
164
+ it('logger undefined on first fire does not consume the one-shot slot (PRI-298 / coderabbit Major)', () => {
165
+ // First call has no logger — the no-op still suppresses the handler,
166
+ // and the once-only slot is preserved for a later call that does
167
+ // have a logger.
168
+ const handler1 = guardHook('hook:after_tool_call.trajectory', undefined, vi.fn());
169
+ handler1({}, {});
170
+
171
+ const mockLogger = { info: vi.fn(), debug: vi.fn() };
172
+ const handler2 = guardHook('hook:after_tool_call.trajectory', mockLogger, vi.fn());
173
+ handler2({}, {});
174
+
175
+ // Only the second call (which had a logger) should have emitted a log.
176
+ expect(mockLogger.info).toHaveBeenCalledTimes(1);
177
+ expect(mockLogger.info).toHaveBeenCalledWith(
178
+ expect.stringContaining('[PD:surface-guard] SKIP hook:after_tool_call.trajectory'),
179
+ );
180
+
181
+ // A third call should now be silent (slot consumed by the second call).
182
+ const handler3 = guardHook('hook:after_tool_call.trajectory', mockLogger, vi.fn());
183
+ handler3({}, {});
184
+ expect(mockLogger.info).toHaveBeenCalledTimes(1);
185
+ });
186
+
147
187
  it('does not log for enabled surface', () => {
148
188
  const mockLogger = { info: vi.fn(), debug: vi.fn() };
149
189
  const mockHandler = vi.fn();
@@ -160,6 +200,45 @@ describe('surface-guard', () => {
160
200
  expect.stringContaining('not found in registry'),
161
201
  );
162
202
  });
203
+
204
+ it('logs once on first fire and stays silent on subsequent fires (PRI-298 rate-limit)', () => {
205
+ const mockLogger = { info: vi.fn(), debug: vi.fn() };
206
+ const mockHandler = vi.fn();
207
+ const guarded = guardHook('hook:after_tool_call.trajectory', mockLogger, mockHandler);
208
+
209
+ // First fire: log is emitted once with the disabled reason.
210
+ guarded({}, {});
211
+ expect(mockLogger.info).toHaveBeenCalledTimes(1);
212
+ expect(mockLogger.info).toHaveBeenCalledWith(
213
+ expect.stringContaining('[PD:surface-guard] SKIP hook:after_tool_call.trajectory'),
214
+ );
215
+
216
+ // Subsequent fires on the same surfaceId: no further log noise, but the
217
+ // handler is still suppressed (observability remains: the no-op returns
218
+ // undefined; the surface is still classified as disabled).
219
+ for (let i = 0; i < 5; i += 1) {
220
+ guarded({}, {});
221
+ }
222
+ expect(mockLogger.info).toHaveBeenCalledTimes(1);
223
+ expect(mockHandler).not.toHaveBeenCalled();
224
+ });
225
+
226
+ it('logs first fire per surfaceId independently (one log per quiet surface)', () => {
227
+ const mockLogger = { info: vi.fn(), debug: vi.fn() };
228
+ const handler1 = guardHook('hook:after_tool_call.trajectory', mockLogger, vi.fn());
229
+ const handler2 = guardHook('hook:llm_output.trajectory', mockLogger, vi.fn());
230
+ const handler3 = guardHook('hook:subagent_spawning', mockLogger, vi.fn());
231
+
232
+ handler1({}, {});
233
+ handler2({}, {});
234
+ handler3({}, {});
235
+
236
+ expect(mockLogger.info).toHaveBeenCalledTimes(3);
237
+ const calls = mockLogger.info.mock.calls.map(c => String(c[0]));
238
+ expect(calls.some(c => c.includes('hook:after_tool_call.trajectory'))).toBe(true);
239
+ expect(calls.some(c => c.includes('hook:llm_output.trajectory'))).toBe(true);
240
+ expect(calls.some(c => c.includes('hook:subagent_spawning'))).toBe(true);
241
+ });
163
242
  });
164
243
 
165
244
  describe('guardService', () => {
@@ -194,5 +273,68 @@ describe('surface-guard', () => {
194
273
  const result = guardService('service:nonexistent', mockService);
195
274
  expect(result).toBeNull();
196
275
  });
276
+
277
+ it('logs once per service surface on first registration (PRI-298 rate-limit)', () => {
278
+ const mockLogger = { info: vi.fn(), debug: vi.fn() };
279
+ const service: OpenClawPluginService = { id: 'test-service' };
280
+
281
+ // First registration call: the disabled reason is logged once.
282
+ const first = guardService('service:trajectory', service, mockLogger);
283
+ expect(first).toBeNull();
284
+ expect(mockLogger.info).toHaveBeenCalledTimes(1);
285
+ expect(mockLogger.info).toHaveBeenCalledWith(
286
+ expect.stringContaining('SKIP service service:trajectory'),
287
+ );
288
+
289
+ // Subsequent guardService calls for the same surfaceId stay silent.
290
+ guardService('service:trajectory', service, mockLogger);
291
+ guardService('service:trajectory', service, mockLogger);
292
+ expect(mockLogger.info).toHaveBeenCalledTimes(1);
293
+ });
294
+ });
295
+
296
+ describe('PRI-298 disabledReason copy', () => {
297
+ it('no quiet surface disabledReason references "Story A" or "Story A\'"', () => {
298
+ const quietOrNonCore = PLUGIN_SURFACE_REGISTRY.filter(
299
+ s => s.category === 'quiet' || s.category === 'gone' || s.category === 'legacy_retire',
300
+ );
301
+ expect(quietOrNonCore.length).toBeGreaterThan(0);
302
+ for (const surface of quietOrNonCore) {
303
+ expect(surface.disabledReason).toBeDefined();
304
+ // MVP residue that should not appear in production log copy.
305
+ expect(surface.disabledReason).not.toMatch(/Story A/);
306
+ expect(surface.disabledReason).not.toMatch(/MVP\s*验收/);
307
+ expect(surface.disabledReason).not.toMatch(/测试任务/);
308
+ }
309
+ });
310
+
311
+ it('trajectory hook disabledReason is opt-in and ADR-anchored (PRI-298)', () => {
312
+ const trajectory = PLUGIN_SURFACE_REGISTRY.find(
313
+ s => s.id === 'hook:after_tool_call.trajectory',
314
+ );
315
+ expect(trajectory?.disabledReason).toBeDefined();
316
+ const reason = trajectory!.disabledReason!.toLowerCase();
317
+ // Quiet hook copy is opt-in / opt-out anchored on a real ADR section
318
+ // (no MVP-phase residue, no promise of a feature-flag override that
319
+ // the production guard path does not actually consume — chatgpt P2).
320
+ expect(reason).toContain('opt-in');
321
+ expect(reason).toContain('default off');
322
+ expect(reason).toMatch(/adr-?0014/);
323
+ });
324
+
325
+ it('no quiet surface disabledReason promises a feature flag override (PRI-298 / chatgpt P2)', () => {
326
+ // The runtime guard path (`isSurfaceEnabled(surfaceId)` with no
327
+ // overrides argument) does not consume `.pd/config.yaml`, so
328
+ // telling operators to "enable via feature flag override" would be
329
+ // an impossible next action. Quiet copy must describe the surface
330
+ // honestly without pointing to a non-existent override path.
331
+ const quiet = PLUGIN_SURFACE_REGISTRY.filter(s => s.category === 'quiet');
332
+ expect(quiet.length).toBeGreaterThan(0);
333
+ for (const surface of quiet) {
334
+ const reason = surface.disabledReason!.toLowerCase();
335
+ expect(reason).not.toContain('enable via feature flag');
336
+ expect(reason).not.toMatch(/enable via .* override/);
337
+ }
338
+ });
197
339
  });
198
340
  });
@@ -124,6 +124,7 @@ describe('PRI-212 plugin core anti-growth guard', () => {
124
124
  'runtime-v2-prompt-activation-reader.ts',
125
125
  'workspace-guidance-migrator.ts',
126
126
  'surface-guard.ts',
127
+ 'pd-config-loader.ts',
127
128
  ] as const;
128
129
 
129
130
  // Category 6: Test files
@@ -2,8 +2,8 @@
2
2
  * PRI-288: Quarantine EvolutionWorkerService default startup behind MVP feature flag.
3
3
  *
4
4
  * Tests prove:
5
- * 1. Default config (no feature-flags.yaml) → EvolutionWorkerService does NOT start.
6
- * 2. Explicit enable in feature-flags.yaml → EvolutionWorkerService starts.
5
+ * 1. Default config (no config.yaml) → EvolutionWorkerService does NOT start.
6
+ * 2. Explicit enable in config.yaml → EvolutionWorkerService starts.
7
7
  * 3. Disabled state has structured observability from real helper, not hand-written JSON.
8
8
  * 4. api.registerService still works regardless of flag state.
9
9
  *
@@ -60,9 +60,63 @@ function createTempWorkspace(): string {
60
60
  return dir;
61
61
  }
62
62
 
63
- function writeFeatureFlags(workspaceDir: string, flags: Record<string, unknown>): void {
64
- const configPath = path.join(workspaceDir, '.pd', 'feature-flags.yaml');
65
- const content = yaml.dump(flags, { schema: yaml.JSON_SCHEMA });
63
+ function deepMergeFeatures(
64
+ defaults: Record<string, unknown>,
65
+ overrides: Record<string, unknown>,
66
+ ): Record<string, unknown> {
67
+ const result: Record<string, unknown> = { ...defaults };
68
+ for (const [key, value] of Object.entries(overrides)) {
69
+ if (
70
+ value != null &&
71
+ typeof value === 'object' &&
72
+ !Array.isArray(value) &&
73
+ Object.hasOwn(result, key) &&
74
+ result[key] != null &&
75
+ typeof result[key] === 'object' &&
76
+ !Array.isArray(result[key])
77
+ ) {
78
+ result[key] = { ...(result[key] as Record<string, unknown>), ...(value as Record<string, unknown>) };
79
+ } else {
80
+ result[key] = value;
81
+ }
82
+ }
83
+ return result;
84
+ }
85
+
86
+ function writeConfigYaml(workspaceDir: string, featureOverrides: Record<string, unknown>): void {
87
+ const configPath = path.join(workspaceDir, '.pd', 'config.yaml');
88
+ const defaultFeatures: Record<string, unknown> = {
89
+ prompt: { category: 'core', enabled: true },
90
+ code_tool_hook: { category: 'core', enabled: true },
91
+ defer_archive: { category: 'core', enabled: true },
92
+ correction_observer: { category: 'quiet', enabled: false },
93
+ empathy_observer: { category: 'quiet', enabled: false },
94
+ evolution_worker: { category: 'quiet', enabled: false },
95
+ nocturnal: { category: 'gone', enabled: false },
96
+ };
97
+ const config = {
98
+ version: 1,
99
+ features: deepMergeFeatures(defaultFeatures, featureOverrides),
100
+ runtimeProfiles: {
101
+ 'openclaw.default': { type: 'openclaw', source: 'default' },
102
+ },
103
+ internalAgents: {
104
+ defaultRuntime: 'openclaw.default',
105
+ agents: {
106
+ diagnostician: { enabled: true },
107
+ dreamer: { enabled: true },
108
+ scribe: { enabled: true },
109
+ artificer: { enabled: true },
110
+ philosopher: { enabled: false },
111
+ evaluator: { enabled: false },
112
+ rolloutReviewer: { enabled: false },
113
+ trainer: { enabled: false },
114
+ correctionObserver: { enabled: false },
115
+ empathyObserver: { enabled: false },
116
+ },
117
+ },
118
+ };
119
+ const content = yaml.dump(config, { schema: yaml.JSON_SCHEMA });
66
120
  fs.writeFileSync(configPath, content, 'utf8');
67
121
  }
68
122
 
@@ -112,32 +166,32 @@ describe('PRI-288: EvolutionWorkerService quarantine', () => {
112
166
  // ── 2. loadFeatureFlagFromWorkspace ──
113
167
 
114
168
  describe('loadFeatureFlagFromWorkspace', () => {
115
- it('returns enabled=false when no feature-flags.yaml exists', () => {
169
+ it('returns enabled=false when no config.yaml exists', () => {
116
170
  const logger = createMockLogger();
117
171
  const result = loadFeatureFlagFromWorkspace(workspaceDir, 'evolution_worker', logger);
118
172
  expect(result.enabled).toBe(false);
119
173
  expect(result.source).toBe('defaults');
120
174
  });
121
175
 
122
- it('returns enabled=false when feature-flags.yaml has no evolution_worker entry', () => {
123
- writeFeatureFlags(workspaceDir, { prompt: { enabled: true } });
176
+ it('returns enabled=false when config.yaml has no evolution_worker entry', () => {
177
+ writeConfigYaml(workspaceDir, { prompt: { enabled: true } });
124
178
  const logger = createMockLogger();
125
179
  const result = loadFeatureFlagFromWorkspace(workspaceDir, 'evolution_worker', logger);
126
180
  expect(result.enabled).toBe(false);
127
181
  });
128
182
 
129
- it('returns enabled=true when feature-flags.yaml explicitly enables evolution_worker', () => {
130
- writeFeatureFlags(workspaceDir, {
183
+ it('returns enabled=true when config.yaml explicitly enables evolution_worker', () => {
184
+ writeConfigYaml(workspaceDir, {
131
185
  evolution_worker: { enabled: true },
132
186
  });
133
187
  const logger = createMockLogger();
134
188
  const result = loadFeatureFlagFromWorkspace(workspaceDir, 'evolution_worker', logger);
135
189
  expect(result.enabled).toBe(true);
136
- expect(result.source).toBe('workspace_file');
190
+ expect(result.source).toBe('user_config');
137
191
  });
138
192
 
139
193
  it('returns enabled=false when YAML is malformed and warning includes error detail', () => {
140
- const configPath = path.join(workspaceDir, '.pd', 'feature-flags.yaml');
194
+ const configPath = path.join(workspaceDir, '.pd', 'config.yaml');
141
195
  fs.writeFileSync(configPath, ' bad: [yaml: content', 'utf8');
142
196
  const logger = createMockLogger();
143
197
  const result = loadFeatureFlagFromWorkspace(workspaceDir, 'evolution_worker', logger);
@@ -152,7 +206,7 @@ describe('PRI-288: EvolutionWorkerService quarantine', () => {
152
206
 
153
207
  it('returns defaults when file is unreadable', () => {
154
208
  // Create a directory where a file should be — causes read error
155
- const configPath = path.join(workspaceDir, '.pd', 'feature-flags.yaml');
209
+ const configPath = path.join(workspaceDir, '.pd', 'config.yaml');
156
210
  fs.mkdirSync(configPath, { recursive: true });
157
211
  const logger = createMockLogger();
158
212
  const result = loadFeatureFlagFromWorkspace(workspaceDir, 'evolution_worker', logger);
@@ -162,7 +216,7 @@ describe('PRI-288: EvolutionWorkerService quarantine', () => {
162
216
 
163
217
  it('rejects dangerous keys (__proto__) and does not enable via prototype pollution', () => {
164
218
  // Write raw YAML with __proto__ to test dangerous key rejection on raw parsed output
165
- writeFeatureFlags(workspaceDir, {
219
+ writeConfigYaml(workspaceDir, {
166
220
  __proto__: { enabled: true },
167
221
  evolution_worker: { enabled: false },
168
222
  });
@@ -175,7 +229,7 @@ describe('PRI-288: EvolutionWorkerService quarantine', () => {
175
229
  // ── 3. shouldStartEvolutionWorker — real helper, real output ──
176
230
 
177
231
  describe('shouldStartEvolutionWorker gate helper', () => {
178
- it('returns shouldStart=false by default (no feature-flags.yaml)', () => {
232
+ it('returns shouldStart=false by default (no config.yaml)', () => {
179
233
  const logger = createMockLogger();
180
234
  const gate = shouldStartEvolutionWorker(workspaceDir, logger);
181
235
  expect(gate.shouldStart).toBe(false);
@@ -184,13 +238,13 @@ describe('PRI-288: EvolutionWorkerService quarantine', () => {
184
238
  });
185
239
 
186
240
  it('returns shouldStart=true when explicitly enabled', () => {
187
- writeFeatureFlags(workspaceDir, {
241
+ writeConfigYaml(workspaceDir, {
188
242
  evolution_worker: { enabled: true },
189
243
  });
190
244
  const logger = createMockLogger();
191
245
  const gate = shouldStartEvolutionWorker(workspaceDir, logger);
192
246
  expect(gate.shouldStart).toBe(true);
193
- expect(gate.flagSource).toBe('workspace_file');
247
+ expect(gate.flagSource).toBe('user_config');
194
248
  expect(gate.disabledInfo).toBeNull();
195
249
  });
196
250
 
@@ -248,18 +302,18 @@ describe('PRI-288: EvolutionWorkerService quarantine', () => {
248
302
 
249
303
  describe('explicit enable: worker starts', () => {
250
304
  it('shouldStartEvolutionWorker returns true when enabled in config', () => {
251
- writeFeatureFlags(workspaceDir, {
305
+ writeConfigYaml(workspaceDir, {
252
306
  evolution_worker: { enabled: true },
253
307
  });
254
308
 
255
309
  const logger = createMockLogger();
256
310
  const gate = shouldStartEvolutionWorker(workspaceDir, logger);
257
311
  expect(gate.shouldStart).toBe(true);
258
- expect(gate.flagSource).toBe('workspace_file');
312
+ expect(gate.flagSource).toBe('user_config');
259
313
  });
260
314
 
261
315
  it('EvolutionWorkerService.start actually runs when gate is true', () => {
262
- writeFeatureFlags(workspaceDir, {
316
+ writeConfigYaml(workspaceDir, {
263
317
  evolution_worker: { enabled: true },
264
318
  });
265
319
 
@@ -292,17 +346,18 @@ describe('PRI-288: EvolutionWorkerService quarantine', () => {
292
346
  });
293
347
 
294
348
  it('computeEffectiveFlags preserves core flags even with evolution_worker override', () => {
295
- writeFeatureFlags(workspaceDir, {
349
+ writeConfigYaml(workspaceDir, {
296
350
  evolution_worker: { enabled: true },
297
351
  });
298
352
 
299
- const configPath = path.join(workspaceDir, '.pd', 'feature-flags.yaml');
353
+ const configPath = path.join(workspaceDir, '.pd', 'config.yaml');
300
354
  const raw = fs.readFileSync(configPath, 'utf8');
301
355
  const parsed: unknown = yaml.load(raw, { schema: yaml.JSON_SCHEMA });
302
356
 
303
357
  // Use isRecord type guard instead of `as`
304
358
  expect(isRecord(parsed)).toBe(true);
305
- const flags = computeEffectiveFlags(parsed as Record<string, unknown>, DEFAULT_FEATURE_FLAGS, configPath);
359
+ const features = (parsed as Record<string, unknown>).features;
360
+ const flags = computeEffectiveFlags(features as Record<string, unknown>, DEFAULT_FEATURE_FLAGS, configPath);
306
361
  expect(flags.flags['prompt']?.enabled).toBe(true);
307
362
  expect(flags.flags['code_tool_hook']?.enabled).toBe(true);
308
363
  expect(flags.flags['defer_archive']?.enabled).toBe(true);
@@ -310,17 +365,18 @@ describe('PRI-288: EvolutionWorkerService quarantine', () => {
310
365
  });
311
366
 
312
367
  it('core flags cannot be disabled by user override', () => {
313
- writeFeatureFlags(workspaceDir, {
368
+ writeConfigYaml(workspaceDir, {
314
369
  prompt: { enabled: false },
315
370
  code_tool_hook: { enabled: false },
316
371
  });
317
372
 
318
- const configPath = path.join(workspaceDir, '.pd', 'feature-flags.yaml');
373
+ const configPath = path.join(workspaceDir, '.pd', 'config.yaml');
319
374
  const raw = fs.readFileSync(configPath, 'utf8');
320
375
  const parsed: unknown = yaml.load(raw, { schema: yaml.JSON_SCHEMA });
321
376
 
322
377
  expect(isRecord(parsed)).toBe(true);
323
- const flags = computeEffectiveFlags(parsed as Record<string, unknown>, DEFAULT_FEATURE_FLAGS, configPath);
378
+ const features = (parsed as Record<string, unknown>).features;
379
+ const flags = computeEffectiveFlags(features as Record<string, unknown>, DEFAULT_FEATURE_FLAGS, configPath);
324
380
  expect(flags.flags['prompt']?.enabled).toBe(true); // core cannot be disabled
325
381
  expect(flags.flags['code_tool_hook']?.enabled).toBe(true); // core cannot be disabled
326
382
  expect(flags.warnings.length).toBeGreaterThan(0); // warnings about core override attempt
@@ -331,7 +387,7 @@ describe('PRI-288: EvolutionWorkerService quarantine', () => {
331
387
 
332
388
  describe('no confirm-first gate regression', () => {
333
389
  it('no PLAN.md or confirm-first files are created in workspace', () => {
334
- writeFeatureFlags(workspaceDir, {
390
+ writeConfigYaml(workspaceDir, {
335
391
  evolution_worker: { enabled: false },
336
392
  });
337
393
 
@@ -28,9 +28,63 @@ function createTempWorkspace(): string {
28
28
  return dir;
29
29
  }
30
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 });
31
+ function deepMergeFeatures(
32
+ defaults: Record<string, unknown>,
33
+ overrides: Record<string, unknown>,
34
+ ): Record<string, unknown> {
35
+ const result: Record<string, unknown> = { ...defaults };
36
+ for (const [key, value] of Object.entries(overrides)) {
37
+ if (
38
+ value != null &&
39
+ typeof value === 'object' &&
40
+ !Array.isArray(value) &&
41
+ Object.hasOwn(result, key) &&
42
+ result[key] != null &&
43
+ typeof result[key] === 'object' &&
44
+ !Array.isArray(result[key])
45
+ ) {
46
+ result[key] = { ...(result[key] as Record<string, unknown>), ...(value as Record<string, unknown>) };
47
+ } else {
48
+ result[key] = value;
49
+ }
50
+ }
51
+ return result;
52
+ }
53
+
54
+ function writeConfigYaml(workspaceDir: string, featureOverrides: Record<string, unknown>): void {
55
+ const configPath = path.join(workspaceDir, '.pd', 'config.yaml');
56
+ const defaultFeatures: Record<string, unknown> = {
57
+ prompt: { category: 'core', enabled: true },
58
+ code_tool_hook: { category: 'core', enabled: true },
59
+ defer_archive: { category: 'core', enabled: true },
60
+ correction_observer: { category: 'quiet', enabled: false },
61
+ empathy_observer: { category: 'quiet', enabled: false },
62
+ evolution_worker: { category: 'quiet', enabled: false },
63
+ nocturnal: { category: 'gone', enabled: false },
64
+ };
65
+ const config = {
66
+ version: 1,
67
+ features: deepMergeFeatures(defaultFeatures, featureOverrides),
68
+ runtimeProfiles: {
69
+ 'openclaw.default': { type: 'openclaw', source: 'default' },
70
+ },
71
+ internalAgents: {
72
+ defaultRuntime: 'openclaw.default',
73
+ agents: {
74
+ diagnostician: { enabled: true },
75
+ dreamer: { enabled: true },
76
+ scribe: { enabled: true },
77
+ artificer: { enabled: true },
78
+ philosopher: { enabled: false },
79
+ evaluator: { enabled: false },
80
+ rolloutReviewer: { enabled: false },
81
+ trainer: { enabled: false },
82
+ correctionObserver: { enabled: false },
83
+ empathyObserver: { enabled: false },
84
+ },
85
+ },
86
+ };
87
+ const content = yaml.dump(config, { schema: yaml.JSON_SCHEMA });
34
88
  fs.writeFileSync(configPath, content, 'utf8');
35
89
  }
36
90
 
@@ -151,6 +205,9 @@ describe('PRI-294: CorrectionObserver independence from EvolutionWorker', () =>
151
205
  });
152
206
 
153
207
  it('CorrectionObserver starts when EvolutionWorker is disabled (default)', () => {
208
+ writeConfigYaml(workspaceDir, {
209
+ correction_observer: { enabled: true },
210
+ });
154
211
  const logger = createMockLogger();
155
212
 
156
213
  // Verify EvolutionWorker is disabled
@@ -164,8 +221,9 @@ describe('PRI-294: CorrectionObserver independence from EvolutionWorker', () =>
164
221
  });
165
222
 
166
223
  it('CorrectionObserver starts when EvolutionWorker is explicitly enabled', () => {
167
- writeFeatureFlags(workspaceDir, {
224
+ writeConfigYaml(workspaceDir, {
168
225
  evolution_worker: { enabled: true },
226
+ correction_observer: { enabled: true },
169
227
  });
170
228
  const logger = createMockLogger();
171
229
 
@@ -177,7 +235,7 @@ describe('PRI-294: CorrectionObserver independence from EvolutionWorker', () =>
177
235
  });
178
236
 
179
237
  it('CorrectionObserver can be independently disabled', () => {
180
- writeFeatureFlags(workspaceDir, {
238
+ writeConfigYaml(workspaceDir, {
181
239
  correction_observer: { enabled: false },
182
240
  });
183
241
  const logger = createMockLogger();
@@ -494,9 +494,11 @@ describe('Runtime V2 prompt activation — additional guard tests', () => {
494
494
 
495
495
  it('malformed DB/config input fails loud with warning', async () => {
496
496
  const pdDir = path.join(tempWorkspaceDir, '.pd');
497
+ // PRI-305/PRI-307: Write a malformed .pd/config.yaml with dangerous keys
498
+ // The core validator rejects __proto__ and constructor as dangerous keys
497
499
  fs.writeFileSync(
498
- path.join(pdDir, 'feature-flags.yaml'),
499
- '__proto__:\n enabled: true\nprompt:\n enabled: true\nconstructor:\n enabled: false\n',
500
+ path.join(pdDir, 'config.yaml'),
501
+ 'version: 1\nfeatures:\n __proto__:\n category: core\n enabled: true\n prompt:\n category: core\n enabled: true\n constructor:\n category: core\n enabled: false\nruntimeProfiles:\n openclaw.default:\n type: openclaw\n source: default\ninternalAgents:\n defaultRuntime: openclaw.default\n agents:\n diagnostician:\n enabled: true\n dreamer:\n enabled: true\n scribe:\n enabled: true\n artificer:\n enabled: true\n philosopher:\n enabled: false\n evaluator:\n enabled: false\n rolloutReviewer:\n enabled: false\n trainer:\n enabled: false\n correctionObserver:\n enabled: false\n empathyObserver:\n enabled: false\n',
500
502
  'utf8',
501
503
  );
502
504
 
@@ -507,10 +509,14 @@ describe('Runtime V2 prompt activation — additional guard tests', () => {
507
509
  const result = await reader.readActivatedPrinciples();
508
510
 
509
511
  const warnCalls = warnSpy.mock.calls.map((c: unknown[]) => String(c[0]));
512
+ // PRI-305/PRI-307: Core validator rejects dangerous keys as errors.
513
+ // The plugin config loader logs config errors as warnings.
510
514
  const hasDangerousKeyWarning = warnCalls.some(
511
- (c: string) => c.includes('dangerous key') || c.includes('__proto__') || c.includes('constructor'),
515
+ (c: string) => c.includes('dangerous key') || c.includes('__proto__') || c.includes('constructor') || c.includes('Config error'),
512
516
  );
513
517
  expect(hasDangerousKeyWarning).toBe(true);
518
+ // With malformed config, defaults are used (prompt enabled by default),
519
+ // but no DB data exists, so principles should be empty
514
520
  expect(result.principles).toEqual([]);
515
521
  });
516
522