@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.
- package/dist/commands/__tests__/runtime-activation-nextaction.test.d.ts +15 -0
- package/dist/commands/__tests__/runtime-activation-nextaction.test.d.ts.map +1 -0
- package/dist/commands/__tests__/runtime-activation-nextaction.test.js +73 -0
- package/dist/commands/__tests__/runtime-activation-nextaction.test.js.map +1 -0
- package/dist/commands/runtime-activation.d.ts +19 -0
- package/dist/commands/runtime-activation.d.ts.map +1 -1
- package/dist/commands/runtime-activation.js +39 -22
- package/dist/commands/runtime-activation.js.map +1 -1
- package/dist/commands/runtime-init.d.ts +6 -0
- package/dist/commands/runtime-init.d.ts.map +1 -1
- package/dist/commands/runtime-init.js +127 -2
- package/dist/commands/runtime-init.js.map +1 -1
- package/dist/commands/runtime-internalization-run-once.d.ts.map +1 -1
- package/dist/commands/runtime-internalization-run-once.js +6 -1
- package/dist/commands/runtime-internalization-run-once.js.map +1 -1
- package/dist/services/rulehost-pipeline-runner.d.ts.map +1 -1
- package/dist/services/rulehost-pipeline-runner.js +6 -1
- package/dist/services/rulehost-pipeline-runner.js.map +1 -1
- package/package.json +1 -1
- package/src/commands/__tests__/runtime-activation-nextaction.test.ts +79 -0
- package/src/commands/runtime-activation.ts +57 -18
- package/src/commands/runtime-init.ts +156 -2
- package/src/commands/runtime-internalization-run-once.ts +8 -1
- package/src/services/rulehost-pipeline-runner.ts +6 -1
- package/tests/commands/runtime-activation.test.ts +6 -3
- package/tests/commands/runtime-init-empty-workspace.test.ts +28 -0
- 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
|
|
22
|
-
import
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
45
|
-
|
|
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
|
});
|