@principles/pd-cli 1.133.0 → 1.135.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 (215) hide show
  1. package/dist/commands/__tests__/intent-flag-wiring.test.js +74 -3
  2. package/dist/commands/__tests__/intent-flag-wiring.test.js.map +1 -1
  3. package/dist/commands/__tests__/pain-retry-maxTokens-flag.test.d.ts +2 -0
  4. package/dist/commands/__tests__/pain-retry-maxTokens-flag.test.d.ts.map +1 -0
  5. package/dist/commands/__tests__/pain-retry-maxTokens-flag.test.js +60 -0
  6. package/dist/commands/__tests__/pain-retry-maxTokens-flag.test.js.map +1 -0
  7. package/dist/commands/__tests__/rulecode-flag-wiring.test.js +11 -4
  8. package/dist/commands/__tests__/rulecode-flag-wiring.test.js.map +1 -1
  9. package/dist/commands/__tests__/run-rulehost-flag-wiring.test.js +9 -0
  10. package/dist/commands/__tests__/run-rulehost-flag-wiring.test.js.map +1 -1
  11. package/dist/commands/admission-gate.d.ts +24 -0
  12. package/dist/commands/admission-gate.d.ts.map +1 -0
  13. package/dist/commands/admission-gate.js +29 -0
  14. package/dist/commands/admission-gate.js.map +1 -0
  15. package/dist/commands/candidate.d.ts.map +1 -1
  16. package/dist/commands/candidate.js +72 -6
  17. package/dist/commands/candidate.js.map +1 -1
  18. package/dist/commands/console.d.ts.map +1 -1
  19. package/dist/commands/console.js +42 -0
  20. package/dist/commands/console.js.map +1 -1
  21. package/dist/commands/demo-story-a.d.ts.map +1 -1
  22. package/dist/commands/demo-story-a.js +4 -2
  23. package/dist/commands/demo-story-a.js.map +1 -1
  24. package/dist/commands/diagnose.d.ts.map +1 -1
  25. package/dist/commands/diagnose.js +155 -8
  26. package/dist/commands/diagnose.js.map +1 -1
  27. package/dist/commands/errors-list.d.ts +25 -0
  28. package/dist/commands/errors-list.d.ts.map +1 -0
  29. package/dist/commands/errors-list.js +368 -0
  30. package/dist/commands/errors-list.js.map +1 -0
  31. package/dist/commands/health-codex.d.ts +7 -0
  32. package/dist/commands/health-codex.d.ts.map +1 -0
  33. package/dist/commands/health-codex.js +209 -0
  34. package/dist/commands/health-codex.js.map +1 -0
  35. package/dist/commands/health.d.ts +24 -0
  36. package/dist/commands/health.d.ts.map +1 -1
  37. package/dist/commands/health.js +48 -0
  38. package/dist/commands/health.js.map +1 -1
  39. package/dist/commands/intent.d.ts +21 -4
  40. package/dist/commands/intent.d.ts.map +1 -1
  41. package/dist/commands/intent.js +88 -27
  42. package/dist/commands/intent.js.map +1 -1
  43. package/dist/commands/pain-evidence.d.ts +6 -0
  44. package/dist/commands/pain-evidence.d.ts.map +1 -1
  45. package/dist/commands/pain-evidence.js +6 -0
  46. package/dist/commands/pain-evidence.js.map +1 -1
  47. package/dist/commands/pain-record.d.ts.map +1 -1
  48. package/dist/commands/pain-record.js +21 -0
  49. package/dist/commands/pain-record.js.map +1 -1
  50. package/dist/commands/pain-retry.d.ts +4 -0
  51. package/dist/commands/pain-retry.d.ts.map +1 -1
  52. package/dist/commands/pain-retry.js +327 -32
  53. package/dist/commands/pain-retry.js.map +1 -1
  54. package/dist/commands/proven-channel-baseline.d.ts.map +1 -1
  55. package/dist/commands/proven-channel-baseline.js +4 -2
  56. package/dist/commands/proven-channel-baseline.js.map +1 -1
  57. package/dist/commands/runtime-activation.d.ts +30 -0
  58. package/dist/commands/runtime-activation.d.ts.map +1 -1
  59. package/dist/commands/runtime-activation.js +567 -11
  60. package/dist/commands/runtime-activation.js.map +1 -1
  61. package/dist/commands/runtime-canary.d.ts.map +1 -1
  62. package/dist/commands/runtime-canary.js +32 -7
  63. package/dist/commands/runtime-canary.js.map +1 -1
  64. package/dist/commands/runtime-health-snapshot.d.ts.map +1 -1
  65. package/dist/commands/runtime-health-snapshot.js +4 -2
  66. package/dist/commands/runtime-health-snapshot.js.map +1 -1
  67. package/dist/commands/runtime-init.d.ts +43 -0
  68. package/dist/commands/runtime-init.d.ts.map +1 -0
  69. package/dist/commands/runtime-init.js +264 -0
  70. package/dist/commands/runtime-init.js.map +1 -0
  71. package/dist/commands/runtime-internalization-context-trace.d.ts +25 -0
  72. package/dist/commands/runtime-internalization-context-trace.d.ts.map +1 -0
  73. package/dist/commands/runtime-internalization-context-trace.js +374 -0
  74. package/dist/commands/runtime-internalization-context-trace.js.map +1 -0
  75. package/dist/commands/runtime-internalization-enqueue-successors.d.ts.map +1 -1
  76. package/dist/commands/runtime-internalization-enqueue-successors.js +3 -1
  77. package/dist/commands/runtime-internalization-enqueue-successors.js.map +1 -1
  78. package/dist/commands/runtime-internalization-integrity.d.ts.map +1 -1
  79. package/dist/commands/runtime-internalization-integrity.js +4 -2
  80. package/dist/commands/runtime-internalization-integrity.js.map +1 -1
  81. package/dist/commands/runtime-internalization-run-once.d.ts.map +1 -1
  82. package/dist/commands/runtime-internalization-run-once.js +20 -5
  83. package/dist/commands/runtime-internalization-run-once.js.map +1 -1
  84. package/dist/commands/runtime-internalization-run-rulehost.d.ts +1 -0
  85. package/dist/commands/runtime-internalization-run-rulehost.d.ts.map +1 -1
  86. package/dist/commands/runtime-internalization-run-rulehost.js +149 -6
  87. package/dist/commands/runtime-internalization-run-rulehost.js.map +1 -1
  88. package/dist/commands/runtime-uat.d.ts.map +1 -1
  89. package/dist/commands/runtime-uat.guard.test.js +13 -15
  90. package/dist/commands/runtime-uat.guard.test.js.map +1 -1
  91. package/dist/commands/runtime-uat.js +4 -3
  92. package/dist/commands/runtime-uat.js.map +1 -1
  93. package/dist/config-reader.js +1 -1
  94. package/dist/config-reader.js.map +1 -1
  95. package/dist/index.js +54 -78
  96. package/dist/index.js.map +1 -1
  97. package/dist/services/__tests__/evaluator-runner-deps.test.d.ts +2 -0
  98. package/dist/services/__tests__/evaluator-runner-deps.test.d.ts.map +1 -0
  99. package/dist/services/__tests__/evaluator-runner-deps.test.js +304 -0
  100. package/dist/services/__tests__/evaluator-runner-deps.test.js.map +1 -0
  101. package/dist/services/__tests__/rulehost-readiness.test.js.map +1 -1
  102. package/dist/services/__tests__/runtime-adapter-resolver.test.js.map +1 -1
  103. package/dist/services/config-doctor.d.ts.map +1 -1
  104. package/dist/services/config-doctor.js +9 -2
  105. package/dist/services/config-doctor.js.map +1 -1
  106. package/dist/services/console-launcher.d.ts +6 -0
  107. package/dist/services/console-launcher.d.ts.map +1 -1
  108. package/dist/services/console-launcher.js.map +1 -1
  109. package/dist/services/pain-flood-simulation-runner.d.ts.map +1 -1
  110. package/dist/services/pain-flood-simulation-runner.js +9 -0
  111. package/dist/services/pain-flood-simulation-runner.js.map +1 -1
  112. package/dist/services/pd-config-loader.js +2 -2
  113. package/dist/services/pd-config-loader.js.map +1 -1
  114. package/dist/services/proven-channel-baseline-runner.d.ts.map +1 -1
  115. package/dist/services/proven-channel-baseline-runner.js +10 -2
  116. package/dist/services/proven-channel-baseline-runner.js.map +1 -1
  117. package/dist/services/quality-scorecard/data-extractor.js.map +1 -1
  118. package/dist/services/resolve-runtime-from-pd-config.js.map +1 -1
  119. package/dist/services/rulehost-pipeline-runner.d.ts +77 -1
  120. package/dist/services/rulehost-pipeline-runner.d.ts.map +1 -1
  121. package/dist/services/rulehost-pipeline-runner.js +111 -7
  122. package/dist/services/rulehost-pipeline-runner.js.map +1 -1
  123. package/dist/services/rulehost-readiness.js.map +1 -1
  124. package/dist/services/runtime-adapter-resolver.d.ts +1 -0
  125. package/dist/services/runtime-adapter-resolver.d.ts.map +1 -1
  126. package/dist/services/runtime-adapter-resolver.js +3 -0
  127. package/dist/services/runtime-adapter-resolver.js.map +1 -1
  128. package/dist/services/synthetic-baseline-runner.d.ts.map +1 -1
  129. package/dist/services/synthetic-baseline-runner.js +11 -0
  130. package/dist/services/synthetic-baseline-runner.js.map +1 -1
  131. package/dist/utils/production-workspace-guard.d.ts +4 -4
  132. package/dist/utils/production-workspace-guard.d.ts.map +1 -1
  133. package/dist/utils/production-workspace-guard.js +19 -16
  134. package/dist/utils/production-workspace-guard.js.map +1 -1
  135. package/dist/utils/production-workspace-guard.test.js +54 -19
  136. package/dist/utils/production-workspace-guard.test.js.map +1 -1
  137. package/package.json +8 -5
  138. package/src/commands/__tests__/intent-flag-wiring.test.ts +88 -3
  139. package/src/commands/__tests__/pain-retry-maxTokens-flag.test.ts +73 -0
  140. package/src/commands/__tests__/rulecode-flag-wiring.test.ts +31 -24
  141. package/src/commands/__tests__/run-rulehost-flag-wiring.test.ts +12 -0
  142. package/src/commands/admission-gate.ts +30 -0
  143. package/src/commands/candidate.ts +74 -6
  144. package/src/commands/console.ts +41 -0
  145. package/src/commands/demo-story-a.ts +4 -2
  146. package/src/commands/diagnose.ts +158 -10
  147. package/src/commands/errors-list.ts +440 -0
  148. package/src/commands/health-codex.ts +249 -0
  149. package/src/commands/health.ts +60 -0
  150. package/src/commands/intent.ts +98 -29
  151. package/src/commands/pain-evidence.ts +6 -0
  152. package/src/commands/pain-record.ts +21 -0
  153. package/src/commands/pain-retry.ts +353 -35
  154. package/src/commands/proven-channel-baseline.ts +4 -2
  155. package/src/commands/runtime-activation.ts +657 -12
  156. package/src/commands/runtime-canary.ts +31 -5
  157. package/src/commands/runtime-health-snapshot.ts +4 -2
  158. package/src/commands/runtime-init.ts +305 -0
  159. package/src/commands/runtime-internalization-context-trace.ts +445 -0
  160. package/src/commands/runtime-internalization-enqueue-successors.ts +3 -1
  161. package/src/commands/runtime-internalization-integrity.ts +4 -2
  162. package/src/commands/runtime-internalization-run-once.ts +20 -5
  163. package/src/commands/runtime-internalization-run-rulehost.ts +159 -6
  164. package/src/commands/runtime-uat.guard.test.ts +14 -16
  165. package/src/commands/runtime-uat.ts +4 -3
  166. package/src/config-reader.ts +1 -1
  167. package/src/index.ts +69 -78
  168. package/src/services/__tests__/evaluator-runner-deps.test.ts +341 -0
  169. package/src/services/config-doctor.ts +9 -2
  170. package/src/services/console-launcher.ts +6 -0
  171. package/src/services/pain-flood-simulation-runner.ts +9 -0
  172. package/src/services/pd-config-loader.ts +2 -2
  173. package/src/services/proven-channel-baseline-runner.ts +10 -3
  174. package/src/services/rulehost-pipeline-runner.ts +157 -6
  175. package/src/services/runtime-adapter-resolver.ts +4 -0
  176. package/src/services/synthetic-baseline-runner.ts +11 -0
  177. package/src/utils/production-workspace-guard.test.ts +62 -20
  178. package/src/utils/production-workspace-guard.ts +19 -16
  179. package/tests/bdd/cli-contract.steps.ts +343 -0
  180. package/tests/bdd/context-trace.steps.ts +229 -0
  181. package/tests/bdd/support/gherkin-loader.ts +147 -0
  182. package/tests/bdd/support/repo-root.ts +62 -0
  183. package/tests/bdd/support/vitest-bdd.ts +179 -0
  184. package/tests/commands/admission-gate-coverage.test.ts +79 -0
  185. package/tests/commands/candidate-audit-repair.test.ts +77 -0
  186. package/tests/commands/candidate-intake.test.ts +155 -10
  187. package/tests/commands/candidate-internalization-backfill.test.ts +30 -0
  188. package/tests/commands/cli-help-snapshot.test.ts +15 -1
  189. package/tests/commands/console-open.test.ts +229 -32
  190. package/tests/commands/diagnose.test.ts +619 -14
  191. package/tests/commands/errors-list.test.ts +678 -0
  192. package/tests/commands/health-codex.test.ts +243 -0
  193. package/tests/commands/health-host-wiring.test.ts +76 -0
  194. package/tests/commands/intent.test.ts +4 -4
  195. package/tests/commands/pain-record.test.ts +80 -0
  196. package/tests/commands/pain-retry.test.ts +94 -3
  197. package/tests/commands/run-rulehost-handler.test.ts +133 -0
  198. package/tests/commands/runtime-activation-approve-flag-wiring.test.ts +227 -0
  199. package/tests/commands/runtime-activation-deactivate-flag-wiring.test.ts +167 -0
  200. package/tests/commands/runtime-activation-dispatch-flag-wiring.test.ts +262 -0
  201. package/tests/commands/runtime-activation-list-flag-wiring.test.ts +193 -0
  202. package/tests/commands/runtime-activation-promote-flag-wiring.test.ts +222 -0
  203. package/tests/commands/runtime-activation.test.ts +1090 -5
  204. package/tests/commands/runtime-canary.test.ts +50 -0
  205. package/tests/commands/runtime-features.test.ts +2 -0
  206. package/tests/commands/runtime-init-empty-workspace.test.ts +234 -0
  207. package/tests/commands/runtime-init.test.ts +346 -0
  208. package/tests/commands/runtime-internalization-context-trace.test.ts +110 -0
  209. package/tests/commands/runtime-internalization.test.ts +2 -2
  210. package/tests/config-reader.test.ts +6 -5
  211. package/tests/e2e/cross-package-acceptance.test.ts +55 -13
  212. package/tests/services/mainline-snapshot-assembler.test.ts +6 -3
  213. package/tests/services/resolve-runtime-from-pd-config.test.ts +2 -0
  214. package/tests/services/rulehost-pipeline-runner.test.ts +1 -1
  215. package/vitest.config.ts +1 -1
@@ -1,4 +1,15 @@
1
1
  import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import { createRequire } from 'node:module';
3
+
4
+ const require = createRequire(import.meta.url);
5
+ const expectedCliVersion: string = ((): string => {
6
+ const pkg: unknown = require('../../package.json');
7
+ if (typeof pkg === 'object' && pkg !== null && Object.hasOwn(pkg, 'version')) {
8
+ const v: unknown = Reflect.get(pkg, 'version');
9
+ if (typeof v === 'string') return v;
10
+ }
11
+ return 'unknown';
12
+ })();
2
13
 
3
14
  const {
4
15
  mockSchemaCheck,
@@ -88,6 +99,7 @@ function healthyHealthSnapshot() {
88
99
  gfi: { active: null, staleSessionCount: 0, totalSessionCount: 0, activeSessionCount: 0, generatedAt: new Date().toISOString() },
89
100
  overallStatus: 'healthy' as const,
90
101
  recommendedActions: [],
102
+ totalTaskCount: 1,
91
103
  };
92
104
  }
93
105
 
@@ -165,6 +177,44 @@ describe('runCanaryChecks', () => {
165
177
  expect(schemaCheck?.status).toBe('degraded');
166
178
  });
167
179
 
180
+ it('reports an unexercised runtime as degraded with a next action', async () => {
181
+ mockHealthSnapshot.mockResolvedValue({
182
+ ...healthyHealthSnapshot(), totalTaskCount: 0,
183
+ recommendedActions: ['Runtime V2 pipeline has never been exercised.'],
184
+ });
185
+ const result = await runCanaryChecks(WS);
186
+ const runtimeCheck = result.checks.find(check => check.name === 'runtime_health');
187
+ expect(result.overallStatus).toBe('degraded');
188
+ expect(runtimeCheck?.status).toBe('degraded');
189
+ expect(runtimeCheck?.summary).toContain('not been exercised');
190
+ expect(result.recommendedNextActions).toContainEqual(expect.stringContaining('pain record'));
191
+ });
192
+
193
+ it('surfaces a real error (e.g. DB missing) as error, not masked by cold-start totalTaskCount=0', async () => {
194
+ // Simulates DB-missing scenario: overallStatus='error' while totalTaskCount stays 0
195
+ // (getTotalTaskCount() throws and the snapshot keeps the default 0).
196
+ mockHealthSnapshot.mockResolvedValue({
197
+ ...healthyHealthSnapshot(),
198
+ overallStatus: 'error' as const,
199
+ totalTaskCount: 0,
200
+ recommendedActions: ['Re-initialize the runtime database.'],
201
+ });
202
+ const result = await runCanaryChecks(WS);
203
+ const runtimeCheck = result.checks.find(check => check.name === 'runtime_health');
204
+ expect(runtimeCheck?.status).toBe('error');
205
+ // Summary must surface the error, NOT the cold-start "not been exercised" message.
206
+ expect(runtimeCheck?.summary).not.toContain('not been exercised');
207
+ expect(runtimeCheck?.summary).toContain('error');
208
+ expect(runtimeCheck?.summary).toContain('Re-initialize the runtime database.');
209
+ expect(result.overallStatus).toBe('error');
210
+ });
211
+
212
+ it('reports the package version instead of a hard-coded shim version', async () => {
213
+ const result = await runCanaryChecks(WS);
214
+ const shimCheck = result.checks.find(check => check.name === 'pd_shim_info');
215
+ expect(shimCheck?.details).toMatchObject({ version: expectedCliVersion });
216
+ });
217
+
168
218
  it('continues other checks when one throws', async () => {
169
219
  mockAuditConsistency.mockRejectedValue(new Error('DB error'));
170
220
 
@@ -44,6 +44,8 @@ function makeValidConfigYaml(): string {
44
44
  empathy_observer: { category: 'quiet', enabled: false },
45
45
  },
46
46
  runtimeProfiles: {
47
+ // Include pd.default (the M9 default) to avoid "profile not found" warnings
48
+ 'pd.default': { type: 'pi-ai', provider: 'anthropic', model: 'claude-3-5-sonnet', apiKeyEnv: 'ANTHROPIC_API_KEY' },
47
49
  'openclaw.default': { type: 'openclaw', source: 'default' },
48
50
  },
49
51
  internalAgents: {
@@ -0,0 +1,234 @@
1
+ /**
2
+ * runtime-init empty-workspace integration tests — verifies real DB initialization.
3
+ *
4
+ * Does NOT mock the DB layer. Uses real better-sqlite3 to create actual database
5
+ * files in a temp workspace, then verifies all expected tables exist.
6
+ *
7
+ * Covers:
8
+ * - EMPTY-01: initialize empty workspace → all 3 DBs have full schema
9
+ * - EMPTY-02: idempotency — running twice does not error or lose data
10
+ *
11
+ * Prerequisites: @principles/core and principles-disciple must be built.
12
+ *
13
+ * ERR refs:
14
+ * - rc-7-loop-state-freshness: idempotency test uses fresh workspace each run
15
+ */
16
+
17
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
18
+ import * as fs from 'node:fs';
19
+ import * as path from 'node:path';
20
+ import * as os from 'node:os';
21
+ import Database from 'better-sqlite3';
22
+ import { buildRuntimeInitOutput } from '../../src/commands/runtime-init.js';
23
+
24
+ // ── Helpers ─────────────────────────────────────────────────────────────────
25
+
26
+ function mkTmpDir(): string {
27
+ return fs.mkdtempSync(path.join(os.tmpdir(), 'pd-runtime-init-empty-'));
28
+ }
29
+
30
+ function rmTmpDir(dir: string): void {
31
+ try { fs.rmSync(dir, { recursive: true, force: true }); } catch { /* ignore */ }
32
+ }
33
+
34
+ function getTableNames(dbPath: string): string[] {
35
+ const db = new Database(dbPath, { readonly: true });
36
+ try {
37
+ const rows = db.prepare(
38
+ "SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' ORDER BY name"
39
+ ).all() as { name: string }[];
40
+ return rows.map(r => r.name);
41
+ } finally {
42
+ db.close();
43
+ }
44
+ }
45
+
46
+ function getIndexNames(dbPath: string): string[] {
47
+ const db = new Database(dbPath, { readonly: true });
48
+ try {
49
+ const rows = db.prepare(
50
+ "SELECT name FROM sqlite_master WHERE type='index' AND name NOT LIKE 'sqlite_%' ORDER BY name"
51
+ ).all() as { name: string }[];
52
+ return rows.map(r => r.name);
53
+ } finally {
54
+ db.close();
55
+ }
56
+ }
57
+
58
+ // ── Expected schema ─────────────────────────────────────────────────────────
59
+
60
+ const EXPECTED_STATE_TABLES = [
61
+ 'tasks', 'runs', 'artifacts', 'commits',
62
+ 'principle_candidates', 'pi_artifacts',
63
+ 'approvals', 'activations', 'intent_decisions',
64
+ 'schema_version',
65
+ ];
66
+
67
+ const EXPECTED_TRAJECTORY_TABLES = [
68
+ 'schema_version', 'ingest_checkpoint', 'sessions', 'assistant_turns',
69
+ 'user_turns', 'tool_calls', 'pain_events', 'gate_blocks', 'trust_changes',
70
+ 'principle_events', 'task_outcomes', 'correction_samples', 'sample_reviews',
71
+ 'exports_audit', 'evolution_tasks', 'evolution_events',
72
+ ];
73
+
74
+ const EXPECTED_WORKFLOW_TABLES = [
75
+ 'schema_version', 'subagent_workflows', 'subagent_workflow_events',
76
+ ];
77
+
78
+ const EXPECTED_TRAJECTORY_INDEXES = [
79
+ 'idx_assistant_turns_session_id',
80
+ 'idx_assistant_turns_created_at',
81
+ 'idx_assistant_turns_provider_model',
82
+ 'idx_user_turns_session_id',
83
+ 'idx_tool_calls_session_id',
84
+ 'idx_tool_calls_created_at',
85
+ 'idx_pain_events_session_id',
86
+ 'idx_pain_events_canonical_pain_id',
87
+ 'idx_correction_samples_review_status',
88
+ 'idx_evolution_tasks_trace_id',
89
+ 'idx_evolution_tasks_status',
90
+ 'idx_evolution_tasks_created_at',
91
+ 'idx_evolution_events_trace_id',
92
+ 'idx_evolution_events_created_at',
93
+ ];
94
+
95
+ // ── Tests ───────────────────────────────────────────────────────────────────
96
+
97
+ describe('pd runtime init — empty workspace integration', () => {
98
+ let tmpDir: string;
99
+
100
+ beforeEach(() => {
101
+ tmpDir = mkTmpDir();
102
+ });
103
+
104
+ afterEach(() => {
105
+ rmTmpDir(tmpDir);
106
+ });
107
+
108
+ // ── EMPTY-01: initialize empty workspace ───────────────────────────────────
109
+
110
+ describe('EMPTY-01: initialize empty workspace', () => {
111
+ it('returns ok=true with 3 initialized databases', () => {
112
+ const output = buildRuntimeInitOutput(tmpDir, true);
113
+ expect(output.ok).toBe(true);
114
+ expect(output.mode).toBe('confirm');
115
+ expect(output.databases).toHaveLength(3);
116
+ for (const db of output.databases) {
117
+ expect(db.status).toBe('initialized');
118
+ }
119
+ });
120
+
121
+ it('creates state.db with all expected tables', () => {
122
+ buildRuntimeInitOutput(tmpDir, true);
123
+ const stateDbPath = path.join(tmpDir, '.pd', 'state.db');
124
+ expect(fs.existsSync(stateDbPath)).toBe(true);
125
+ const tables = getTableNames(stateDbPath);
126
+ for (const expected of EXPECTED_STATE_TABLES) {
127
+ expect(tables).toContain(expected);
128
+ }
129
+ });
130
+
131
+ it('creates trajectory.db with all 16 expected tables', () => {
132
+ buildRuntimeInitOutput(tmpDir, true);
133
+ const trajDbPath = path.join(tmpDir, '.state', 'trajectory.db');
134
+ expect(fs.existsSync(trajDbPath)).toBe(true);
135
+ const tables = getTableNames(trajDbPath);
136
+ expect(tables).toHaveLength(EXPECTED_TRAJECTORY_TABLES.length);
137
+ for (const expected of EXPECTED_TRAJECTORY_TABLES) {
138
+ expect(tables).toContain(expected);
139
+ }
140
+ });
141
+
142
+ it('creates trajectory.db with all expected indexes', () => {
143
+ buildRuntimeInitOutput(tmpDir, true);
144
+ const trajDbPath = path.join(tmpDir, '.state', 'trajectory.db');
145
+ const indexes = getIndexNames(trajDbPath);
146
+ for (const expected of EXPECTED_TRAJECTORY_INDEXES) {
147
+ expect(indexes).toContain(expected);
148
+ }
149
+ });
150
+
151
+ it('creates subagent_workflows.db with all expected tables', () => {
152
+ buildRuntimeInitOutput(tmpDir, true);
153
+ const wfDbPath = path.join(tmpDir, '.state', 'subagent_workflows.db');
154
+ expect(fs.existsSync(wfDbPath)).toBe(true);
155
+ const tables = getTableNames(wfDbPath);
156
+ for (const expected of EXPECTED_WORKFLOW_TABLES) {
157
+ expect(tables).toContain(expected);
158
+ }
159
+ });
160
+
161
+ it('pain_events table has canonical_pain_id and runtime_task_id columns', () => {
162
+ buildRuntimeInitOutput(tmpDir, true);
163
+ const trajDbPath = path.join(tmpDir, '.state', 'trajectory.db');
164
+ const db = new Database(trajDbPath, { readonly: true });
165
+ try {
166
+ const cols = db.prepare('PRAGMA table_info(pain_events)').all() as { name: string }[];
167
+ const colNames = cols.map(c => c.name);
168
+ expect(colNames).toContain('canonical_pain_id');
169
+ expect(colNames).toContain('runtime_task_id');
170
+ expect(colNames).toContain('text');
171
+ } finally {
172
+ db.close();
173
+ }
174
+ });
175
+
176
+ it('evolution_tasks table has V2 migration columns', () => {
177
+ buildRuntimeInitOutput(tmpDir, true);
178
+ const trajDbPath = path.join(tmpDir, '.state', 'trajectory.db');
179
+ const db = new Database(trajDbPath, { readonly: true });
180
+ try {
181
+ const cols = db.prepare('PRAGMA table_info(evolution_tasks)').all() as { name: string }[];
182
+ const colNames = cols.map(c => c.name);
183
+ expect(colNames).toContain('task_kind');
184
+ expect(colNames).toContain('priority');
185
+ expect(colNames).toContain('retry_count');
186
+ expect(colNames).toContain('max_retries');
187
+ expect(colNames).toContain('last_error');
188
+ expect(colNames).toContain('result_ref');
189
+ } finally {
190
+ db.close();
191
+ }
192
+ });
193
+ });
194
+
195
+ // ── EMPTY-02: idempotency ──────────────────────────────────────────────────
196
+
197
+ describe('EMPTY-02: idempotency', () => {
198
+ it('running twice does not error and tables remain intact', () => {
199
+ // First initialization
200
+ const output1 = buildRuntimeInitOutput(tmpDir, true);
201
+ expect(output1.ok).toBe(true);
202
+
203
+ // Capture table counts after first init
204
+ const trajDbPath = path.join(tmpDir, '.state', 'trajectory.db');
205
+ const tables1 = getTableNames(trajDbPath);
206
+
207
+ // Second initialization (should be idempotent)
208
+ const output2 = buildRuntimeInitOutput(tmpDir, true);
209
+ expect(output2.ok).toBe(true);
210
+ expect(output2.databases).toHaveLength(3);
211
+ for (const db of output2.databases) {
212
+ expect(db.status).toBe('initialized');
213
+ }
214
+
215
+ // Table set should be unchanged
216
+ const tables2 = getTableNames(trajDbPath);
217
+ expect(tables2).toEqual(tables1);
218
+ });
219
+
220
+ it('dry-run after confirm does not modify existing DBs', () => {
221
+ // First: confirm mode (real init)
222
+ buildRuntimeInitOutput(tmpDir, true);
223
+ const trajDbPath = path.join(tmpDir, '.state', 'trajectory.db');
224
+ const tablesBefore = getTableNames(trajDbPath);
225
+
226
+ // Second: dry-run mode (should not modify)
227
+ const output = buildRuntimeInitOutput(tmpDir, false);
228
+ expect(output.mode).toBe('dry-run');
229
+
230
+ const tablesAfter = getTableNames(trajDbPath);
231
+ expect(tablesAfter).toEqual(tablesBefore);
232
+ });
233
+ });
234
+ });
@@ -0,0 +1,346 @@
1
+ /**
2
+ * runtime-init tests — pd runtime init command (unit tests with mocked DB layer).
3
+ *
4
+ * Covers:
5
+ * - INIT-01: --dry-run (default) reports 3 DBs as 'skipped', no real writes
6
+ * - INIT-02: --confirm calls all 3 init functions, output shows 'initialized'
7
+ * - INIT-03: --json outputs single parseable JSON object (cli-1)
8
+ * - INIT-04: --dry-run + --confirm mutually exclusive (cli-4)
9
+ * - INIT-05: --confirm failure sets exitCode=1 with reason + nextAction (cli-6)
10
+ * - INIT-06: missing --workspace falls back to resolveWorkspaceDir()
11
+ *
12
+ * ERR refs:
13
+ * - ERR-001/ERR-005: no `as` casts in test code; uses typeof guards
14
+ * - ERR-009: flag conflict fails loud (cli-4)
15
+ */
16
+
17
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
18
+ import * as path from 'node:path';
19
+ import * as os from 'node:os';
20
+ import * as fs from 'node:fs';
21
+
22
+ // ── Hoisted mocks ───────────────────────────────────────────────────────────
23
+
24
+ const mockState = vi.hoisted(() => {
25
+ return {
26
+ initTrajectorySchema: vi.fn(),
27
+ initWorkflowSchema: vi.fn(),
28
+ sqliteConnectionCtor: vi.fn(),
29
+ sqliteConnectionGetDb: vi.fn(),
30
+ sqliteConnectionGetWarnings: vi.fn(),
31
+ sqliteConnectionClose: vi.fn(),
32
+ schemaConformanceCtor: vi.fn(),
33
+ schemaConformanceCheck: vi.fn(),
34
+ resolveWorkspaceDir: vi.fn(),
35
+ };
36
+ });
37
+
38
+ vi.mock('principles-disciple', () => ({
39
+ initTrajectorySchema: mockState.initTrajectorySchema,
40
+ initWorkflowSchema: mockState.initWorkflowSchema,
41
+ }));
42
+
43
+ vi.mock('@principles/core/runtime-v2', () => ({
44
+ SqliteConnection: mockState.sqliteConnectionCtor,
45
+ SchemaConformanceReadModel: mockState.schemaConformanceCtor,
46
+ }));
47
+
48
+ vi.mock('../../src/resolve-workspace.js', () => ({
49
+ resolveWorkspaceDir: mockState.resolveWorkspaceDir,
50
+ }));
51
+
52
+ // Import after mocks are set up
53
+ import { buildRuntimeInitOutput, handleRuntimeInit } from '../../src/commands/runtime-init.js';
54
+
55
+ // ── Helpers ─────────────────────────────────────────────────────────────────
56
+
57
+ function mkTmpDir(): string {
58
+ return fs.mkdtempSync(path.join(os.tmpdir(), 'pd-runtime-init-test-'));
59
+ }
60
+
61
+ function rmTmpDir(dir: string): void {
62
+ try { fs.rmSync(dir, { recursive: true, force: true }); } catch { /* ignore */ }
63
+ }
64
+
65
+ function setupDefaultConfirmMocks(): void {
66
+ // SqliteConnection mock: constructor returns instance with getDb/getSchemaInitWarnings/close
67
+ // NOTE: Must use `function` (not arrow function) because vitest 4.x requires
68
+ // function/class implementations for mocks called with `new`.
69
+ mockState.sqliteConnectionCtor.mockImplementation(function () {
70
+ return {
71
+ getDb: mockState.sqliteConnectionGetDb,
72
+ getSchemaInitWarnings: mockState.sqliteConnectionGetWarnings,
73
+ close: mockState.sqliteConnectionClose,
74
+ };
75
+ });
76
+ mockState.sqliteConnectionGetDb.mockReturnValue({});
77
+ mockState.sqliteConnectionGetWarnings.mockReturnValue([]);
78
+ mockState.sqliteConnectionClose.mockReturnValue(undefined);
79
+
80
+ // initTrajectorySchema mock
81
+ mockState.initTrajectorySchema.mockReturnValue({
82
+ tables: ['schema_version', 'sessions', 'pain_events', 'evolution_tasks'],
83
+ warnings: [],
84
+ });
85
+
86
+ // initWorkflowSchema mock
87
+ mockState.initWorkflowSchema.mockReturnValue({
88
+ tables: ['schema_version', 'subagent_workflows', 'subagent_workflow_events'],
89
+ warnings: [],
90
+ });
91
+
92
+ // SchemaConformanceReadModel mock
93
+ mockState.schemaConformanceCtor.mockImplementation(function () {
94
+ return {
95
+ check: mockState.schemaConformanceCheck,
96
+ };
97
+ });
98
+ mockState.schemaConformanceCheck.mockReturnValue({ overallStatus: 'ok' });
99
+ }
100
+
101
+ // ── Tests ───────────────────────────────────────────────────────────────────
102
+
103
+ describe('pd runtime init', () => {
104
+ beforeEach(() => {
105
+ vi.clearAllMocks();
106
+ setupDefaultConfirmMocks();
107
+ });
108
+
109
+ // ── INIT-01: dry-run (default) ─────────────────────────────────────────────
110
+
111
+ describe('INIT-01: dry-run (default)', () => {
112
+ it('reports 3 DBs as skipped without calling init functions', () => {
113
+ const tmp = mkTmpDir();
114
+ try {
115
+ const output = buildRuntimeInitOutput(tmp, false);
116
+ expect(output.ok).toBe(true);
117
+ expect(output.mode).toBe('dry-run');
118
+ expect(output.databases).toHaveLength(3);
119
+ for (const db of output.databases) {
120
+ expect(db.status).toBe('skipped');
121
+ }
122
+ // No init functions should be called in dry-run mode
123
+ expect(mockState.initTrajectorySchema).not.toHaveBeenCalled();
124
+ expect(mockState.initWorkflowSchema).not.toHaveBeenCalled();
125
+ expect(mockState.sqliteConnectionCtor).not.toHaveBeenCalled();
126
+ } finally { rmTmpDir(tmp); }
127
+ });
128
+
129
+ it('includes expected table names in dry-run output for each DB', () => {
130
+ const tmp = mkTmpDir();
131
+ try {
132
+ const output = buildRuntimeInitOutput(tmp, false);
133
+ const stateDb = output.databases.find(d => d.name === 'state.db');
134
+ const trajDb = output.databases.find(d => d.name === 'trajectory.db');
135
+ const wfDb = output.databases.find(d => d.name === 'subagent_workflows.db');
136
+ expect(stateDb?.tables).toContain('tasks');
137
+ expect(stateDb?.tables).toContain('runs');
138
+ expect(trajDb?.tables).toContain('pain_events');
139
+ expect(trajDb?.tables).toContain('sessions');
140
+ expect(wfDb?.tables).toContain('subagent_workflows');
141
+ } finally { rmTmpDir(tmp); }
142
+ });
143
+ });
144
+
145
+ // ── INIT-02: --confirm ─────────────────────────────────────────────────────
146
+
147
+ describe('INIT-02: --confirm', () => {
148
+ it('calls all 3 init functions and reports initialized status', () => {
149
+ const tmp = mkTmpDir();
150
+ try {
151
+ const output = buildRuntimeInitOutput(tmp, true);
152
+ expect(output.ok).toBe(true);
153
+ expect(output.mode).toBe('confirm');
154
+ expect(output.databases).toHaveLength(3);
155
+ for (const db of output.databases) {
156
+ expect(db.status).toBe('initialized');
157
+ }
158
+ expect(mockState.sqliteConnectionCtor).toHaveBeenCalledTimes(1);
159
+ expect(mockState.sqliteConnectionGetDb).toHaveBeenCalledTimes(1);
160
+ expect(mockState.initTrajectorySchema).toHaveBeenCalledWith(tmp);
161
+ expect(mockState.initWorkflowSchema).toHaveBeenCalledWith(tmp);
162
+ } finally { rmTmpDir(tmp); }
163
+ });
164
+
165
+ it('runs schema conformance check after initialization', () => {
166
+ const tmp = mkTmpDir();
167
+ try {
168
+ buildRuntimeInitOutput(tmp, true);
169
+ expect(mockState.schemaConformanceCtor).toHaveBeenCalledTimes(1);
170
+ expect(mockState.schemaConformanceCheck).toHaveBeenCalledTimes(1);
171
+ } finally { rmTmpDir(tmp); }
172
+ });
173
+ });
174
+
175
+ // ── INIT-03: --json output (cli-1) ─────────────────────────────────────────
176
+
177
+ describe('INIT-03: --json output (cli-1)', () => {
178
+ let stdoutSpy: ReturnType<typeof vi.spyOn>;
179
+ let originalExitCode: number | undefined;
180
+
181
+ beforeEach(() => {
182
+ stdoutSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
183
+ originalExitCode = process.exitCode;
184
+ process.exitCode = undefined;
185
+ });
186
+
187
+ afterEach(() => {
188
+ stdoutSpy.mockRestore();
189
+ process.exitCode = originalExitCode;
190
+ });
191
+
192
+ it('outputs exactly one parseable JSON object to stdout', async () => {
193
+ const tmp = mkTmpDir();
194
+ try {
195
+ await handleRuntimeInit({ workspace: tmp, json: true });
196
+ expect(stdoutSpy).toHaveBeenCalledTimes(1);
197
+ const output = stdoutSpy.mock.calls[0][0] as string;
198
+ const parsed = JSON.parse(output);
199
+ expect(typeof parsed).toBe('object');
200
+ expect(parsed).not.toBeNull();
201
+ expect(Array.isArray(parsed)).toBe(false);
202
+ expect(parsed).toHaveProperty('ok');
203
+ expect(parsed).toHaveProperty('mode');
204
+ expect(parsed).toHaveProperty('databases');
205
+ } finally { rmTmpDir(tmp); }
206
+ });
207
+
208
+ it('does not set exitCode=1 on successful dry-run', async () => {
209
+ const tmp = mkTmpDir();
210
+ try {
211
+ await handleRuntimeInit({ workspace: tmp, json: true });
212
+ expect(process.exitCode).toBeUndefined();
213
+ } finally { rmTmpDir(tmp); }
214
+ });
215
+ });
216
+
217
+ // ── INIT-04: --dry-run + --confirm mutex (cli-4) ───────────────────────────
218
+
219
+ describe('INIT-04: --dry-run + --confirm mutex (cli-4)', () => {
220
+ let stdoutSpy: ReturnType<typeof vi.spyOn>;
221
+ let stderrSpy: ReturnType<typeof vi.spyOn>;
222
+ let originalExitCode: number | undefined;
223
+
224
+ beforeEach(() => {
225
+ stdoutSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
226
+ stderrSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
227
+ originalExitCode = process.exitCode;
228
+ process.exitCode = undefined;
229
+ });
230
+
231
+ afterEach(() => {
232
+ stdoutSpy.mockRestore();
233
+ stderrSpy.mockRestore();
234
+ process.exitCode = originalExitCode;
235
+ });
236
+
237
+ it('sets exitCode=1 when both --dry-run and --confirm are specified', async () => {
238
+ await handleRuntimeInit({ dryRun: true, confirm: true, json: true });
239
+ expect(process.exitCode).toBe(1);
240
+ });
241
+
242
+ it('emits flag conflict JSON with reason and nextAction', async () => {
243
+ await handleRuntimeInit({ dryRun: true, confirm: true, json: true });
244
+ expect(stdoutSpy).toHaveBeenCalledTimes(1);
245
+ const output = stdoutSpy.mock.calls[0][0] as string;
246
+ const parsed = JSON.parse(output);
247
+ expect(parsed.ok).toBe(false);
248
+ expect(parsed.reason).toBeTruthy();
249
+ expect(parsed.nextAction).toBeTruthy();
250
+ });
251
+
252
+ it('does not call any init functions on flag conflict', async () => {
253
+ await handleRuntimeInit({ dryRun: true, confirm: true, json: true });
254
+ expect(mockState.initTrajectorySchema).not.toHaveBeenCalled();
255
+ expect(mockState.initWorkflowSchema).not.toHaveBeenCalled();
256
+ expect(mockState.sqliteConnectionCtor).not.toHaveBeenCalled();
257
+ });
258
+ });
259
+
260
+ // ── INIT-05: --confirm failure (cli-6) ─────────────────────────────────────
261
+
262
+ describe('INIT-05: --confirm failure (cli-6)', () => {
263
+ let stdoutSpy: ReturnType<typeof vi.spyOn>;
264
+ let originalExitCode: number | undefined;
265
+
266
+ beforeEach(() => {
267
+ stdoutSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
268
+ originalExitCode = process.exitCode;
269
+ process.exitCode = undefined;
270
+ });
271
+
272
+ afterEach(() => {
273
+ stdoutSpy.mockRestore();
274
+ process.exitCode = originalExitCode;
275
+ });
276
+
277
+ it('sets exitCode=1 and includes reason + nextAction when state.db fails', () => {
278
+ const tmp = mkTmpDir();
279
+ try {
280
+ mockState.sqliteConnectionGetDb.mockImplementation(() => {
281
+ throw new Error('disk full');
282
+ });
283
+ const output = buildRuntimeInitOutput(tmp, true);
284
+ expect(output.ok).toBe(false);
285
+ expect(output.reason).toContain('state.db');
286
+ expect(output.reason).toContain('disk full');
287
+ expect(output.nextAction).toBeTruthy();
288
+ } finally { rmTmpDir(tmp); }
289
+ });
290
+
291
+ it('sets exitCode=1 and includes reason when trajectory.db fails', () => {
292
+ const tmp = mkTmpDir();
293
+ try {
294
+ mockState.initTrajectorySchema.mockImplementation(() => {
295
+ throw new Error('permission denied');
296
+ });
297
+ const output = buildRuntimeInitOutput(tmp, true);
298
+ expect(output.ok).toBe(false);
299
+ expect(output.reason).toContain('trajectory.db');
300
+ expect(output.reason).toContain('permission denied');
301
+ } finally { rmTmpDir(tmp); }
302
+ });
303
+
304
+ it('sets exitCode=1 and includes reason when subagent_workflows.db fails', () => {
305
+ const tmp = mkTmpDir();
306
+ try {
307
+ mockState.initWorkflowSchema.mockImplementation(() => {
308
+ throw new Error('locked');
309
+ });
310
+ const output = buildRuntimeInitOutput(tmp, true);
311
+ expect(output.ok).toBe(false);
312
+ expect(output.reason).toContain('subagent_workflows.db');
313
+ expect(output.reason).toContain('locked');
314
+ } finally { rmTmpDir(tmp); }
315
+ });
316
+
317
+ it('handler sets process.exitCode=1 on failure with --json', async () => {
318
+ const tmp = mkTmpDir();
319
+ try {
320
+ mockState.sqliteConnectionGetDb.mockImplementation(() => {
321
+ throw new Error('init failed');
322
+ });
323
+ await handleRuntimeInit({ workspace: tmp, confirm: true, json: true });
324
+ expect(process.exitCode).toBe(1);
325
+ const output = stdoutSpy.mock.calls[0][0] as string;
326
+ const parsed = JSON.parse(output);
327
+ expect(parsed.ok).toBe(false);
328
+ expect(parsed.reason).toBeTruthy();
329
+ expect(parsed.nextAction).toBeTruthy();
330
+ } finally { rmTmpDir(tmp); }
331
+ });
332
+ });
333
+
334
+ // ── INIT-06: missing --workspace ───────────────────────────────────────────
335
+
336
+ describe('INIT-06: missing --workspace', () => {
337
+ it('falls back to resolveWorkspaceDir() when workspace is not specified', async () => {
338
+ const tmp = mkTmpDir();
339
+ mockState.resolveWorkspaceDir.mockReturnValue(tmp);
340
+ try {
341
+ await handleRuntimeInit({ json: true });
342
+ expect(mockState.resolveWorkspaceDir).toHaveBeenCalledTimes(1);
343
+ } finally { rmTmpDir(tmp); }
344
+ });
345
+ });
346
+ });