@principles/pd-cli 1.106.0 → 1.107.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@principles/pd-cli",
3
- "version": "1.106.0",
3
+ "version": "1.107.0",
4
4
  "description": "PD CLI — Pain recording, sample management, and evolution tasks",
5
5
  "type": "module",
6
6
  "bin": {
@@ -281,4 +281,157 @@ funnels:
281
281
  expect(typeof parsed).toBe('object');
282
282
  });
283
283
  });
284
+
285
+ // ── Boundary Condition Tests ────────────────────────────────────────────────
286
+
287
+ describe('resolveRuntimeWithOverrides', () => {
288
+ it('CLI overrides take precedence over config values', async () => {
289
+ writeConfigYaml(tmpDir, makeValidConfigYaml({ provider: 'lmstudio', model: 'local-model' }));
290
+
291
+ const { resolveRuntimeWithOverrides } = await import('../../src/services/resolve-runtime-from-pd-config.js');
292
+ const resolved = resolveRuntimeWithOverrides(tmpDir, {
293
+ provider: 'override-provider',
294
+ model: 'override-model',
295
+ }, () => 'test-key');
296
+
297
+ expect(resolved.mergedConfig).not.toBeNull();
298
+ if (resolved.mergedConfig) {
299
+ expect(resolved.mergedConfig.provider).toBe('override-provider');
300
+ expect(resolved.mergedConfig.model).toBe('override-model');
301
+ }
302
+ });
303
+
304
+ it('returns mergedConfig=null when base config is malformed', async () => {
305
+ const pdDir = path.join(tmpDir, '.pd');
306
+ fs.mkdirSync(pdDir, { recursive: true });
307
+ fs.writeFileSync(path.join(pdDir, 'config.yaml'), 'version: [invalid', 'utf8');
308
+
309
+ const { resolveRuntimeWithOverrides } = await import('../../src/services/resolve-runtime-from-pd-config.js');
310
+ const resolved = resolveRuntimeWithOverrides(tmpDir, {
311
+ provider: 'override',
312
+ }, () => 'test-key');
313
+
314
+ expect(resolved.mergedConfig).toBeNull();
315
+ });
316
+
317
+ it('preserves config values when overrides are undefined', async () => {
318
+ writeConfigYaml(tmpDir, makeValidConfigYaml({ provider: 'lmstudio', model: 'local-model' }));
319
+
320
+ const { resolveRuntimeWithOverrides } = await import('../../src/services/resolve-runtime-from-pd-config.js');
321
+ const resolved = resolveRuntimeWithOverrides(tmpDir, {
322
+ provider: undefined,
323
+ model: undefined,
324
+ }, () => 'test-key');
325
+
326
+ expect(resolved.mergedConfig).not.toBeNull();
327
+ if (resolved.mergedConfig) {
328
+ expect(resolved.mergedConfig.provider).toBe('lmstudio');
329
+ expect(resolved.mergedConfig.model).toBe('local-model');
330
+ }
331
+ });
332
+
333
+ it('merges numeric overrides correctly', async () => {
334
+ writeConfigYaml(tmpDir, makeValidConfigYaml());
335
+
336
+ const { resolveRuntimeWithOverrides } = await import('../../src/services/resolve-runtime-from-pd-config.js');
337
+ const resolved = resolveRuntimeWithOverrides(tmpDir, {
338
+ maxRetries: 5,
339
+ timeoutMs: 60000,
340
+ }, () => 'test-key');
341
+
342
+ expect(resolved.mergedConfig).not.toBeNull();
343
+ if (resolved.mergedConfig) {
344
+ expect(resolved.mergedConfig.maxRetries).toBe(5);
345
+ expect(resolved.mergedConfig.timeoutMs).toBe(60000);
346
+ }
347
+ });
348
+ });
349
+
350
+ describe('edge cases and error handling', () => {
351
+ it('handles empty workspace directory gracefully', async () => {
352
+ // No .pd directory at all
353
+ const { resolveRuntimeFromPdConfig } = await import('../../src/services/resolve-runtime-from-pd-config.js');
354
+ const resolved = resolveRuntimeFromPdConfig(tmpDir, () => 'test-key');
355
+
356
+ // Missing config should return defaults (ok=true with source='defaults')
357
+ expect(resolved.configLoadResult.ok).toBe(true);
358
+ expect(resolved.configSource).toBe('.pd/config.yaml');
359
+ });
360
+
361
+ it('handles config with missing runtimeProfiles section', async () => {
362
+ writeConfigYaml(tmpDir, {
363
+ version: 1,
364
+ features: {},
365
+ // Missing runtimeProfiles
366
+ });
367
+
368
+ const { resolveRuntimeFromPdConfig } = await import('../../src/services/resolve-runtime-from-pd-config.js');
369
+ const { isRuntimeConfigError } = await import('@principles/core/runtime-v2');
370
+ const resolved = resolveRuntimeFromPdConfig(tmpDir, () => 'test-key');
371
+
372
+ // Missing runtimeProfiles should either load with defaults or produce
373
+ // a RuntimeConfigError — never silently succeed with wrong config.
374
+ if (isRuntimeConfigError(resolved.result)) {
375
+ expect(resolved.result.reason).toBeTruthy();
376
+ expect(resolved.result.nextAction).toBeTruthy();
377
+ } else {
378
+ // If defaults were returned, configLoadResult.ok should be true
379
+ expect(resolved.configLoadResult.ok).toBe(true);
380
+ }
381
+ });
382
+
383
+ it('handles config with invalid runtime profile type', async () => {
384
+ writeConfigYaml(tmpDir, {
385
+ version: 1,
386
+ runtimeProfiles: {
387
+ 'bad-profile': {
388
+ type: 'invalid-type', // Invalid type
389
+ },
390
+ },
391
+ internalAgents: {
392
+ defaultRuntime: 'bad-profile',
393
+ agents: { diagnostician: { enabled: true, runtimeProfile: 'bad-profile' } },
394
+ },
395
+ });
396
+
397
+ const { resolveRuntimeFromPdConfig } = await import('../../src/services/resolve-runtime-from-pd-config.js');
398
+ const { isRuntimeConfigError } = await import('@principles/core/runtime-v2');
399
+ const resolved = resolveRuntimeFromPdConfig(tmpDir, () => 'test-key');
400
+
401
+ // Invalid type should be caught — verify external contract, not internal state
402
+ expect(isRuntimeConfigError(resolved.result)).toBe(true);
403
+ if (isRuntimeConfigError(resolved.result)) {
404
+ expect(resolved.result.reason).toBeTruthy();
405
+ expect(resolved.result.nextAction).toBeTruthy();
406
+ }
407
+ });
408
+
409
+ it('handles env var returning undefined for apiKeyEnv', async () => {
410
+ writeConfigYaml(tmpDir, makeValidConfigYaml());
411
+
412
+ const { resolveRuntimeFromPdConfig } = await import('../../src/services/resolve-runtime-from-pd-config.js');
413
+ const resolved = resolveRuntimeFromPdConfig(tmpDir, () => undefined);
414
+
415
+ // Should handle undefined env var gracefully
416
+ expect(resolved.result).toBeDefined();
417
+ });
418
+
419
+ it('handles multiple legacy files detected', async () => {
420
+ writeConfigYaml(tmpDir, makeValidConfigYaml());
421
+
422
+ // Create multiple legacy files
423
+ const stateDir = path.join(tmpDir, '.state');
424
+ fs.mkdirSync(stateDir, { recursive: true });
425
+ fs.writeFileSync(path.join(stateDir, 'workflows.yaml'), 'version: 1', 'utf8');
426
+
427
+ const ffDir = path.join(tmpDir, '.pd');
428
+ fs.writeFileSync(path.join(ffDir, 'feature-flags.yaml'), 'flags: []', 'utf8');
429
+
430
+ const { resolveRuntimeFromPdConfig } = await import('../../src/services/resolve-runtime-from-pd-config.js');
431
+ const resolved = resolveRuntimeFromPdConfig(tmpDir, () => 'test-key');
432
+
433
+ expect(resolved.legacyWarnings.length).toBeGreaterThan(0);
434
+ expect(resolved.configLoadResult.legacyFilesDetected.length).toBeGreaterThanOrEqual(2);
435
+ });
436
+ });
284
437
  });