principles-disciple 1.87.0 → 1.89.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/src/commands/pain.ts +5 -0
- package/src/core/pd-config-loader.ts +400 -0
- package/src/core/runtime-v2-prompt-activation-reader.ts +15 -63
- package/src/hooks/pain.ts +6 -0
- package/src/index.ts +8 -56
- package/src/service/correction-observer-service.ts +62 -31
- package/tests/core/pd-config-loader.test.ts +407 -0
- package/tests/core/surface-guard.test.ts +1 -1
- package/tests/core-anti-growth.test.ts +1 -0
- package/tests/evolution-worker-quarantine.test.ts +83 -27
- package/tests/evolution-worker-slimming.test.ts +63 -5
- package/tests/hooks/runtime-v2-prompt-activation.test.ts +9 -3
- package/tests/service/correction-observer-service.test.ts +147 -21
- package/tests/service/evolution-worker.correction-observer.test.ts +1 -1
|
@@ -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
|
|
6
|
-
* 2. Explicit enable in
|
|
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
|
|
64
|
-
|
|
65
|
-
|
|
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
|
|
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
|
|
123
|
-
|
|
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
|
|
130
|
-
|
|
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('
|
|
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', '
|
|
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', '
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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('
|
|
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
|
-
|
|
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('
|
|
312
|
+
expect(gate.flagSource).toBe('user_config');
|
|
259
313
|
});
|
|
260
314
|
|
|
261
315
|
it('EvolutionWorkerService.start actually runs when gate is true', () => {
|
|
262
|
-
|
|
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
|
-
|
|
349
|
+
writeConfigYaml(workspaceDir, {
|
|
296
350
|
evolution_worker: { enabled: true },
|
|
297
351
|
});
|
|
298
352
|
|
|
299
|
-
const configPath = path.join(workspaceDir, '.pd', '
|
|
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
|
|
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
|
-
|
|
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', '
|
|
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
|
|
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
|
-
|
|
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
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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, '
|
|
499
|
-
'__proto__:\n enabled: true\
|
|
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
|
|
|
@@ -2,6 +2,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
|
2
2
|
import * as fs from 'fs';
|
|
3
3
|
import * as os from 'os';
|
|
4
4
|
import * as path from 'path';
|
|
5
|
+
import * as yaml from 'js-yaml';
|
|
5
6
|
import { WorkspaceContext } from '../../src/core/workspace-context.js';
|
|
6
7
|
|
|
7
8
|
const mockLearner = {
|
|
@@ -35,6 +36,89 @@ vi.mock('../../src/service/keyword-optimization-service.js', () => ({
|
|
|
35
36
|
KeywordOptimizationService: { get: vi.fn(() => mockOptimizationService) },
|
|
36
37
|
}));
|
|
37
38
|
|
|
39
|
+
// PRI-307: Mock the pd-config-loader instead of @principles/core/runtime-v2
|
|
40
|
+
// The service now reads .pd/config.yaml via resolveObserverConfig
|
|
41
|
+
vi.mock('../../src/core/pd-config-loader.js', () => {
|
|
42
|
+
return {
|
|
43
|
+
loadPdConfigForPlugin: vi.fn(() => ({
|
|
44
|
+
ok: true,
|
|
45
|
+
effective: {
|
|
46
|
+
config: {
|
|
47
|
+
version: 1,
|
|
48
|
+
features: {
|
|
49
|
+
prompt: { category: 'core', enabled: true },
|
|
50
|
+
code_tool_hook: { category: 'core', enabled: true },
|
|
51
|
+
defer_archive: { category: 'core', enabled: true },
|
|
52
|
+
correction_observer: { category: 'quiet', enabled: true },
|
|
53
|
+
empathy_observer: { category: 'quiet', enabled: false },
|
|
54
|
+
},
|
|
55
|
+
runtimeProfiles: {
|
|
56
|
+
'openclaw.default': { type: 'openclaw', source: 'default' },
|
|
57
|
+
'pd.anthropic-sonnet': { type: 'pi-ai', provider: 'anthropic', model: 'claude-3-5-sonnet', apiKeyEnv: 'ANTHROPIC_API_KEY', timeoutMs: 30000 },
|
|
58
|
+
},
|
|
59
|
+
internalAgents: {
|
|
60
|
+
defaultRuntime: 'openclaw.default',
|
|
61
|
+
agents: {
|
|
62
|
+
diagnostician: { enabled: true },
|
|
63
|
+
dreamer: { enabled: true },
|
|
64
|
+
scribe: { enabled: true },
|
|
65
|
+
artificer: { enabled: true },
|
|
66
|
+
philosopher: { enabled: false },
|
|
67
|
+
evaluator: { enabled: false },
|
|
68
|
+
rolloutReviewer: { enabled: false },
|
|
69
|
+
trainer: { enabled: false },
|
|
70
|
+
correctionObserver: { enabled: true, runtimeProfile: 'pd.anthropic-sonnet' },
|
|
71
|
+
empathyObserver: { enabled: false },
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
warnings: [],
|
|
76
|
+
},
|
|
77
|
+
source: 'defaults',
|
|
78
|
+
configPath: '.pd/config.yaml',
|
|
79
|
+
warnings: [],
|
|
80
|
+
errors: [],
|
|
81
|
+
})),
|
|
82
|
+
loadFeatureFlagFromConfig: vi.fn(() => ({ enabled: true, source: 'defaults' })),
|
|
83
|
+
resolveObserverConfig: vi.fn((_workspaceDir: string, flagId: string, _agentName: string) => {
|
|
84
|
+
// Default: return disabled for correction_observer (no config file in test tmp dirs)
|
|
85
|
+
if (flagId === 'correction_observer') {
|
|
86
|
+
return {
|
|
87
|
+
enabled: true,
|
|
88
|
+
readiness: 'not_ready',
|
|
89
|
+
source: 'defaults',
|
|
90
|
+
reason: 'pi-ai profile configured with apiKeyEnv',
|
|
91
|
+
nextAction: 'Run pd runtime probe',
|
|
92
|
+
runtimeProfileId: 'pd.anthropic-sonnet',
|
|
93
|
+
runtimeProfileType: 'pi-ai',
|
|
94
|
+
apiKeyEnv: 'ANTHROPIC_API_KEY',
|
|
95
|
+
apiKeyPresent: !!process.env.ANTHROPIC_API_KEY,
|
|
96
|
+
provider: 'anthropic',
|
|
97
|
+
model: 'claude-3-5-sonnet',
|
|
98
|
+
timeoutMs: 30000,
|
|
99
|
+
baseUrl: null,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
return {
|
|
103
|
+
enabled: false,
|
|
104
|
+
readiness: 'disabled',
|
|
105
|
+
source: 'defaults',
|
|
106
|
+
reason: `${flagId} is disabled`,
|
|
107
|
+
nextAction: `Set features.${flagId}.enabled=true in .pd/config.yaml`,
|
|
108
|
+
runtimeProfileId: null,
|
|
109
|
+
runtimeProfileType: null,
|
|
110
|
+
apiKeyEnv: null,
|
|
111
|
+
apiKeyPresent: false,
|
|
112
|
+
provider: null,
|
|
113
|
+
model: null,
|
|
114
|
+
timeoutMs: null,
|
|
115
|
+
baseUrl: null,
|
|
116
|
+
};
|
|
117
|
+
}),
|
|
118
|
+
getPdConfigPath: vi.fn((workspaceDir: string) => path.join(workspaceDir, '.pd', 'config.yaml')),
|
|
119
|
+
};
|
|
120
|
+
});
|
|
121
|
+
|
|
38
122
|
const mockDispatch = vi.fn().mockResolvedValue({
|
|
39
123
|
updated: true,
|
|
40
124
|
summary: 'Keyword store optimized',
|
|
@@ -45,23 +129,12 @@ const mockRegister = vi.fn();
|
|
|
45
129
|
|
|
46
130
|
vi.mock('@principles/core/runtime-v2', () => {
|
|
47
131
|
return {
|
|
48
|
-
WorkflowFunnelLoader: class {
|
|
49
|
-
getFunnel = vi.fn(() => ({
|
|
50
|
-
policy: {
|
|
51
|
-
runtimeKind: 'pi-ai',
|
|
52
|
-
provider: 'anthropic',
|
|
53
|
-
model: 'anthropic/claude-3-5-sonnet',
|
|
54
|
-
apiKeyEnv: 'ANTHROPIC_API_KEY',
|
|
55
|
-
timeoutMs: 30000,
|
|
56
|
-
}
|
|
57
|
-
}));
|
|
58
|
-
},
|
|
59
132
|
PiAiRuntimeAdapter: class {},
|
|
60
133
|
CorrectionObserver: class {},
|
|
61
134
|
AgentScheduler: class {
|
|
62
135
|
register = mockRegister;
|
|
63
136
|
dispatch = mockDispatch;
|
|
64
|
-
}
|
|
137
|
+
},
|
|
65
138
|
};
|
|
66
139
|
});
|
|
67
140
|
|
|
@@ -330,12 +403,12 @@ describe('runCorrectionObserverCycle — Independent Execution', () => {
|
|
|
330
403
|
});
|
|
331
404
|
});
|
|
332
405
|
|
|
333
|
-
describe('resolveCorrectionObserver — Configuration Resolution', () => {
|
|
406
|
+
describe('resolveCorrectionObserver — Configuration Resolution (PRI-307)', () => {
|
|
334
407
|
beforeEach(() => {
|
|
335
408
|
vi.clearAllMocks();
|
|
336
409
|
});
|
|
337
410
|
|
|
338
|
-
it('returns observer when API key env is set with
|
|
411
|
+
it('returns observer when API key env is set with pi-ai profile', async () => {
|
|
339
412
|
const workspaceDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pd-corr-resolve-'));
|
|
340
413
|
const stateDir = path.join(workspaceDir, '.state');
|
|
341
414
|
fs.mkdirSync(stateDir, { recursive: true });
|
|
@@ -348,7 +421,7 @@ describe('resolveCorrectionObserver — Configuration Resolution', () => {
|
|
|
348
421
|
const wctx = WorkspaceContext.fromHookContext({ workspaceDir });
|
|
349
422
|
const result = resolveCorrectionObserver(wctx, logger as any);
|
|
350
423
|
|
|
351
|
-
// With mocked
|
|
424
|
+
// With mocked resolveObserverConfig returning enabled + not_ready, should return observer
|
|
352
425
|
expect(result).not.toBeNull();
|
|
353
426
|
} finally {
|
|
354
427
|
delete process.env.ANTHROPIC_API_KEY;
|
|
@@ -356,23 +429,76 @@ describe('resolveCorrectionObserver — Configuration Resolution', () => {
|
|
|
356
429
|
}
|
|
357
430
|
});
|
|
358
431
|
|
|
359
|
-
it('returns
|
|
360
|
-
const workspaceDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pd-corr-
|
|
432
|
+
it('returns null when observer is disabled in config', async () => {
|
|
433
|
+
const workspaceDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pd-corr-disabled-'));
|
|
361
434
|
const stateDir = path.join(workspaceDir, '.state');
|
|
362
435
|
fs.mkdirSync(stateDir, { recursive: true });
|
|
363
436
|
|
|
364
|
-
|
|
437
|
+
const logger = { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() };
|
|
438
|
+
|
|
439
|
+
// Override the mock to return disabled
|
|
440
|
+
const { resolveObserverConfig } = await import('../../src/core/pd-config-loader.js');
|
|
441
|
+
vi.mocked(resolveObserverConfig).mockReturnValueOnce({
|
|
442
|
+
enabled: false,
|
|
443
|
+
readiness: 'disabled',
|
|
444
|
+
source: 'defaults',
|
|
445
|
+
reason: 'correction_observer is disabled in .pd/config.yaml',
|
|
446
|
+
nextAction: 'Set features.correction_observer.enabled=true in .pd/config.yaml to enable',
|
|
447
|
+
runtimeProfileId: null,
|
|
448
|
+
runtimeProfileType: null,
|
|
449
|
+
apiKeyEnv: null,
|
|
450
|
+
apiKeyPresent: false,
|
|
451
|
+
provider: null,
|
|
452
|
+
model: null,
|
|
453
|
+
timeoutMs: null,
|
|
454
|
+
baseUrl: null,
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
try {
|
|
458
|
+
const wctx = WorkspaceContext.fromHookContext({ workspaceDir });
|
|
459
|
+
const result = resolveCorrectionObserver(wctx, logger as any);
|
|
460
|
+
|
|
461
|
+
expect(result).toBeNull();
|
|
462
|
+
} finally {
|
|
463
|
+
safeRmDir(workspaceDir);
|
|
464
|
+
}
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
it('returns null when observer needs setup (no API key)', async () => {
|
|
468
|
+
const workspaceDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pd-corr-needs-setup-'));
|
|
469
|
+
const stateDir = path.join(workspaceDir, '.state');
|
|
470
|
+
fs.mkdirSync(stateDir, { recursive: true });
|
|
365
471
|
|
|
366
472
|
const logger = { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() };
|
|
367
473
|
|
|
474
|
+
// Override the mock to return needs_setup
|
|
475
|
+
const { resolveObserverConfig } = await import('../../src/core/pd-config-loader.js');
|
|
476
|
+
vi.mocked(resolveObserverConfig).mockReturnValueOnce({
|
|
477
|
+
enabled: true,
|
|
478
|
+
readiness: 'needs_setup',
|
|
479
|
+
source: 'defaults',
|
|
480
|
+
reason: "Environment variable 'ANTHROPIC_API_KEY' is not set or empty",
|
|
481
|
+
nextAction: 'Set the environment variable ANTHROPIC_API_KEY with a valid API key',
|
|
482
|
+
runtimeProfileId: 'pd.anthropic-sonnet',
|
|
483
|
+
runtimeProfileType: 'pi-ai',
|
|
484
|
+
apiKeyEnv: 'ANTHROPIC_API_KEY',
|
|
485
|
+
apiKeyPresent: false,
|
|
486
|
+
provider: 'anthropic',
|
|
487
|
+
model: 'claude-3-5-sonnet',
|
|
488
|
+
timeoutMs: 30000,
|
|
489
|
+
baseUrl: null,
|
|
490
|
+
});
|
|
491
|
+
|
|
368
492
|
try {
|
|
369
493
|
const wctx = WorkspaceContext.fromHookContext({ workspaceDir });
|
|
370
494
|
const result = resolveCorrectionObserver(wctx, logger as any);
|
|
371
495
|
|
|
372
|
-
|
|
373
|
-
|
|
496
|
+
expect(result).toBeNull();
|
|
497
|
+
// Should log the needs_setup reason, not noisy "no API key" cycling
|
|
498
|
+
expect(logger.info).toHaveBeenCalledWith(
|
|
499
|
+
expect.stringContaining('ANTHROPIC_API_KEY')
|
|
500
|
+
);
|
|
374
501
|
} finally {
|
|
375
|
-
delete process.env.ANTHROPIC_API_KEY;
|
|
376
502
|
safeRmDir(workspaceDir);
|
|
377
503
|
}
|
|
378
504
|
});
|
|
@@ -14,7 +14,7 @@ describe('Correction Observer Ownership — Feature Flag & Surface Registry Cons
|
|
|
14
14
|
const result = computeEffectiveFlags(
|
|
15
15
|
{ correction_observer: { enabled: false } },
|
|
16
16
|
DEFAULT_FEATURE_FLAGS,
|
|
17
|
-
'.pd/
|
|
17
|
+
'.pd/config.yaml',
|
|
18
18
|
);
|
|
19
19
|
expect(result.flags['correction_observer'].enabled).toBe(false);
|
|
20
20
|
expect(result.warnings).not.toContain(expect.stringContaining('core flag cannot be disabled'));
|