@principles/pd-cli 1.136.0 → 1.138.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 (27) hide show
  1. package/dist/commands/__tests__/runtime-activation-nextaction.test.d.ts +15 -0
  2. package/dist/commands/__tests__/runtime-activation-nextaction.test.d.ts.map +1 -0
  3. package/dist/commands/__tests__/runtime-activation-nextaction.test.js +73 -0
  4. package/dist/commands/__tests__/runtime-activation-nextaction.test.js.map +1 -0
  5. package/dist/commands/runtime-activation.d.ts +19 -0
  6. package/dist/commands/runtime-activation.d.ts.map +1 -1
  7. package/dist/commands/runtime-activation.js +39 -22
  8. package/dist/commands/runtime-activation.js.map +1 -1
  9. package/dist/commands/runtime-init.d.ts +6 -0
  10. package/dist/commands/runtime-init.d.ts.map +1 -1
  11. package/dist/commands/runtime-init.js +127 -2
  12. package/dist/commands/runtime-init.js.map +1 -1
  13. package/dist/commands/runtime-internalization-run-once.d.ts.map +1 -1
  14. package/dist/commands/runtime-internalization-run-once.js +6 -1
  15. package/dist/commands/runtime-internalization-run-once.js.map +1 -1
  16. package/dist/services/rulehost-pipeline-runner.d.ts.map +1 -1
  17. package/dist/services/rulehost-pipeline-runner.js +6 -1
  18. package/dist/services/rulehost-pipeline-runner.js.map +1 -1
  19. package/package.json +1 -1
  20. package/src/commands/__tests__/runtime-activation-nextaction.test.ts +79 -0
  21. package/src/commands/runtime-activation.ts +57 -18
  22. package/src/commands/runtime-init.ts +156 -2
  23. package/src/commands/runtime-internalization-run-once.ts +8 -1
  24. package/src/services/rulehost-pipeline-runner.ts +6 -1
  25. package/tests/commands/runtime-activation.test.ts +6 -3
  26. package/tests/commands/runtime-init-empty-workspace.test.ts +28 -0
  27. package/tests/commands/runtime-init.test.ts +71 -4
@@ -18,12 +18,38 @@
18
18
  */
19
19
 
20
20
  import * as path from 'path';
21
- import { SqliteConnection } from '@principles/core/runtime-v2';
22
- import { SchemaConformanceReadModel } from '@principles/core/runtime-v2';
21
+ import * as fs from 'fs';
22
+ import * as yaml from 'js-yaml';
23
+ import { SqliteConnection, SchemaConformanceReadModel } from '@principles/core/runtime-v2';
24
+ import { getDefaultPdConfig, validatePdConfig } from '@principles/core/runtime-v2';
23
25
  import { initTrajectorySchema, initWorkflowSchema } from 'principles-disciple';
24
26
  import { resolveWorkspaceDir } from '../resolve-workspace.js';
25
27
  import { emitResult, emitFlagConflict, emitError } from '../services/cli-output.js';
26
28
 
29
+ // ── Constants ─────────────────────────────────────────────────────────────────
30
+
31
+ const CONFIG_DIR = '.pd';
32
+ const CONFIG_FILENAME = 'config.yaml';
33
+ const CONFIG_HEADER =
34
+ `# PD Runtime Configuration — single source of truth (.pd/config.yaml, ADR-0016)\n` +
35
+ `# Auto-generated by 'pd runtime init'. Existing config files are never overwritten.\n` +
36
+ `# Edit to configure feature flags, runtime profiles, internal agents, and UI.\n`;
37
+
38
+ /**
39
+ * Type guard for Node.js errno errors (rc-2: no `as` on caught unknown).
40
+ * Mirrors principle-tree-ledger.ts — uses Object.hasOwn, not `in`.
41
+ * Structural type (not NodeJS.ErrnoException) to avoid the NodeJS global,
42
+ * which is not defined in this package's eslint config.
43
+ * Needed to detect EEXIST from the atomic exclusive write below.
44
+ */
45
+ interface ErrnoExceptionLike {
46
+ code?: string;
47
+ }
48
+
49
+ function isErrnoException(value: unknown): value is ErrnoExceptionLike {
50
+ return typeof value === 'object' && value !== null && Object.hasOwn(value, 'code');
51
+ }
52
+
27
53
  // ── Output types ─────────────────────────────────────────────────────────────
28
54
 
29
55
  export interface DatabaseInitResult {
@@ -34,11 +60,18 @@ export interface DatabaseInitResult {
34
60
  warnings: string[];
35
61
  }
36
62
 
63
+ export interface ConfigInitResult {
64
+ path: string;
65
+ status: 'initialized' | 'skipped' | 'failed' | 'verified';
66
+ warnings: string[];
67
+ }
68
+
37
69
  export interface RuntimeInitOutput {
38
70
  ok: boolean;
39
71
  mode: 'dry-run' | 'confirm';
40
72
  workspace: string;
41
73
  databases: DatabaseInitResult[];
74
+ config?: ConfigInitResult;
42
75
  warnings: string[];
43
76
  reason?: string;
44
77
  nextAction?: string;
@@ -52,6 +85,100 @@ const DB_NAMES = {
52
85
  workflow: 'subagent_workflows.db',
53
86
  } as const;
54
87
 
88
+ // ── Config file generation ─────────────────────────────────────────────────────
89
+
90
+ /**
91
+ * Build a clean YAML representation of the default PD config, injecting the
92
+ * workspace.default path so that discoverWorkspaceDefault() can find it.
93
+ *
94
+ * Returns the YAML string (with header comment) for writing to disk.
95
+ */
96
+ function buildConfigYaml(workspaceDir: string): string {
97
+ const config = getDefaultPdConfig();
98
+ const yamlObj: Record<string, unknown> = {
99
+ version: config.version,
100
+ workspace: {
101
+ default: workspaceDir,
102
+ },
103
+ features: config.features,
104
+ runtimeProfiles: config.runtimeProfiles,
105
+ internalAgents: config.internalAgents,
106
+ ui: config.ui,
107
+ };
108
+ return CONFIG_HEADER + yaml.dump(yamlObj, {
109
+ indent: 2,
110
+ lineWidth: 120,
111
+ noRefs: true,
112
+ sortKeys: false,
113
+ quotingType: '"',
114
+ forceQuotes: false,
115
+ });
116
+ }
117
+
118
+ /**
119
+ * Generate and write .pd/config.yaml if it does not already exist.
120
+ * Validates the generated file with a round-trip parse+validate check.
121
+ */
122
+ function buildConfigInitResult(workspaceDir: string, confirm: boolean): ConfigInitResult {
123
+ const configPath = path.join(workspaceDir, CONFIG_DIR, CONFIG_FILENAME);
124
+ const warnings: string[] = [];
125
+
126
+ if (!confirm) {
127
+ return { path: configPath, status: 'skipped', warnings: [] };
128
+ }
129
+
130
+ // Ensure .pd directory exists
131
+ const configDir = path.dirname(configPath);
132
+ try {
133
+ fs.mkdirSync(configDir, { recursive: true });
134
+ } catch (err) {
135
+ const reason = err instanceof Error ? err.message : String(err);
136
+ return { path: configPath, status: 'failed', warnings: [`Failed to create ${configDir}: ${reason}`] };
137
+ }
138
+
139
+ // Build and write YAML
140
+ let yamlContent: string;
141
+ try {
142
+ yamlContent = buildConfigYaml(workspaceDir);
143
+ } catch (err) {
144
+ const reason = err instanceof Error ? err.message : String(err);
145
+ return { path: configPath, status: 'failed', warnings: [`Failed to build config YAML: ${reason}`] };
146
+ }
147
+
148
+ try {
149
+ // Atomic exclusive create ('wx'): the write itself is the authoritative
150
+ // existence check — fails with EEXIST if the file already exists (from a
151
+ // previous run or a concurrent process). No existsSync pre-check, so there
152
+ // is no TOCTOU window and the "never overwrite existing config" contract
153
+ // is race-free (CodeQL js/race-condition).
154
+ fs.writeFileSync(configPath, yamlContent, { encoding: 'utf8', flag: 'wx' });
155
+ } catch (err) {
156
+ if (isErrnoException(err) && err.code === 'EEXIST') {
157
+ return { path: configPath, status: 'skipped', warnings: ['config.yaml already exists, skipped'] };
158
+ }
159
+ const reason = err instanceof Error ? err.message : String(err);
160
+ return { path: configPath, status: 'failed', warnings: [`Failed to write ${configPath}: ${reason}`] };
161
+ }
162
+
163
+ // Round-trip validation: read back, parse, validate
164
+ try {
165
+ const raw = fs.readFileSync(configPath, 'utf8');
166
+ const parsed: unknown = yaml.load(raw, { schema: yaml.JSON_SCHEMA });
167
+ const validationResult = validatePdConfig(parsed);
168
+ if (!validationResult.ok) {
169
+ warnings.push(`config.yaml round-trip validation produced ${validationResult.errors.length} warnings`);
170
+ for (const ve of validationResult.errors) {
171
+ warnings.push(` [${ve.path}] ${ve.reason} — ${ve.nextAction}`);
172
+ }
173
+ }
174
+ } catch (err) {
175
+ // Non-fatal: proceed even if round-trip check fails
176
+ warnings.push(`config.yaml round-trip check skipped: ${err instanceof Error ? err.message : String(err)}`);
177
+ }
178
+
179
+ return { path: configPath, status: 'initialized', warnings };
180
+ }
181
+
55
182
  export function buildRuntimeInitOutput(workspaceDir: string, confirm: boolean): RuntimeInitOutput {
56
183
  const resolvedWorkspace = path.resolve(workspaceDir);
57
184
  const warnings: string[] = [];
@@ -213,11 +340,27 @@ export function buildRuntimeInitOutput(workspaceDir: string, confirm: boolean):
213
340
  }
214
341
  }
215
342
 
343
+ // 4. config.yaml — scaffold the single source of truth (P0: runtime discovery)
344
+ const config = buildConfigInitResult(resolvedWorkspace, confirm);
345
+ if (config.status === 'failed') {
346
+ return {
347
+ ok: false,
348
+ mode: 'confirm',
349
+ workspace: resolvedWorkspace,
350
+ databases,
351
+ config,
352
+ warnings,
353
+ reason: config.warnings[0] ?? 'config.yaml initialization failed',
354
+ nextAction: 'Check .pd directory permissions and disk space, then re-run `pd runtime init --confirm`.',
355
+ };
356
+ }
357
+
216
358
  return {
217
359
  ok: true,
218
360
  mode: confirm ? 'confirm' : 'dry-run',
219
361
  workspace: resolvedWorkspace,
220
362
  databases,
363
+ config,
221
364
  warnings,
222
365
  };
223
366
  }
@@ -246,6 +389,17 @@ function formatTextOutput(output: RuntimeInitOutput): string {
246
389
  }
247
390
  }
248
391
 
392
+ if (output.config) {
393
+ const icon = output.config.status === 'initialized' ? '[+]' :
394
+ output.config.status === 'verified' ? '[v]' :
395
+ output.config.status === 'skipped' ? '[ ]' : '[x]';
396
+ lines.push(`${icon} config.yaml (${output.config.status})`);
397
+ lines.push(` path: ${output.config.path}`);
398
+ for (const w of output.config.warnings) {
399
+ lines.push(` warning: ${w}`);
400
+ }
401
+ }
402
+
249
403
  if (output.warnings.length > 0) {
250
404
  lines.push('');
251
405
  lines.push('Warnings:');
@@ -20,6 +20,8 @@ import {
20
20
  import type { WakeOnceResult, DreamerRunnerResult, PhilosopherRunnerResult, ScribeRunnerResult, ArtificerRunnerResult, EvaluatorRunnerResult, RolloutReviewerRunnerResult, PDRuntimeAdapter, PeerRunnerKind, OutputLanguage } from '@principles/core/runtime-v2';
21
21
  import { resolveWorkspaceDir } from '../resolve-workspace.js';
22
22
  import { readOutputLanguageFromWorkspace } from '../config-reader.js';
23
+ import { loadPdConfig } from '../services/pd-config-loader.js';
24
+ import type { EffectivePdConfig } from '@principles/core/runtime-v2';
23
25
  import {
24
26
  resolveRuntimeAdapterFromConfig,
25
27
  ConfigResolutionError,
@@ -447,6 +449,11 @@ export async function handleRuntimeInternalizationRunOnce(opts: RunOnceOptions):
447
449
  const stateManager = new RuntimeStateManager({ workspaceDir });
448
450
  await stateManager.initialize();
449
451
 
452
+ // Issue 2: resolve effective config for feature-flag-aware runners
453
+ // (e.g. `artificer_output_retry`). Mirrors rulehost-pipeline-runner.
454
+ const configLoad = loadPdConfig(workspaceDir);
455
+ const effectiveConfig: EffectivePdConfig | undefined = configLoad.ok ? configLoad.effective : configLoad.defaults;
456
+
450
457
  try {
451
458
  const orchestrator = new InternalizationOrchestrator(
452
459
  { stateManager },
@@ -515,7 +522,7 @@ export async function handleRuntimeInternalizationRunOnce(opts: RunOnceOptions):
515
522
  const validator = new DefaultArtificerValidator();
516
523
  const runner = new ArtificerRunner(
517
524
  { stateManager, runtimeAdapter, eventEmitter, validator, artifactStore, contentHashFn },
518
- { owner: OWNER, runtimeKind: runtimeAdapter.kind(), pollIntervalMs: 100, timeoutMs: effectiveTimeoutMs },
525
+ { owner: OWNER, runtimeKind: runtimeAdapter.kind(), pollIntervalMs: 100, timeoutMs: effectiveTimeoutMs, effectiveConfig },
519
526
  );
520
527
  runnerResult = await runner.run(wakeResult.taskId);
521
528
  } else if (runnerKind === 'evaluator') {
@@ -272,6 +272,11 @@ export async function runRuleHostPipeline(opts: RuleHostPipelineOptions): Promis
272
272
  try {
273
273
  const artifactStore = stateManager.piArtifactStore;
274
274
  const eventEmitter = new StoreEventEmitter();
275
+ // Issue 2: load effective config once so every stage runner can resolve
276
+ // feature flags (e.g. `artificer_output_retry`). Mirrors
277
+ // createEvaluatorRunnerDeps (rc-9: malformed config → fallback defaults).
278
+ const configLoad = loadPdConfig(opts.workspaceDir);
279
+ const effectiveConfig = configLoad.ok ? configLoad.effective : configLoad.defaults;
275
280
  // Allow the caller's adapter to resolve real artifactIds (needed by
276
281
  // test-double adapters whose scripted outputs must match store-assigned IDs).
277
282
  opts.onStoreReady?.(artifactStore);
@@ -282,7 +287,7 @@ export async function runRuleHostPipeline(opts: RuleHostPipelineOptions): Promis
282
287
  scribe: opts.runtimeAdapter,
283
288
  evaluator: opts.runtimeAdapter,
284
289
  };
285
- const runnerOptsFor = (adapter: PDRuntimeAdapter) => ({ owner, runtimeKind: adapter.kind(), pollIntervalMs, timeoutMs });
290
+ const runnerOptsFor = (adapter: PDRuntimeAdapter) => ({ owner, runtimeKind: adapter.kind(), pollIntervalMs, timeoutMs, effectiveConfig });
286
291
 
287
292
  // ── Stage: pain lookup ──
288
293
  // Find a dreamer task already seeded for this pain (the pain→dreamer bridge
@@ -655,7 +655,8 @@ describe('handleRuntimeActivationList', () => {
655
655
  expect(rec.evidenceRefs).toEqual(['ex-1', 'ex-2']);
656
656
  expect(rec.evidenceSummary).toContain('2 evidence ref(s)');
657
657
  expect(rec.nextAction).toContain('Enable rulecode_context_v2 flag');
658
- expect(rec.nextAction).toContain('pd activation deactivate --activation-id act-v2-shadow --confirm');
658
+ expect(rec.nextAction).toContain('pd activation deactivate --activation-id act-v2-shadow');
659
+ expect(rec.nextAction).not.toContain('--confirm');
659
660
 
660
661
  // Restore the mock for subsequent tests.
661
662
  mockFeatureFlags.flags.rulecode_context_v2.enabled = true;
@@ -761,7 +762,8 @@ describe('handleRuntimeActivationList', () => {
761
762
  expect(rec.status).toBe('active');
762
763
  expect(rec.mode).toBe('live');
763
764
  expect(rec.promotedAt).toBe('2026-06-19T00:00:00.000Z');
764
- expect(rec.nextAction).toBe('pd activation deactivate --activation-id act-v1-live --confirm');
765
+ expect(rec.nextAction).toBe('pd activation deactivate --activation-id act-v1-live');
766
+ expect(rec.nextAction).not.toContain('--confirm');
765
767
  });
766
768
 
767
769
  it('PRI-491: text output shows promotedAt timestamp when present', async () => {
@@ -790,7 +792,8 @@ describe('handleRuntimeActivationList', () => {
790
792
  const text = consoleLogSpy.mock.calls.map(c => c[0]).join('\n');
791
793
  expect(text).toContain('(live)');
792
794
  expect(text).toContain('promotedAt: 2026-06-19T00:00:00.000Z');
793
- expect(text).toContain('nextAction: pd activation deactivate --activation-id act-v1-live --confirm');
795
+ expect(text).toContain('nextAction: pd activation deactivate --activation-id act-v1-live');
796
+ expect(text).not.toContain('--confirm');
794
797
  });
795
798
 
796
799
  it('PRI-491: deactivated activation shows [DEACTIVATED <ts>] regardless of contextVersion (precedence)', async () => {
@@ -18,7 +18,9 @@ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
18
18
  import * as fs from 'node:fs';
19
19
  import * as path from 'node:path';
20
20
  import * as os from 'node:os';
21
+ import * as yaml from 'js-yaml';
21
22
  import Database from 'better-sqlite3';
23
+ import { validatePdConfig } from '@principles/core/runtime-v2';
22
24
  import { buildRuntimeInitOutput } from '../../src/commands/runtime-init.js';
23
25
 
24
26
  // ── Helpers ─────────────────────────────────────────────────────────────────
@@ -190,6 +192,26 @@ describe('pd runtime init — empty workspace integration', () => {
190
192
  db.close();
191
193
  }
192
194
  });
195
+
196
+ // ── P0: config.yaml scaffolding (Issue 1b) ────────────────────────────────
197
+
198
+ it('creates .pd/config.yaml with version 1 and workspace.default set', () => {
199
+ buildRuntimeInitOutput(tmpDir, true);
200
+ const configPath = path.join(tmpDir, '.pd', 'config.yaml');
201
+ expect(fs.existsSync(configPath)).toBe(true);
202
+ const parsed = yaml.load(fs.readFileSync(configPath, 'utf8'), { schema: yaml.JSON_SCHEMA });
203
+ const obj = parsed as { version?: unknown; workspace?: { default?: unknown } };
204
+ expect(obj.version).toBe(1);
205
+ expect(obj.workspace?.default).toBe(path.resolve(tmpDir));
206
+ });
207
+
208
+ it('generated config.yaml passes validatePdConfig (round-trip)', () => {
209
+ buildRuntimeInitOutput(tmpDir, true);
210
+ const configPath = path.join(tmpDir, '.pd', 'config.yaml');
211
+ const parsed: unknown = yaml.load(fs.readFileSync(configPath, 'utf8'), { schema: yaml.JSON_SCHEMA });
212
+ const result = validatePdConfig(parsed);
213
+ expect(result.ok).toBe(true);
214
+ });
193
215
  });
194
216
 
195
217
  // ── EMPTY-02: idempotency ──────────────────────────────────────────────────
@@ -215,6 +237,12 @@ describe('pd runtime init — empty workspace integration', () => {
215
237
  // Table set should be unchanged
216
238
  const tables2 = getTableNames(trajDbPath);
217
239
  expect(tables2).toEqual(tables1);
240
+
241
+ // config.yaml is preserved: second run reports skipped, content unchanged
242
+ expect(output2.config?.status).toBe('skipped');
243
+ const configPath = path.join(tmpDir, '.pd', 'config.yaml');
244
+ const config1 = fs.readFileSync(configPath, 'utf8');
245
+ expect(fs.readFileSync(configPath, 'utf8')).toBe(config1);
218
246
  });
219
247
 
220
248
  it('dry-run after confirm does not modify existing DBs', () => {
@@ -40,10 +40,14 @@ vi.mock('principles-disciple', () => ({
40
40
  initWorkflowSchema: mockState.initWorkflowSchema,
41
41
  }));
42
42
 
43
- vi.mock('@principles/core/runtime-v2', () => ({
44
- SqliteConnection: mockState.sqliteConnectionCtor,
45
- SchemaConformanceReadModel: mockState.schemaConformanceCtor,
46
- }));
43
+ vi.mock('@principles/core/runtime-v2', async () => {
44
+ const actual = await vi.importActual<typeof import('@principles/core/runtime-v2')>('@principles/core/runtime-v2');
45
+ return {
46
+ ...actual,
47
+ SqliteConnection: mockState.sqliteConnectionCtor,
48
+ SchemaConformanceReadModel: mockState.schemaConformanceCtor,
49
+ };
50
+ });
47
51
 
48
52
  vi.mock('../../src/resolve-workspace.js', () => ({
49
53
  resolveWorkspaceDir: mockState.resolveWorkspaceDir,
@@ -343,4 +347,67 @@ describe('pd runtime init', () => {
343
347
  } finally { rmTmpDir(tmp); }
344
348
  });
345
349
  });
350
+
351
+ // ── INIT-07: config.yaml scaffolding (P0: runtime discovery) ───────────────
352
+
353
+ describe('INIT-07: config.yaml scaffolding', () => {
354
+ it('dry-run reports config.yaml as skipped without writing the file', () => {
355
+ const tmp = mkTmpDir();
356
+ try {
357
+ const output = buildRuntimeInitOutput(tmp, false);
358
+ expect(output.config).toBeDefined();
359
+ expect(output.config?.status).toBe('skipped');
360
+ expect(fs.existsSync(path.join(tmp, '.pd', 'config.yaml'))).toBe(false);
361
+ } finally { rmTmpDir(tmp); }
362
+ });
363
+
364
+ it('--confirm writes a valid config.yaml with version and workspace.default', async () => {
365
+ const tmp = mkTmpDir();
366
+ try {
367
+ const output = buildRuntimeInitOutput(tmp, true);
368
+ expect(output.config?.status).toBe('initialized');
369
+ const configPath = path.join(tmp, '.pd', 'config.yaml');
370
+ expect(fs.existsSync(configPath)).toBe(true);
371
+ const yaml = (await import('js-yaml')).default;
372
+ const raw = fs.readFileSync(configPath, 'utf8');
373
+ const parsed = yaml.load(raw, { schema: yaml.JSON_SCHEMA });
374
+ expect(typeof parsed).toBe('object');
375
+ const obj = parsed as { version?: unknown; workspace?: { default?: unknown }; features?: unknown };
376
+ expect(obj.version).toBe(1);
377
+ expect(obj.workspace?.default).toBe(path.resolve(tmp));
378
+ expect(obj.features).toBeDefined();
379
+ expect(Object.keys(obj.features as Record<string, unknown>).length).toBeGreaterThan(0);
380
+ } finally { rmTmpDir(tmp); }
381
+ });
382
+
383
+ it('round-trip: generated config passes validatePdConfig', async () => {
384
+ const tmp = mkTmpDir();
385
+ try {
386
+ buildRuntimeInitOutput(tmp, true);
387
+ const configPath = path.join(tmp, '.pd', 'config.yaml');
388
+ const yaml = (await import('js-yaml')).default;
389
+ const { validatePdConfig } = await import('@principles/core/runtime-v2');
390
+ const parsed: unknown = yaml.load(fs.readFileSync(configPath, 'utf8'), { schema: yaml.JSON_SCHEMA });
391
+ const result = validatePdConfig(parsed);
392
+ expect(result.ok).toBe(true);
393
+ } finally { rmTmpDir(tmp); }
394
+ });
395
+
396
+ it('--confirm skips when config.yaml already exists (preserves user file)', () => {
397
+ const tmp = mkTmpDir();
398
+ try {
399
+ // Pre-create a user-modified config.yaml
400
+ const configDir = path.join(tmp, '.pd');
401
+ fs.mkdirSync(configDir, { recursive: true });
402
+ const userConfig = '# user config\nversion: 1\n';
403
+ fs.writeFileSync(path.join(configDir, 'config.yaml'), userConfig, 'utf8');
404
+
405
+ const output = buildRuntimeInitOutput(tmp, true);
406
+ expect(output.config?.status).toBe('skipped');
407
+ expect(output.config?.warnings.some(w => w.includes('already exists'))).toBe(true);
408
+ // Content must be preserved
409
+ expect(fs.readFileSync(path.join(configDir, 'config.yaml'), 'utf8')).toBe(userConfig);
410
+ } finally { rmTmpDir(tmp); }
411
+ });
412
+ });
346
413
  });