@principles/core 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.
Files changed (55) hide show
  1. package/dist/runtime-v2/__tests__/architecture-regression.test.js +39 -0
  2. package/dist/runtime-v2/__tests__/architecture-regression.test.js.map +1 -1
  3. package/dist/runtime-v2/__tests__/scribe-runner-vslice.test.js +425 -2
  4. package/dist/runtime-v2/__tests__/scribe-runner-vslice.test.js.map +1 -1
  5. package/dist/runtime-v2/config/__tests__/pd-config-contract.test.d.ts +15 -0
  6. package/dist/runtime-v2/config/__tests__/pd-config-contract.test.d.ts.map +1 -0
  7. package/dist/runtime-v2/config/__tests__/pd-config-contract.test.js +685 -0
  8. package/dist/runtime-v2/config/__tests__/pd-config-contract.test.js.map +1 -0
  9. package/dist/runtime-v2/config/index.d.ts +14 -0
  10. package/dist/runtime-v2/config/index.d.ts.map +1 -0
  11. package/dist/runtime-v2/config/index.js +16 -0
  12. package/dist/runtime-v2/config/index.js.map +1 -0
  13. package/dist/runtime-v2/config/pd-config-defaults.d.ts +14 -0
  14. package/dist/runtime-v2/config/pd-config-defaults.d.ts.map +1 -0
  15. package/dist/runtime-v2/config/pd-config-defaults.js +74 -0
  16. package/dist/runtime-v2/config/pd-config-defaults.js.map +1 -0
  17. package/dist/runtime-v2/config/pd-config-effective.d.ts +14 -0
  18. package/dist/runtime-v2/config/pd-config-effective.d.ts.map +1 -0
  19. package/dist/runtime-v2/config/pd-config-effective.js +118 -0
  20. package/dist/runtime-v2/config/pd-config-effective.js.map +1 -0
  21. package/dist/runtime-v2/config/pd-config-feature-flags.d.ts +35 -0
  22. package/dist/runtime-v2/config/pd-config-feature-flags.d.ts.map +1 -0
  23. package/dist/runtime-v2/config/pd-config-feature-flags.js +99 -0
  24. package/dist/runtime-v2/config/pd-config-feature-flags.js.map +1 -0
  25. package/dist/runtime-v2/config/pd-config-redaction.d.ts +22 -0
  26. package/dist/runtime-v2/config/pd-config-redaction.d.ts.map +1 -0
  27. package/dist/runtime-v2/config/pd-config-redaction.js +179 -0
  28. package/dist/runtime-v2/config/pd-config-redaction.js.map +1 -0
  29. package/dist/runtime-v2/config/pd-config-types.d.ts +124 -0
  30. package/dist/runtime-v2/config/pd-config-types.d.ts.map +1 -0
  31. package/dist/runtime-v2/config/pd-config-types.js +35 -0
  32. package/dist/runtime-v2/config/pd-config-types.js.map +1 -0
  33. package/dist/runtime-v2/config/pd-config-validate.d.ts +16 -0
  34. package/dist/runtime-v2/config/pd-config-validate.d.ts.map +1 -0
  35. package/dist/runtime-v2/config/pd-config-validate.js +443 -0
  36. package/dist/runtime-v2/config/pd-config-validate.js.map +1 -0
  37. package/dist/runtime-v2/index.d.ts +2 -0
  38. package/dist/runtime-v2/index.d.ts.map +1 -1
  39. package/dist/runtime-v2/index.js +6 -0
  40. package/dist/runtime-v2/index.js.map +1 -1
  41. package/dist/runtime-v2/internalization/scribe-output.d.ts +8 -2
  42. package/dist/runtime-v2/internalization/scribe-output.d.ts.map +1 -1
  43. package/dist/runtime-v2/internalization/scribe-output.js +33 -17
  44. package/dist/runtime-v2/internalization/scribe-output.js.map +1 -1
  45. package/dist/runtime-v2/internalization/scribe-runner.d.ts +56 -49
  46. package/dist/runtime-v2/internalization/scribe-runner.d.ts.map +1 -1
  47. package/dist/runtime-v2/internalization/scribe-runner.js +116 -398
  48. package/dist/runtime-v2/internalization/scribe-runner.js.map +1 -1
  49. package/dist/runtime-v2/runner/__tests__/base-peer-runner-trust-boundary.test.js +119 -0
  50. package/dist/runtime-v2/runner/__tests__/base-peer-runner-trust-boundary.test.js.map +1 -1
  51. package/dist/runtime-v2/runner/base-peer-runner.d.ts +1 -1
  52. package/dist/runtime-v2/runner/base-peer-runner.d.ts.map +1 -1
  53. package/dist/runtime-v2/runner/base-peer-runner.js +8 -0
  54. package/dist/runtime-v2/runner/base-peer-runner.js.map +1 -1
  55. package/package.json +1 -1
@@ -0,0 +1,685 @@
1
+ /**
2
+ * PD Config Contract Tests — PRI-304
3
+ *
4
+ * 8 required test scenarios:
5
+ * 1. Missing config → deterministic defaults
6
+ * 2. Valid MVP config → effective config correct
7
+ * 3. Malformed root/object/array/value → structured error
8
+ * 4. OpenClaw runtime reference → summary shows safe label/id only
9
+ * 5. PD-local profile → shows apiKeyEnv, not secret value
10
+ * 6. Per-agent override beats default runtime
11
+ * 7. Feature flags computed from new config contract
12
+ * 8. Redaction does not leak token-like/key-like/raw provider data
13
+ */
14
+ import { describe, it, expect } from 'vitest';
15
+ import { validatePdConfig, computeEffectivePdConfig, redactPdConfig, redactConfigValue, computeFeatureFlagsFromConfig, isFeatureEnabled, getDefaultPdConfig, DEFAULT_RUNTIME_PROFILE_ID, PD_CONFIG_VERSION, INTERNAL_AGENT_NAMES, } from '../index.js';
16
+ // ── Helpers ─────────────────────────────────────────────────────────────────
17
+ /** Non-null assert for Record<string, T> lookups in tests */
18
+ function nn(value, msg) {
19
+ if (value === undefined)
20
+ throw new Error(msg ?? 'Expected non-undefined');
21
+ return value;
22
+ }
23
+ function makeValidConfig() {
24
+ return {
25
+ version: 1,
26
+ features: {
27
+ prompt: { category: 'core', enabled: true },
28
+ code_tool_hook: { category: 'core', enabled: true },
29
+ defer_archive: { category: 'core', enabled: true },
30
+ correction_observer: { category: 'quiet', enabled: true },
31
+ gfi: { category: 'quiet', enabled: false },
32
+ nocturnal: { category: 'gone', enabled: false },
33
+ },
34
+ runtimeProfiles: {
35
+ 'openclaw.default': { type: 'openclaw', source: 'default' },
36
+ 'openclaw.model.lmstudio.qwen3': { type: 'openclaw', provider: 'lmstudio', model: 'qwen3.6-27b-mtp' },
37
+ 'pd.anthropic-sonnet': { type: 'pi-ai', provider: 'anthropic', model: 'claude-3-5-sonnet', apiKeyEnv: 'ANTHROPIC_API_KEY', timeoutMs: 300000 },
38
+ },
39
+ internalAgents: {
40
+ defaultRuntime: 'openclaw.default',
41
+ agents: {
42
+ diagnostician: { enabled: true, runtimeProfile: 'openclaw.model.lmstudio.qwen3' },
43
+ dreamer: { enabled: true },
44
+ scribe: { enabled: true },
45
+ artificer: { enabled: true },
46
+ philosopher: { enabled: false },
47
+ evaluator: { enabled: false },
48
+ rolloutReviewer: { enabled: false },
49
+ trainer: { enabled: false },
50
+ correctionObserver: { enabled: false },
51
+ empathyObserver: { enabled: false },
52
+ },
53
+ },
54
+ ui: { diagnostics: { mode: 'simple' } },
55
+ };
56
+ }
57
+ // ── Scenario 1: Missing config → deterministic defaults ────────────────────
58
+ describe('Scenario 1: Missing config → deterministic defaults', () => {
59
+ it('null input returns defaults', () => {
60
+ const effective = computeEffectivePdConfig(null);
61
+ expect(effective.source).toBe('defaults');
62
+ expect(effective.config.version).toBe(PD_CONFIG_VERSION);
63
+ expect(effective.warnings).toEqual([]);
64
+ });
65
+ it('undefined input returns defaults', () => {
66
+ const effective = computeEffectivePdConfig(undefined);
67
+ expect(effective.source).toBe('defaults');
68
+ expect(effective.config.version).toBe(PD_CONFIG_VERSION);
69
+ });
70
+ it('defaults include all MVP core features enabled', () => {
71
+ const effective = computeEffectivePdConfig(null);
72
+ const { config } = effective;
73
+ expect(nn(config.features.prompt).enabled).toBe(true);
74
+ expect(nn(config.features.code_tool_hook).enabled).toBe(true);
75
+ expect(nn(config.features.defer_archive).enabled).toBe(true);
76
+ expect(nn(config.features.prompt).category).toBe('core');
77
+ expect(nn(config.features.code_tool_hook).category).toBe('core');
78
+ expect(nn(config.features.defer_archive).category).toBe('core');
79
+ });
80
+ it('defaults include gone features disabled', () => {
81
+ const effective = computeEffectivePdConfig(null);
82
+ expect(nn(effective.config.features.nocturnal).enabled).toBe(false);
83
+ expect(nn(effective.config.features.nocturnal).category).toBe('gone');
84
+ expect(nn(effective.config.features.model_training).enabled).toBe(false);
85
+ expect(nn(effective.config.features.trainer).enabled).toBe(false);
86
+ });
87
+ it('defaults include openclaw.default runtime profile', () => {
88
+ const effective = computeEffectivePdConfig(null);
89
+ expect(Object.hasOwn(effective.config.runtimeProfiles, DEFAULT_RUNTIME_PROFILE_ID)).toBe(true);
90
+ expect(nn(effective.config.runtimeProfiles[DEFAULT_RUNTIME_PROFILE_ID]).type).toBe('openclaw');
91
+ });
92
+ it('defaults include all internal agents', () => {
93
+ const effective = computeEffectivePdConfig(null);
94
+ for (const name of INTERNAL_AGENT_NAMES) {
95
+ expect(Object.hasOwn(effective.config.internalAgents.agents, name)).toBe(true);
96
+ }
97
+ });
98
+ it('defaults are deterministic (same result each call)', () => {
99
+ const a = getDefaultPdConfig();
100
+ const b = getDefaultPdConfig();
101
+ expect(JSON.stringify(a)).toBe(JSON.stringify(b));
102
+ });
103
+ });
104
+ // ── Scenario 2: Valid MVP config → effective config correct ────────────────
105
+ describe('Scenario 2: Valid MVP config → effective config correct', () => {
106
+ it('validates a valid config', () => {
107
+ const raw = makeValidConfig();
108
+ const result = validatePdConfig(raw);
109
+ expect(result.ok).toBe(true);
110
+ if (!result.ok)
111
+ throw new Error('Expected ok');
112
+ expect(result.value.version).toBe(1);
113
+ });
114
+ it('effective config preserves user features', () => {
115
+ const raw = makeValidConfig();
116
+ const result = validatePdConfig(raw);
117
+ if (!result.ok)
118
+ throw new Error('Expected ok');
119
+ const effective = computeEffectivePdConfig(result.value);
120
+ expect(effective.source).toBe('user_config');
121
+ expect(nn(effective.config.features.prompt).enabled).toBe(true);
122
+ expect(nn(effective.config.features.gfi).enabled).toBe(false);
123
+ });
124
+ it('effective config preserves runtime profiles', () => {
125
+ const raw = makeValidConfig();
126
+ const result = validatePdConfig(raw);
127
+ if (!result.ok)
128
+ throw new Error('Expected ok');
129
+ const effective = computeEffectivePdConfig(result.value);
130
+ expect(Object.hasOwn(effective.config.runtimeProfiles, 'openclaw.default')).toBe(true);
131
+ expect(Object.hasOwn(effective.config.runtimeProfiles, 'pd.anthropic-sonnet')).toBe(true);
132
+ });
133
+ it('effective config preserves internal agent bindings', () => {
134
+ const raw = makeValidConfig();
135
+ const result = validatePdConfig(raw);
136
+ if (!result.ok)
137
+ throw new Error('Expected ok');
138
+ const effective = computeEffectivePdConfig(result.value);
139
+ const diag = nn(effective.config.internalAgents.agents.diagnostician);
140
+ expect(diag.enabled).toBe(true);
141
+ expect(diag.runtimeProfile).toBe('openclaw.model.lmstudio.qwen3');
142
+ });
143
+ });
144
+ // ── Scenario 3: Malformed root/object/array/value → structured error ───────
145
+ describe('Scenario 3: Malformed config → structured error', () => {
146
+ it('null root returns structured error', () => {
147
+ const result = validatePdConfig(null);
148
+ expect(result.ok).toBe(false);
149
+ if (result.ok)
150
+ throw new Error('Expected error');
151
+ expect(result.errors.length).toBeGreaterThan(0);
152
+ expect(nn(result.errors[0]).reason).toBeTruthy();
153
+ expect(nn(result.errors[0]).nextAction).toBeTruthy();
154
+ });
155
+ it('string root returns structured error', () => {
156
+ const result = validatePdConfig('not an object');
157
+ expect(result.ok).toBe(false);
158
+ if (result.ok)
159
+ throw new Error('Expected error');
160
+ expect(result.errors.some(e => e.path === '')).toBe(true);
161
+ });
162
+ it('array root returns structured error', () => {
163
+ const result = validatePdConfig([]);
164
+ expect(result.ok).toBe(false);
165
+ if (result.ok)
166
+ throw new Error('Expected error');
167
+ expect(result.errors.some(e => e.path === '')).toBe(true);
168
+ });
169
+ it('missing version returns structured error', () => {
170
+ const raw = { ...makeValidConfig() };
171
+ delete raw.version;
172
+ const result = validatePdConfig(raw);
173
+ expect(result.ok).toBe(false);
174
+ if (result.ok)
175
+ throw new Error('Expected error');
176
+ expect(result.errors.some(e => e.path === 'version')).toBe(true);
177
+ });
178
+ it('wrong version returns structured error', () => {
179
+ const raw = { ...makeValidConfig(), version: 99 };
180
+ const result = validatePdConfig(raw);
181
+ expect(result.ok).toBe(false);
182
+ if (result.ok)
183
+ throw new Error('Expected error');
184
+ expect(result.errors.some(e => e.path === 'version')).toBe(true);
185
+ });
186
+ it('non-boolean enabled returns structured error', () => {
187
+ const raw = makeValidConfig();
188
+ raw.features.prompt = { category: 'core', enabled: 'true' };
189
+ const result = validatePdConfig(raw);
190
+ expect(result.ok).toBe(false);
191
+ if (result.ok)
192
+ throw new Error('Expected error');
193
+ expect(result.errors.some(e => e.path === 'features.prompt.enabled')).toBe(true);
194
+ });
195
+ it('missing features returns structured error', () => {
196
+ const raw = { ...makeValidConfig() };
197
+ delete raw.features;
198
+ const result = validatePdConfig(raw);
199
+ expect(result.ok).toBe(false);
200
+ if (result.ok)
201
+ throw new Error('Expected error');
202
+ expect(result.errors.some(e => e.path === 'features')).toBe(true);
203
+ });
204
+ it('pi-ai profile missing apiKeyEnv returns structured error', () => {
205
+ const raw = makeValidConfig();
206
+ raw.runtimeProfiles['pd.test'] = { type: 'pi-ai', provider: 'test', model: 'test-model' };
207
+ const result = validatePdConfig(raw);
208
+ expect(result.ok).toBe(false);
209
+ if (result.ok)
210
+ throw new Error('Expected error');
211
+ expect(result.errors.some(e => e.path.includes('apiKeyEnv'))).toBe(true);
212
+ });
213
+ it('forbidden secret field in openclaw profile returns structured error', () => {
214
+ const raw = makeValidConfig();
215
+ raw.runtimeProfiles['bad.profile'] = { type: 'openclaw', apiKey: 'sk-1234567890abcdef' };
216
+ const result = validatePdConfig(raw);
217
+ expect(result.ok).toBe(false);
218
+ if (result.ok)
219
+ throw new Error('Expected error');
220
+ expect(result.errors.some(e => e.reason.includes('forbidden secret field'))).toBe(true);
221
+ });
222
+ it('forbidden secret field in pi-ai profile returns structured error', () => {
223
+ const raw = makeValidConfig();
224
+ raw.runtimeProfiles['bad.pd'] = { type: 'pi-ai', provider: 'test', model: 'test', apiKeyEnv: 'TEST_KEY', token: 'secret-value' };
225
+ const result = validatePdConfig(raw);
226
+ expect(result.ok).toBe(false);
227
+ if (result.ok)
228
+ throw new Error('Expected error');
229
+ expect(result.errors.some(e => e.reason.includes('forbidden secret field'))).toBe(true);
230
+ });
231
+ it('gateway_token in pi-ai profile is rejected as forbidden secret field', () => {
232
+ const raw = makeValidConfig();
233
+ raw.runtimeProfiles['bad.gw'] = { type: 'pi-ai', provider: 'test', model: 'test', apiKeyEnv: 'TEST_KEY', gateway_token: 'gw-secret' };
234
+ const result = validatePdConfig(raw);
235
+ expect(result.ok).toBe(false);
236
+ if (result.ok)
237
+ throw new Error('Expected error');
238
+ expect(result.errors.some(e => e.reason.includes('forbidden secret field') && e.path.includes('gateway_token'))).toBe(true);
239
+ });
240
+ it('dangerous key at root returns structured error', () => {
241
+ // Use JSON.parse to create an object with 'constructor' as an own property
242
+ // (spread/Object.assign cannot set __proto__ as an own enumerable property)
243
+ const raw = JSON.parse('{"constructor":"evil","version":1,"features":{"prompt":{"category":"core","enabled":true}},"runtimeProfiles":{"openclaw.default":{"type":"openclaw","source":"default"}},"internalAgents":{"defaultRuntime":"openclaw.default","agents":{}}}');
244
+ const result = validatePdConfig(raw);
245
+ expect(result.ok).toBe(false);
246
+ if (result.ok)
247
+ throw new Error('Expected error');
248
+ expect(result.errors.some(e => e.reason.includes('dangerous key'))).toBe(true);
249
+ });
250
+ it('each error has reason and nextAction', () => {
251
+ const result = validatePdConfig('bad');
252
+ expect(result.ok).toBe(false);
253
+ if (result.ok)
254
+ throw new Error('Expected error');
255
+ for (const error of result.errors) {
256
+ expect(error.reason.length).toBeGreaterThan(0);
257
+ expect(error.nextAction.length).toBeGreaterThan(0);
258
+ }
259
+ });
260
+ });
261
+ // ── Scenario 4: OpenClaw runtime reference → summary shows safe label/id only ──
262
+ describe('Scenario 4: OpenClaw runtime reference → safe summary', () => {
263
+ it('OpenClaw profile summary shows label without secrets', () => {
264
+ const raw = makeValidConfig();
265
+ const result = validatePdConfig(raw);
266
+ if (!result.ok)
267
+ throw new Error('Expected ok');
268
+ const effective = computeEffectivePdConfig(result.value);
269
+ const summary = redactPdConfig(effective);
270
+ const ocProfile = summary.runtimeProfiles.find(p => p.id === 'openclaw.model.lmstudio.qwen3');
271
+ expect(ocProfile).toBeDefined();
272
+ expect(nn(ocProfile).type).toBe('openclaw');
273
+ expect(nn(ocProfile).label).toContain('openclaw');
274
+ expect(nn(ocProfile).label).toContain('lmstudio');
275
+ expect(nn(ocProfile).label).toContain('qwen3.6-27b-mtp');
276
+ // No apiKeyEnv for openclaw profiles
277
+ expect(nn(ocProfile).apiKeyEnv).toBeUndefined();
278
+ });
279
+ it('OpenClaw default profile shows source label', () => {
280
+ const raw = makeValidConfig();
281
+ const result = validatePdConfig(raw);
282
+ if (!result.ok)
283
+ throw new Error('Expected ok');
284
+ const effective = computeEffectivePdConfig(result.value);
285
+ const summary = redactPdConfig(effective);
286
+ const defaultProfile = summary.runtimeProfiles.find(p => p.id === 'openclaw.default');
287
+ expect(defaultProfile).toBeDefined();
288
+ expect(nn(defaultProfile).label).toContain('openclaw');
289
+ });
290
+ it('OpenClaw profile summary does not contain raw provider object', () => {
291
+ const raw = makeValidConfig();
292
+ const result = validatePdConfig(raw);
293
+ if (!result.ok)
294
+ throw new Error('Expected ok');
295
+ const effective = computeEffectivePdConfig(result.value);
296
+ const summary = redactPdConfig(effective);
297
+ const summaryStr = JSON.stringify(summary);
298
+ // Should not contain raw provider config objects (apiKey as a field name, not apiKeyEnv)
299
+ expect(summaryStr).not.toContain('"apiKey"');
300
+ expect(summaryStr).not.toContain('"gatewayToken"');
301
+ expect(summaryStr).not.toContain('"baseUrl"');
302
+ });
303
+ });
304
+ // ── Scenario 5: PD-local profile → shows apiKeyEnv, not secret value ───────
305
+ describe('Scenario 5: PD-local profile → apiKeyEnv shown, not value', () => {
306
+ it('pi-ai profile summary shows apiKeyEnv name', () => {
307
+ const raw = makeValidConfig();
308
+ const result = validatePdConfig(raw);
309
+ if (!result.ok)
310
+ throw new Error('Expected ok');
311
+ const effective = computeEffectivePdConfig(result.value);
312
+ const summary = redactPdConfig(effective);
313
+ const pdProfile = summary.runtimeProfiles.find(p => p.id === 'pd.anthropic-sonnet');
314
+ expect(pdProfile).toBeDefined();
315
+ expect(nn(pdProfile).apiKeyEnv).toBe('ANTHROPIC_API_KEY');
316
+ });
317
+ it('pi-ai profile summary does not contain secret value', () => {
318
+ const raw = makeValidConfig();
319
+ const result = validatePdConfig(raw);
320
+ if (!result.ok)
321
+ throw new Error('Expected ok');
322
+ const effective = computeEffectivePdConfig(result.value);
323
+ const summary = redactPdConfig(effective);
324
+ const summaryStr = JSON.stringify(summary);
325
+ // Should not contain actual API key values
326
+ expect(summaryStr).not.toContain('sk-ant-');
327
+ expect(summaryStr).not.toContain('sk-');
328
+ });
329
+ it('pi-ai profile label shows provider/model', () => {
330
+ const raw = makeValidConfig();
331
+ const result = validatePdConfig(raw);
332
+ if (!result.ok)
333
+ throw new Error('Expected ok');
334
+ const effective = computeEffectivePdConfig(result.value);
335
+ const summary = redactPdConfig(effective);
336
+ const pdProfile = summary.runtimeProfiles.find(p => p.id === 'pd.anthropic-sonnet');
337
+ expect(nn(pdProfile).label).toContain('pi-ai');
338
+ expect(nn(pdProfile).label).toContain('anthropic');
339
+ expect(nn(pdProfile).label).toContain('claude-3-5-sonnet');
340
+ });
341
+ });
342
+ // ── Scenario 6: Per-agent override beats default runtime ───────────────────
343
+ describe('Scenario 6: Per-agent override beats default runtime', () => {
344
+ it('diagnostician uses explicit override, not defaultRuntime', () => {
345
+ const raw = makeValidConfig();
346
+ const result = validatePdConfig(raw);
347
+ if (!result.ok)
348
+ throw new Error('Expected ok');
349
+ const effective = computeEffectivePdConfig(result.value);
350
+ // diagnostician has explicit runtimeProfile: 'openclaw.model.lmstudio.qwen3'
351
+ expect(nn(effective.config.internalAgents.agents.diagnostician).runtimeProfile).toBe('openclaw.model.lmstudio.qwen3');
352
+ // defaultRuntime is 'openclaw.default'
353
+ expect(effective.config.internalAgents.defaultRuntime).toBe('openclaw.default');
354
+ });
355
+ it('agent without override uses defaultRuntime', () => {
356
+ const raw = makeValidConfig();
357
+ const result = validatePdConfig(raw);
358
+ if (!result.ok)
359
+ throw new Error('Expected ok');
360
+ const effective = computeEffectivePdConfig(result.value);
361
+ // dreamer has no explicit runtimeProfile, so it should use defaultRuntime
362
+ expect(nn(effective.config.internalAgents.agents.dreamer).runtimeProfile).toBe('openclaw.default');
363
+ });
364
+ it('agent without override uses user-configured defaultRuntime, not hard-coded default', () => {
365
+ const raw = makeValidConfig();
366
+ // User configures a custom defaultRuntime
367
+ raw.internalAgents.defaultRuntime = 'pd.anthropic-sonnet';
368
+ const result = validatePdConfig(raw);
369
+ if (!result.ok)
370
+ throw new Error('Expected ok');
371
+ const effective = computeEffectivePdConfig(result.value);
372
+ // dreamer has no explicit runtimeProfile, so it should use the user's defaultRuntime
373
+ expect(nn(effective.config.internalAgents.agents.dreamer).runtimeProfile).toBe('pd.anthropic-sonnet');
374
+ // scribe also has no explicit runtimeProfile
375
+ expect(nn(effective.config.internalAgents.agents.scribe).runtimeProfile).toBe('pd.anthropic-sonnet');
376
+ // diagnostician has explicit override, so it keeps its own profile
377
+ expect(nn(effective.config.internalAgents.agents.diagnostician).runtimeProfile).toBe('openclaw.model.lmstudio.qwen3');
378
+ });
379
+ it('redacted summary reflects per-agent override', () => {
380
+ const raw = makeValidConfig();
381
+ const result = validatePdConfig(raw);
382
+ if (!result.ok)
383
+ throw new Error('Expected ok');
384
+ const effective = computeEffectivePdConfig(result.value);
385
+ const summary = redactPdConfig(effective);
386
+ const diag = summary.agents.find(a => a.name === 'diagnostician');
387
+ expect(nn(diag).runtimeProfileId).toBe('openclaw.model.lmstudio.qwen3');
388
+ expect(nn(diag).runtimeProfileLabel).toContain('lmstudio');
389
+ const dreamer = summary.agents.find(a => a.name === 'dreamer');
390
+ expect(nn(dreamer).runtimeProfileId).toBe('openclaw.default');
391
+ });
392
+ });
393
+ // ── Scenario 7: Feature flags computed from new config contract ────────────
394
+ describe('Scenario 7: Feature flags from new config contract', () => {
395
+ it('feature flags include MVP core channels enabled', () => {
396
+ const effective = computeEffectivePdConfig(null);
397
+ const flags = computeFeatureFlagsFromConfig(effective);
398
+ expect(flags.enabledChannels).toContain('prompt');
399
+ expect(flags.enabledChannels).toContain('code_tool_hook');
400
+ expect(flags.enabledChannels).toContain('defer_archive');
401
+ });
402
+ it('isFeatureEnabled works for known flags', () => {
403
+ const effective = computeEffectivePdConfig(null);
404
+ const flags = computeFeatureFlagsFromConfig(effective);
405
+ expect(isFeatureEnabled(flags, 'prompt')).toBe(true);
406
+ expect(isFeatureEnabled(flags, 'nocturnal')).toBe(false);
407
+ expect(isFeatureEnabled(flags, 'nonexistent')).toBe(false);
408
+ });
409
+ it('core flags cannot be disabled via config', () => {
410
+ const raw = makeValidConfig();
411
+ raw.features.prompt = { category: 'core', enabled: false };
412
+ const result = validatePdConfig(raw);
413
+ // Validation passes (user said disabled) but effective config overrides
414
+ if (!result.ok)
415
+ throw new Error('Expected ok');
416
+ const effective = computeEffectivePdConfig(result.value);
417
+ const flags = computeFeatureFlagsFromConfig(effective);
418
+ expect(nn(flags.flags.prompt).enabled).toBe(true);
419
+ expect(effective.warnings.some(w => w.includes('core flag cannot be disabled'))).toBe(true);
420
+ });
421
+ it('gone flags cannot be re-enabled via config', () => {
422
+ const raw = makeValidConfig();
423
+ raw.features.nocturnal = { category: 'gone', enabled: true };
424
+ const result = validatePdConfig(raw);
425
+ if (!result.ok)
426
+ throw new Error('Expected ok');
427
+ const effective = computeEffectivePdConfig(result.value);
428
+ const flags = computeFeatureFlagsFromConfig(effective);
429
+ expect(nn(flags.flags.nocturnal).enabled).toBe(false);
430
+ expect(effective.warnings.some(w => w.includes('gone flag cannot be re-enabled'))).toBe(true);
431
+ });
432
+ it('quiet flags can be toggled', () => {
433
+ const raw = makeValidConfig();
434
+ raw.features.gfi = { category: 'quiet', enabled: true };
435
+ const result = validatePdConfig(raw);
436
+ if (!result.ok)
437
+ throw new Error('Expected ok');
438
+ const effective = computeEffectivePdConfig(result.value);
439
+ const flags = computeFeatureFlagsFromConfig(effective);
440
+ expect(nn(flags.flags.gfi).enabled).toBe(true);
441
+ });
442
+ it('unknown flags are accepted with warning', () => {
443
+ const raw = makeValidConfig();
444
+ raw.features.custom_flag = { category: 'quiet', enabled: true };
445
+ const result = validatePdConfig(raw);
446
+ if (!result.ok)
447
+ throw new Error('Expected ok');
448
+ const effective = computeEffectivePdConfig(result.value);
449
+ const flags = computeFeatureFlagsFromConfig(effective);
450
+ expect(nn(flags.flags.custom_flag).enabled).toBe(true);
451
+ expect(flags.warnings.some(w => w.includes('unknown flag'))).toBe(true);
452
+ });
453
+ });
454
+ // ── Scenario 8: Redaction does not leak secrets ────────────────────────────
455
+ describe('Scenario 8: Redaction does not leak secrets', () => {
456
+ it('redactConfigValue redacts sensitive keys', () => {
457
+ expect(redactConfigValue('sk-ant-secret123456789', 'apiKey')).toBe('[REDACTED]');
458
+ expect(redactConfigValue('secret-value', 'token')).toBe('[REDACTED]');
459
+ expect(redactConfigValue('secret-value', 'password')).toBe('[REDACTED]');
460
+ expect(redactConfigValue('secret-value', 'auth_token')).toBe('[REDACTED]');
461
+ });
462
+ it('redactConfigValue preserves non-sensitive keys', () => {
463
+ expect(redactConfigValue('hello', 'name')).toBe('hello');
464
+ expect(redactConfigValue(42, 'count')).toBe(42);
465
+ expect(redactConfigValue(true, 'enabled')).toBe(true);
466
+ });
467
+ it('redactConfigValue redacts token-like values in strings', () => {
468
+ const result = redactConfigValue('key=sk-ant-1234567890abcdef', 'description');
469
+ expect(result).not.toContain('sk-ant-');
470
+ expect(result).toContain('[REDACTED]');
471
+ });
472
+ it('redactConfigValue redacts Bearer tokens in strings', () => {
473
+ const result = redactConfigValue('Authorization: Bearer abc123def456ghi789', 'header');
474
+ expect(result).not.toContain('abc123def456ghi789');
475
+ });
476
+ it('redactConfigValue handles nested objects with sensitive keys', () => {
477
+ const input = {
478
+ provider: 'anthropic',
479
+ apiKey: 'sk-ant-super-secret-key',
480
+ model: 'claude-3-5-sonnet',
481
+ };
482
+ const result = redactConfigValue(input);
483
+ expect(result.provider).toBe('anthropic');
484
+ expect(result.apiKey).toBe('[REDACTED]');
485
+ expect(result.model).toBe('claude-3-5-sonnet');
486
+ });
487
+ it('redacted summary never contains raw provider secrets', () => {
488
+ const raw = makeValidConfig();
489
+ const result = validatePdConfig(raw);
490
+ if (!result.ok)
491
+ throw new Error('Expected ok');
492
+ const effective = computeEffectivePdConfig(result.value);
493
+ const summary = redactPdConfig(effective);
494
+ const summaryStr = JSON.stringify(summary);
495
+ // No raw key values
496
+ expect(summaryStr).not.toContain('sk-ant-');
497
+ expect(summaryStr).not.toContain('sk-');
498
+ // No raw provider objects (baseUrl, apiKey, etc.)
499
+ expect(summaryStr).not.toContain('"apiKey"');
500
+ expect(summaryStr).not.toContain('"baseUrl"');
501
+ expect(summaryStr).not.toContain('"gatewayToken"');
502
+ });
503
+ it('redacted summary shows apiKeyEnv name but not value', () => {
504
+ const raw = makeValidConfig();
505
+ const result = validatePdConfig(raw);
506
+ if (!result.ok)
507
+ throw new Error('Expected ok');
508
+ const effective = computeEffectivePdConfig(result.value);
509
+ const summary = redactPdConfig(effective);
510
+ const pdProfile = summary.runtimeProfiles.find(p => p.id === 'pd.anthropic-sonnet');
511
+ expect(nn(pdProfile).apiKeyEnv).toBe('ANTHROPIC_API_KEY');
512
+ // The summary should NOT contain any actual key value
513
+ const summaryStr = JSON.stringify(summary);
514
+ expect(summaryStr).not.toContain('sk-ant-api03-');
515
+ });
516
+ it('redactConfigValue truncates long strings', () => {
517
+ const longValue = 'a'.repeat(300);
518
+ const result = redactConfigValue(longValue, 'description');
519
+ expect(typeof result === 'string' && result.length <= 203).toBe(true); // 200 + '…'
520
+ });
521
+ it('redactConfigValue handles dangerous keys', () => {
522
+ const input = { __proto__: 'evil', constructor: 'bad', normal: 'ok' };
523
+ const result = redactConfigValue(input);
524
+ expect(Object.hasOwn(result, '__proto__')).toBe(false);
525
+ expect(Object.hasOwn(result, 'constructor')).toBe(false);
526
+ expect(result.normal).toBe('ok');
527
+ });
528
+ });
529
+ // ── Additional edge cases ───────────────────────────────────────────────────
530
+ describe('Edge cases', () => {
531
+ it('empty features object passes validation but effective config fills defaults', () => {
532
+ const raw = makeValidConfig();
533
+ raw.features = {};
534
+ const result = validatePdConfig(raw);
535
+ if (!result.ok)
536
+ throw new Error('Expected ok');
537
+ const effective = computeEffectivePdConfig(result.value);
538
+ // Defaults should fill in
539
+ expect(nn(effective.config.features.prompt).enabled).toBe(true);
540
+ });
541
+ it('negative timeoutMs fails validation', () => {
542
+ const raw = makeValidConfig();
543
+ raw.runtimeProfiles['pd.bad-timeout'] = {
544
+ type: 'pi-ai',
545
+ provider: 'test',
546
+ model: 'test',
547
+ apiKeyEnv: 'TEST_KEY',
548
+ timeoutMs: -1,
549
+ };
550
+ const result = validatePdConfig(raw);
551
+ expect(result.ok).toBe(false);
552
+ if (result.ok)
553
+ throw new Error('Expected error');
554
+ expect(result.errors.some(e => e.path.includes('timeoutMs'))).toBe(true);
555
+ });
556
+ it('zero timeoutMs fails validation', () => {
557
+ const raw = makeValidConfig();
558
+ raw.runtimeProfiles['pd.zero-timeout'] = {
559
+ type: 'pi-ai',
560
+ provider: 'test',
561
+ model: 'test',
562
+ apiKeyEnv: 'TEST_KEY',
563
+ timeoutMs: 0,
564
+ };
565
+ const result = validatePdConfig(raw);
566
+ expect(result.ok).toBe(false);
567
+ });
568
+ it('NaN timeoutMs fails validation', () => {
569
+ const raw = makeValidConfig();
570
+ raw.runtimeProfiles['pd.nan-timeout'] = {
571
+ type: 'pi-ai',
572
+ provider: 'test',
573
+ model: 'test',
574
+ apiKeyEnv: 'TEST_KEY',
575
+ timeoutMs: NaN,
576
+ };
577
+ const result = validatePdConfig(raw);
578
+ expect(result.ok).toBe(false);
579
+ });
580
+ it('Infinity timeoutMs fails validation', () => {
581
+ const raw = makeValidConfig();
582
+ raw.runtimeProfiles['pd.inf-timeout'] = {
583
+ type: 'pi-ai',
584
+ provider: 'test',
585
+ model: 'test',
586
+ apiKeyEnv: 'TEST_KEY',
587
+ timeoutMs: Infinity,
588
+ };
589
+ const result = validatePdConfig(raw);
590
+ expect(result.ok).toBe(false);
591
+ });
592
+ it('unknown profile type fails validation', () => {
593
+ const raw = makeValidConfig();
594
+ raw.runtimeProfiles['bad.type'] = { type: 'unknown' };
595
+ const result = validatePdConfig(raw);
596
+ expect(result.ok).toBe(false);
597
+ if (result.ok)
598
+ throw new Error('Expected error');
599
+ expect(result.errors.some(e => e.reason.includes('type must be one of'))).toBe(true);
600
+ });
601
+ it('unknown agent key in internalAgents produces error', () => {
602
+ const raw = { ...makeValidConfig() };
603
+ const rawObj = raw;
604
+ const ia = rawObj.internalAgents;
605
+ const agentsObj = { ...ia.agents, unknownAgent: { enabled: true } };
606
+ ia.agents = agentsObj;
607
+ const result = validatePdConfig(raw);
608
+ expect(result.ok).toBe(false);
609
+ if (result.ok)
610
+ throw new Error('Expected error');
611
+ expect(result.errors.some(e => e.reason.includes('unknown agent key'))).toBe(true);
612
+ });
613
+ it('invalid diagnostics mode fails validation', () => {
614
+ const raw = makeValidConfig();
615
+ raw.ui = { diagnostics: { mode: 'expert' } };
616
+ const result = validatePdConfig(raw);
617
+ expect(result.ok).toBe(false);
618
+ if (result.ok)
619
+ throw new Error('Expected error');
620
+ expect(result.errors.some(e => e.path.includes('mode'))).toBe(true);
621
+ });
622
+ it('missing ui section is ok (defaults applied)', () => {
623
+ const raw = { ...makeValidConfig() };
624
+ delete raw.ui;
625
+ const result = validatePdConfig(raw);
626
+ expect(result.ok).toBe(true);
627
+ if (!result.ok)
628
+ throw new Error('Expected ok');
629
+ const effective = computeEffectivePdConfig(result.value);
630
+ expect(effective.config.ui.diagnostics.mode).toBe('simple');
631
+ });
632
+ it('agent referencing non-existent profile gets warning', () => {
633
+ const raw = makeValidConfig();
634
+ raw.internalAgents.agents.diagnostician = { enabled: true, runtimeProfile: 'nonexistent.profile' };
635
+ const result = validatePdConfig(raw);
636
+ if (!result.ok)
637
+ throw new Error('Expected ok');
638
+ const effective = computeEffectivePdConfig(result.value);
639
+ expect(effective.warnings.some(w => w.includes('not found'))).toBe(true);
640
+ });
641
+ it('openclaw profile with only source=default is ready', () => {
642
+ const raw = makeValidConfig();
643
+ const result = validatePdConfig(raw);
644
+ if (!result.ok)
645
+ throw new Error('Expected ok');
646
+ const effective = computeEffectivePdConfig(result.value);
647
+ const summary = redactPdConfig(effective);
648
+ const defaultProfile = summary.runtimeProfiles.find(p => p.id === 'openclaw.default');
649
+ expect(nn(defaultProfile).readiness).toBe('ready');
650
+ });
651
+ it('disabled agent has readiness=disabled', () => {
652
+ const raw = makeValidConfig();
653
+ const result = validatePdConfig(raw);
654
+ if (!result.ok)
655
+ throw new Error('Expected ok');
656
+ const effective = computeEffectivePdConfig(result.value);
657
+ const summary = redactPdConfig(effective);
658
+ const trainer = summary.agents.find(a => a.name === 'trainer');
659
+ expect(nn(trainer).enabled).toBe(false);
660
+ expect(nn(trainer).readiness).toBe('disabled');
661
+ });
662
+ it('pi-ai profile with all required fields has readiness=not_ready (runtime unknown)', () => {
663
+ const raw = makeValidConfig();
664
+ const result = validatePdConfig(raw);
665
+ if (!result.ok)
666
+ throw new Error('Expected ok');
667
+ const effective = computeEffectivePdConfig(result.value);
668
+ const summary = redactPdConfig(effective);
669
+ const pdProfile = summary.runtimeProfiles.find(p => p.id === 'pd.anthropic-sonnet');
670
+ // pi-ai profile is "not_ready" because runtime availability is unknown
671
+ expect(nn(pdProfile).readiness).toBe('not_ready');
672
+ });
673
+ it('openclaw profile without provider/model has readiness=needs_setup', () => {
674
+ const raw = makeValidConfig();
675
+ raw.runtimeProfiles['oc.minimal'] = { type: 'openclaw' };
676
+ const result = validatePdConfig(raw);
677
+ if (!result.ok)
678
+ throw new Error('Expected ok');
679
+ const effective = computeEffectivePdConfig(result.value);
680
+ const summary = redactPdConfig(effective);
681
+ const minimal = summary.runtimeProfiles.find(p => p.id === 'oc.minimal');
682
+ expect(nn(minimal).readiness).toBe('needs_setup');
683
+ });
684
+ });
685
+ //# sourceMappingURL=pd-config-contract.test.js.map