@principles/pd-cli 1.73.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 (298) hide show
  1. package/README.md +90 -0
  2. package/dist/commands/artifact.d.ts +14 -0
  3. package/dist/commands/artifact.d.ts.map +1 -0
  4. package/dist/commands/artifact.js +67 -0
  5. package/dist/commands/artifact.js.map +1 -0
  6. package/dist/commands/candidate.d.ts +83 -0
  7. package/dist/commands/candidate.d.ts.map +1 -0
  8. package/dist/commands/candidate.js +891 -0
  9. package/dist/commands/candidate.js.map +1 -0
  10. package/dist/commands/central-sync.d.ts +10 -0
  11. package/dist/commands/central-sync.d.ts.map +1 -0
  12. package/dist/commands/central-sync.js +32 -0
  13. package/dist/commands/central-sync.js.map +1 -0
  14. package/dist/commands/console.d.ts +9 -0
  15. package/dist/commands/console.d.ts.map +1 -0
  16. package/dist/commands/console.js +114 -0
  17. package/dist/commands/console.js.map +1 -0
  18. package/dist/commands/context.d.ts +7 -0
  19. package/dist/commands/context.d.ts.map +1 -0
  20. package/dist/commands/context.js +55 -0
  21. package/dist/commands/context.js.map +1 -0
  22. package/dist/commands/demo-story-a.d.ts +12 -0
  23. package/dist/commands/demo-story-a.d.ts.map +1 -0
  24. package/dist/commands/demo-story-a.js +175 -0
  25. package/dist/commands/demo-story-a.js.map +1 -0
  26. package/dist/commands/diagnose.d.ts +35 -0
  27. package/dist/commands/diagnose.d.ts.map +1 -0
  28. package/dist/commands/diagnose.js +390 -0
  29. package/dist/commands/diagnose.js.map +1 -0
  30. package/dist/commands/evolution-tasks-list.d.ts +15 -0
  31. package/dist/commands/evolution-tasks-list.d.ts.map +1 -0
  32. package/dist/commands/evolution-tasks-list.js +34 -0
  33. package/dist/commands/evolution-tasks-list.js.map +1 -0
  34. package/dist/commands/evolution-tasks-show.d.ts +14 -0
  35. package/dist/commands/evolution-tasks-show.d.ts.map +1 -0
  36. package/dist/commands/evolution-tasks-show.js +52 -0
  37. package/dist/commands/evolution-tasks-show.js.map +1 -0
  38. package/dist/commands/flow.d.ts +7 -0
  39. package/dist/commands/flow.d.ts.map +1 -0
  40. package/dist/commands/flow.js +57 -0
  41. package/dist/commands/flow.js.map +1 -0
  42. package/dist/commands/health.d.ts +16 -0
  43. package/dist/commands/health.d.ts.map +1 -0
  44. package/dist/commands/health.js +150 -0
  45. package/dist/commands/health.js.map +1 -0
  46. package/dist/commands/history.d.ts +11 -0
  47. package/dist/commands/history.d.ts.map +1 -0
  48. package/dist/commands/history.js +50 -0
  49. package/dist/commands/history.js.map +1 -0
  50. package/dist/commands/legacy-cleanup.d.ts +27 -0
  51. package/dist/commands/legacy-cleanup.d.ts.map +1 -0
  52. package/dist/commands/legacy-cleanup.js +171 -0
  53. package/dist/commands/legacy-cleanup.js.map +1 -0
  54. package/dist/commands/legacy-import.d.ts +7 -0
  55. package/dist/commands/legacy-import.d.ts.map +1 -0
  56. package/dist/commands/legacy-import.js +86 -0
  57. package/dist/commands/legacy-import.js.map +1 -0
  58. package/dist/commands/pain-record.d.ts +10 -0
  59. package/dist/commands/pain-record.d.ts.map +1 -0
  60. package/dist/commands/pain-record.js +162 -0
  61. package/dist/commands/pain-record.js.map +1 -0
  62. package/dist/commands/proven-channel-baseline.d.ts +12 -0
  63. package/dist/commands/proven-channel-baseline.d.ts.map +1 -0
  64. package/dist/commands/proven-channel-baseline.js +97 -0
  65. package/dist/commands/proven-channel-baseline.js.map +1 -0
  66. package/dist/commands/remediation-output.d.ts +40 -0
  67. package/dist/commands/remediation-output.d.ts.map +1 -0
  68. package/dist/commands/remediation-output.js +23 -0
  69. package/dist/commands/remediation-output.js.map +1 -0
  70. package/dist/commands/run.d.ts +10 -0
  71. package/dist/commands/run.d.ts.map +1 -0
  72. package/dist/commands/run.js +68 -0
  73. package/dist/commands/run.js.map +1 -0
  74. package/dist/commands/runtime-activation.d.ts +11 -0
  75. package/dist/commands/runtime-activation.d.ts.map +1 -0
  76. package/dist/commands/runtime-activation.js +150 -0
  77. package/dist/commands/runtime-activation.js.map +1 -0
  78. package/dist/commands/runtime-canary.d.ts +30 -0
  79. package/dist/commands/runtime-canary.d.ts.map +1 -0
  80. package/dist/commands/runtime-canary.js +343 -0
  81. package/dist/commands/runtime-canary.js.map +1 -0
  82. package/dist/commands/runtime-diagnostics-export.d.ts +20 -0
  83. package/dist/commands/runtime-diagnostics-export.d.ts.map +1 -0
  84. package/dist/commands/runtime-diagnostics-export.js +177 -0
  85. package/dist/commands/runtime-diagnostics-export.js.map +1 -0
  86. package/dist/commands/runtime-features.d.ts +26 -0
  87. package/dist/commands/runtime-features.d.ts.map +1 -0
  88. package/dist/commands/runtime-features.js +70 -0
  89. package/dist/commands/runtime-features.js.map +1 -0
  90. package/dist/commands/runtime-gfi-snapshot.d.ts +7 -0
  91. package/dist/commands/runtime-gfi-snapshot.d.ts.map +1 -0
  92. package/dist/commands/runtime-gfi-snapshot.js +101 -0
  93. package/dist/commands/runtime-gfi-snapshot.js.map +1 -0
  94. package/dist/commands/runtime-health-snapshot.d.ts +7 -0
  95. package/dist/commands/runtime-health-snapshot.d.ts.map +1 -0
  96. package/dist/commands/runtime-health-snapshot.js +93 -0
  97. package/dist/commands/runtime-health-snapshot.js.map +1 -0
  98. package/dist/commands/runtime-idle-trigger.d.ts +12 -0
  99. package/dist/commands/runtime-idle-trigger.d.ts.map +1 -0
  100. package/dist/commands/runtime-idle-trigger.js +102 -0
  101. package/dist/commands/runtime-idle-trigger.js.map +1 -0
  102. package/dist/commands/runtime-internalization-enqueue-successors.d.ts +9 -0
  103. package/dist/commands/runtime-internalization-enqueue-successors.d.ts.map +1 -0
  104. package/dist/commands/runtime-internalization-enqueue-successors.js +393 -0
  105. package/dist/commands/runtime-internalization-enqueue-successors.js.map +1 -0
  106. package/dist/commands/runtime-internalization-integrity-repair.d.ts +9 -0
  107. package/dist/commands/runtime-internalization-integrity-repair.d.ts.map +1 -0
  108. package/dist/commands/runtime-internalization-integrity-repair.js +54 -0
  109. package/dist/commands/runtime-internalization-integrity-repair.js.map +1 -0
  110. package/dist/commands/runtime-internalization-integrity.d.ts +7 -0
  111. package/dist/commands/runtime-internalization-integrity.d.ts.map +1 -0
  112. package/dist/commands/runtime-internalization-integrity.js +53 -0
  113. package/dist/commands/runtime-internalization-integrity.js.map +1 -0
  114. package/dist/commands/runtime-internalization-queue.d.ts +7 -0
  115. package/dist/commands/runtime-internalization-queue.d.ts.map +1 -0
  116. package/dist/commands/runtime-internalization-queue.js +85 -0
  117. package/dist/commands/runtime-internalization-queue.js.map +1 -0
  118. package/dist/commands/runtime-internalization-run-once.d.ts +12 -0
  119. package/dist/commands/runtime-internalization-run-once.d.ts.map +1 -0
  120. package/dist/commands/runtime-internalization-run-once.js +546 -0
  121. package/dist/commands/runtime-internalization-run-once.js.map +1 -0
  122. package/dist/commands/runtime-internalization-wake-once.d.ts +8 -0
  123. package/dist/commands/runtime-internalization-wake-once.d.ts.map +1 -0
  124. package/dist/commands/runtime-internalization-wake-once.js +72 -0
  125. package/dist/commands/runtime-internalization-wake-once.js.map +1 -0
  126. package/dist/commands/runtime-pain-flood-simulation.d.ts +10 -0
  127. package/dist/commands/runtime-pain-flood-simulation.d.ts.map +1 -0
  128. package/dist/commands/runtime-pain-flood-simulation.js +104 -0
  129. package/dist/commands/runtime-pain-flood-simulation.js.map +1 -0
  130. package/dist/commands/runtime-pruning.d.ts +45 -0
  131. package/dist/commands/runtime-pruning.d.ts.map +1 -0
  132. package/dist/commands/runtime-pruning.js +355 -0
  133. package/dist/commands/runtime-pruning.js.map +1 -0
  134. package/dist/commands/runtime-recovery.d.ts +9 -0
  135. package/dist/commands/runtime-recovery.d.ts.map +1 -0
  136. package/dist/commands/runtime-recovery.js +94 -0
  137. package/dist/commands/runtime-recovery.js.map +1 -0
  138. package/dist/commands/runtime-synthetic-baseline.d.ts +7 -0
  139. package/dist/commands/runtime-synthetic-baseline.d.ts.map +1 -0
  140. package/dist/commands/runtime-synthetic-baseline.js +59 -0
  141. package/dist/commands/runtime-synthetic-baseline.js.map +1 -0
  142. package/dist/commands/runtime-uat.d.ts +52 -0
  143. package/dist/commands/runtime-uat.d.ts.map +1 -0
  144. package/dist/commands/runtime-uat.js +274 -0
  145. package/dist/commands/runtime-uat.js.map +1 -0
  146. package/dist/commands/runtime.d.ts +20 -0
  147. package/dist/commands/runtime.d.ts.map +1 -0
  148. package/dist/commands/runtime.js +256 -0
  149. package/dist/commands/runtime.js.map +1 -0
  150. package/dist/commands/samples-list.d.ts +11 -0
  151. package/dist/commands/samples-list.d.ts.map +1 -0
  152. package/dist/commands/samples-list.js +37 -0
  153. package/dist/commands/samples-list.js.map +1 -0
  154. package/dist/commands/samples-review.d.ts +14 -0
  155. package/dist/commands/samples-review.d.ts.map +1 -0
  156. package/dist/commands/samples-review.js +22 -0
  157. package/dist/commands/samples-review.js.map +1 -0
  158. package/dist/commands/task.d.ts +14 -0
  159. package/dist/commands/task.d.ts.map +1 -0
  160. package/dist/commands/task.js +92 -0
  161. package/dist/commands/task.js.map +1 -0
  162. package/dist/commands/trace.d.ts +19 -0
  163. package/dist/commands/trace.d.ts.map +1 -0
  164. package/dist/commands/trace.js +154 -0
  165. package/dist/commands/trace.js.map +1 -0
  166. package/dist/commands/trajectory.d.ts +11 -0
  167. package/dist/commands/trajectory.d.ts.map +1 -0
  168. package/dist/commands/trajectory.js +47 -0
  169. package/dist/commands/trajectory.js.map +1 -0
  170. package/dist/index.d.ts +9 -0
  171. package/dist/index.d.ts.map +1 -0
  172. package/dist/index.js +736 -0
  173. package/dist/index.js.map +1 -0
  174. package/dist/legacy/legacy-import.d.ts +15 -0
  175. package/dist/legacy/legacy-import.d.ts.map +1 -0
  176. package/dist/legacy/legacy-import.js +141 -0
  177. package/dist/legacy/legacy-import.js.map +1 -0
  178. package/dist/legacy/session-history-import.d.ts +26 -0
  179. package/dist/legacy/session-history-import.d.ts.map +1 -0
  180. package/dist/legacy/session-history-import.js +151 -0
  181. package/dist/legacy/session-history-import.js.map +1 -0
  182. package/dist/principle-tree-ledger-adapter.d.ts +12 -0
  183. package/dist/principle-tree-ledger-adapter.d.ts.map +1 -0
  184. package/dist/principle-tree-ledger-adapter.js +12 -0
  185. package/dist/principle-tree-ledger-adapter.js.map +1 -0
  186. package/dist/resolve-workspace.d.ts +12 -0
  187. package/dist/resolve-workspace.d.ts.map +1 -0
  188. package/dist/resolve-workspace.js +20 -0
  189. package/dist/resolve-workspace.js.map +1 -0
  190. package/dist/services/demo-story-a-runner.d.ts +8 -0
  191. package/dist/services/demo-story-a-runner.d.ts.map +1 -0
  192. package/dist/services/demo-story-a-runner.js +369 -0
  193. package/dist/services/demo-story-a-runner.js.map +1 -0
  194. package/dist/services/feature-flag-loader.d.ts +6 -0
  195. package/dist/services/feature-flag-loader.d.ts.map +1 -0
  196. package/dist/services/feature-flag-loader.js +54 -0
  197. package/dist/services/feature-flag-loader.js.map +1 -0
  198. package/dist/services/pain-flood-simulation-runner.d.ts +10 -0
  199. package/dist/services/pain-flood-simulation-runner.d.ts.map +1 -0
  200. package/dist/services/pain-flood-simulation-runner.js +289 -0
  201. package/dist/services/pain-flood-simulation-runner.js.map +1 -0
  202. package/dist/services/proven-channel-baseline-runner.d.ts +12 -0
  203. package/dist/services/proven-channel-baseline-runner.d.ts.map +1 -0
  204. package/dist/services/proven-channel-baseline-runner.js +114 -0
  205. package/dist/services/proven-channel-baseline-runner.js.map +1 -0
  206. package/dist/services/synthetic-baseline-runner.d.ts +8 -0
  207. package/dist/services/synthetic-baseline-runner.d.ts.map +1 -0
  208. package/dist/services/synthetic-baseline-runner.js +251 -0
  209. package/dist/services/synthetic-baseline-runner.js.map +1 -0
  210. package/package.json +35 -0
  211. package/src/commands/artifact.ts +82 -0
  212. package/src/commands/candidate.ts +1117 -0
  213. package/src/commands/central-sync.ts +44 -0
  214. package/src/commands/console.ts +121 -0
  215. package/src/commands/context.ts +72 -0
  216. package/src/commands/demo-story-a.ts +195 -0
  217. package/src/commands/diagnose.ts +452 -0
  218. package/src/commands/evolution-tasks-list.ts +44 -0
  219. package/src/commands/evolution-tasks-show.ts +60 -0
  220. package/src/commands/flow.ts +60 -0
  221. package/src/commands/health.ts +189 -0
  222. package/src/commands/history.ts +63 -0
  223. package/src/commands/legacy-cleanup.ts +206 -0
  224. package/src/commands/legacy-import.ts +104 -0
  225. package/src/commands/pain-record.ts +167 -0
  226. package/src/commands/proven-channel-baseline.ts +113 -0
  227. package/src/commands/remediation-output.ts +66 -0
  228. package/src/commands/run.ts +89 -0
  229. package/src/commands/runtime-activation.ts +176 -0
  230. package/src/commands/runtime-canary.ts +371 -0
  231. package/src/commands/runtime-diagnostics-export.ts +229 -0
  232. package/src/commands/runtime-features.ts +103 -0
  233. package/src/commands/runtime-gfi-snapshot.ts +135 -0
  234. package/src/commands/runtime-health-snapshot.ts +106 -0
  235. package/src/commands/runtime-internalization-enqueue-successors.ts +479 -0
  236. package/src/commands/runtime-internalization-integrity-repair.ts +69 -0
  237. package/src/commands/runtime-internalization-integrity.ts +63 -0
  238. package/src/commands/runtime-internalization-queue.ts +106 -0
  239. package/src/commands/runtime-internalization-run-once.ts +658 -0
  240. package/src/commands/runtime-internalization-wake-once.ts +87 -0
  241. package/src/commands/runtime-pain-flood-simulation.ts +121 -0
  242. package/src/commands/runtime-pruning.ts +438 -0
  243. package/src/commands/runtime-recovery.ts +107 -0
  244. package/src/commands/runtime-synthetic-baseline.ts +70 -0
  245. package/src/commands/runtime-uat.ts +339 -0
  246. package/src/commands/runtime.ts +281 -0
  247. package/src/commands/samples-list.ts +43 -0
  248. package/src/commands/samples-review.ts +32 -0
  249. package/src/commands/task.ts +130 -0
  250. package/src/commands/trace.ts +174 -0
  251. package/src/commands/trajectory.ts +64 -0
  252. package/src/index.ts +829 -0
  253. package/src/legacy/legacy-import.ts +179 -0
  254. package/src/legacy/session-history-import.ts +231 -0
  255. package/src/principle-tree-ledger-adapter.ts +13 -0
  256. package/src/resolve-workspace.ts +20 -0
  257. package/src/services/demo-story-a-runner.ts +472 -0
  258. package/src/services/feature-flag-loader.ts +73 -0
  259. package/src/services/pain-flood-simulation-runner.ts +354 -0
  260. package/src/services/proven-channel-baseline-runner.ts +150 -0
  261. package/src/services/synthetic-baseline-runner.ts +291 -0
  262. package/tests/commands/candidate-audit-repair.test.ts +338 -0
  263. package/tests/commands/candidate-intake.test.ts +589 -0
  264. package/tests/commands/candidate-internalization-backfill.test.ts +480 -0
  265. package/tests/commands/candidate-internalize.test.ts +272 -0
  266. package/tests/commands/candidate-route.test.ts +328 -0
  267. package/tests/commands/candidate-show.test.ts +95 -0
  268. package/tests/commands/cli-command-tree.test.ts +64 -0
  269. package/tests/commands/context.test.ts +114 -0
  270. package/tests/commands/demo-story-a.test.ts +255 -0
  271. package/tests/commands/diagnose.test.ts +792 -0
  272. package/tests/commands/health.test.ts +330 -0
  273. package/tests/commands/pain-record.test.ts +316 -0
  274. package/tests/commands/plugin-config-resolution-cutover.test.ts +220 -0
  275. package/tests/commands/proven-channel-baseline.test.ts +441 -0
  276. package/tests/commands/runtime-activation.test.ts +168 -0
  277. package/tests/commands/runtime-canary.test.ts +369 -0
  278. package/tests/commands/runtime-diagnostics-export.test.ts +170 -0
  279. package/tests/commands/runtime-features.test.ts +114 -0
  280. package/tests/commands/runtime-health-snapshot.test.ts +357 -0
  281. package/tests/commands/runtime-internalization-enqueue-successors.test.ts +803 -0
  282. package/tests/commands/runtime-internalization-integrity-repair.test.ts +169 -0
  283. package/tests/commands/runtime-internalization-integrity.test.ts +102 -0
  284. package/tests/commands/runtime-internalization-queue.test.ts +252 -0
  285. package/tests/commands/runtime-internalization-run-once.test.ts +1318 -0
  286. package/tests/commands/runtime-internalization-wake-once.test.ts +170 -0
  287. package/tests/commands/runtime-internalization.test.ts +52 -0
  288. package/tests/commands/runtime-pain-flood-simulation.test.ts +418 -0
  289. package/tests/commands/runtime-pruning.test.ts +693 -0
  290. package/tests/commands/runtime-recovery.test.ts +96 -0
  291. package/tests/commands/runtime-synthetic-baseline.test.ts +249 -0
  292. package/tests/commands/runtime-uat.test.ts +397 -0
  293. package/tests/commands/runtime.test.ts +262 -0
  294. package/tests/commands/trace.test.ts +314 -0
  295. package/tests/e2e/candidate-intake-e2e.test.ts +316 -0
  296. package/tests/services/feature-flag-loader.test.ts +207 -0
  297. package/tests/services/proven-channel-baseline-runner.test.ts +30 -0
  298. package/tsconfig.json +26 -0
@@ -0,0 +1,170 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
+ import * as fs from 'fs';
3
+ import * as path from 'path';
4
+ import * as os from 'os';
5
+
6
+ const mockSchemaCheck = vi.hoisted(() => vi.fn());
7
+ const mockHealthSnapshot = vi.hoisted(() => vi.fn());
8
+ const mockHealthClose = vi.hoisted(() => vi.fn().mockResolvedValue(undefined));
9
+ const mockOrphanCandidates = vi.hoisted(() => vi.fn());
10
+ const mockQueueSnapshot = vi.hoisted(() => vi.fn());
11
+ const mockQueueClose = vi.hoisted(() => vi.fn().mockResolvedValue(undefined));
12
+ const mockAuditConsistency = vi.hoisted(() => vi.fn());
13
+ const mockBuildGfiSnapshot = vi.hoisted(() => vi.fn());
14
+ const mockIntegrityCheck = vi.hoisted(() => vi.fn());
15
+
16
+ vi.mock('../../src/resolve-workspace.js', () => ({
17
+ resolveWorkspaceDir: vi.fn().mockReturnValue('/fake/workspace'),
18
+ }));
19
+
20
+ vi.mock('@principles/core/runtime-v2', () => ({
21
+ SchemaConformanceReadModel: vi.fn().mockImplementation(function () {
22
+ return { check: mockSchemaCheck };
23
+ }),
24
+ OperatorHealthReadModel: vi.fn().mockImplementation(function () {
25
+ return { getSnapshot: mockHealthSnapshot, close: mockHealthClose };
26
+ }),
27
+ PruningReadModel: vi.fn().mockImplementation(function () {
28
+ return { getOrphanDerivedCandidates: mockOrphanCandidates };
29
+ }),
30
+ createInternalizationQueueReadModel: vi.fn().mockResolvedValue({
31
+ readModel: { getSnapshot: mockQueueSnapshot },
32
+ close: mockQueueClose,
33
+ }),
34
+ InternalizationChainIntegrityReadModel: vi.fn().mockImplementation(function () {
35
+ return { check: mockIntegrityCheck };
36
+ }),
37
+ auditCandidateLedgerConsistency: mockAuditConsistency,
38
+ buildGfiWorkspaceSnapshot: mockBuildGfiSnapshot,
39
+ }));
40
+
41
+ vi.mock('../../src/commands/runtime-canary.js', () => ({
42
+ runCanaryChecks: vi.fn().mockResolvedValue({
43
+ overallStatus: 'healthy',
44
+ checks: [],
45
+ recommendedNextActions: [],
46
+ generatedAt: new Date().toISOString(),
47
+ }),
48
+ }));
49
+
50
+ import { exportDiagnosticsBundle } from '../../src/commands/runtime-diagnostics-export.js';
51
+
52
+ function healthySchemaResult() {
53
+ return {
54
+ overallStatus: 'ok' as const,
55
+ checkedDatabasePath: '/fake/workspace/.pd/state.db',
56
+ tables: { tasks: { exists: true, missingColumns: [] } },
57
+ indexes: { missingIndexes: [] },
58
+ migrationsNeeded: [],
59
+ generatedAt: new Date().toISOString(),
60
+ };
61
+ }
62
+
63
+ function healthyIntegrityResult() {
64
+ return {
65
+ overallStatus: 'ok' as const,
66
+ brokenLinks: [],
67
+ chainSummaries: { totalCandidates: 0, totalDreamerTasks: 0, totalPhilosopherTasks: 0, totalPIArtifacts: 0, chainsWithBrokenLinks: 0 },
68
+ generatedAt: new Date().toISOString(),
69
+ };
70
+ }
71
+
72
+ describe('exportDiagnosticsBundle', () => {
73
+ let tempDir: string;
74
+
75
+ beforeEach(() => {
76
+ vi.clearAllMocks();
77
+ tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'diag-test-'));
78
+
79
+ mockSchemaCheck.mockReturnValue(healthySchemaResult());
80
+ mockHealthSnapshot.mockResolvedValue({ overallStatus: 'healthy', generatedAt: new Date().toISOString() });
81
+ mockHealthClose.mockResolvedValue(undefined);
82
+ mockOrphanCandidates.mockReturnValue({ candidates: [], dbReadable: true });
83
+ mockQueueSnapshot.mockResolvedValue({ pendingCount: 0, readyTasks: [] });
84
+ mockQueueClose.mockResolvedValue(undefined);
85
+ mockAuditConsistency.mockResolvedValue({ status: 'ok', consumedCount: 0, orphanCandidateCount: 0, missingLedgerCount: 0 });
86
+ mockBuildGfiSnapshot.mockReturnValue({ active: null, staleSessionCount: 0, totalSessionCount: 0, activeSessionCount: 0, generatedAt: new Date().toISOString() });
87
+ mockIntegrityCheck.mockReturnValue(healthyIntegrityResult());
88
+ });
89
+
90
+ afterEach(() => {
91
+ fs.rmSync(tempDir, { recursive: true, force: true });
92
+ });
93
+
94
+ it('generates bundle with all artifacts', async () => {
95
+ const outDir = path.join(tempDir, 'snapshots');
96
+ const manifest = await exportDiagnosticsBundle(tempDir, outDir);
97
+
98
+ expect(manifest.artifacts.length).toBeGreaterThanOrEqual(8);
99
+ expect(manifest.artifacts.every(a => a.status === 'ok')).toBe(true);
100
+ expect(fs.existsSync(path.join(outDir, 'manifest.json'))).toBe(true);
101
+ expect(fs.existsSync(path.join(outDir, 'schema-conformance.json'))).toBe(true);
102
+ expect(fs.existsSync(path.join(outDir, 'canary.json'))).toBe(true);
103
+ });
104
+
105
+ it('still generates manifest when sub-check fails', async () => {
106
+ mockHealthSnapshot.mockRejectedValue(new Error('DB error'));
107
+
108
+ const outDir = path.join(tempDir, 'snapshots');
109
+ const manifest = await exportDiagnosticsBundle(tempDir, outDir);
110
+
111
+ expect(manifest.artifacts.length).toBeGreaterThanOrEqual(8);
112
+ const failedArtifact = manifest.artifacts.find(a => a.name === 'runtime-health');
113
+ expect(failedArtifact?.status).toBe('failed');
114
+ expect(fs.existsSync(path.join(outDir, 'manifest.json'))).toBe(true);
115
+ });
116
+
117
+ it('rejects output path outside workspace', async () => {
118
+ await expect(
119
+ exportDiagnosticsBundle(tempDir, '/tmp/outside-workspace'),
120
+ ).rejects.toThrow('Output path must be within workspace directory');
121
+ });
122
+
123
+ it('rejects sibling path that starts with workspace prefix', async () => {
124
+ const siblingDir = tempDir + '-backup';
125
+ await expect(
126
+ exportDiagnosticsBundle(tempDir, siblingDir),
127
+ ).rejects.toThrow('Output path must be within workspace directory');
128
+ });
129
+
130
+ it('manifest contains path and status for each artifact', async () => {
131
+ const outDir = path.join(tempDir, 'snapshots');
132
+ const manifest = await exportDiagnosticsBundle(tempDir, outDir);
133
+
134
+ for (const artifact of manifest.artifacts) {
135
+ expect(artifact.path).toBeTruthy();
136
+ expect(artifact.status).toMatch(/^(ok|failed)$/);
137
+ }
138
+ });
139
+
140
+ it('does not include sensitive env/API key content', async () => {
141
+ mockSchemaCheck.mockReturnValue({
142
+ ...healthySchemaResult(),
143
+ apiKey: 'sk-secret-key-12345',
144
+ config: { token: 'bearer-abc123', safeValue: 'hello' },
145
+ });
146
+
147
+ const outDir = path.join(tempDir, 'snapshots');
148
+ await exportDiagnosticsBundle(tempDir, outDir);
149
+
150
+ const schemaContent = fs.readFileSync(path.join(outDir, 'schema-conformance.json'), 'utf8');
151
+ const parsed = JSON.parse(schemaContent);
152
+ expect(parsed.apiKey).toBe('[REDACTED]');
153
+ expect(parsed.config.token).toBe('[REDACTED]');
154
+ expect(parsed.config.safeValue).toBe('hello');
155
+ });
156
+
157
+ it('uses createInternalizationQueueReadModel with readonly: true (no RuntimeStateManager)', async () => {
158
+ const fs = await import('fs');
159
+ const src = fs.readFileSync(require.resolve('../../src/commands/runtime-diagnostics-export.ts'), 'utf-8');
160
+ expect(src).not.toContain('RuntimeStateManager');
161
+ expect(src).toContain('createInternalizationQueueReadModel');
162
+ expect(src).toMatch(/createInternalizationQueueReadModel\(\{[^}]*readonly:\s*true/);
163
+ });
164
+
165
+ it('calls queue close once on main path', async () => {
166
+ const outDir = path.join(tempDir, 'snapshots');
167
+ await exportDiagnosticsBundle(tempDir, outDir);
168
+ expect(mockQueueClose).toHaveBeenCalledTimes(1);
169
+ });
170
+ });
@@ -0,0 +1,114 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import * as fs from 'node:fs';
3
+ import * as path from 'node:path';
4
+ import * as os from 'node:os';
5
+ import { buildFeatureFlagsStatus } from '../../src/commands/runtime-features.js';
6
+ import { loadEffectiveFeatureFlags } from '../../src/services/feature-flag-loader.js';
7
+
8
+ describe('buildFeatureFlagsStatus', () => {
9
+ it('returns status ok with clean config (no config file)', () => {
10
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pd-features-test-'));
11
+ try {
12
+ const output = buildFeatureFlagsStatus(tmpDir);
13
+ expect(output.status).toBe('ok');
14
+ expect(output.reason).toBeUndefined();
15
+ expect(output.nextAction).toBeUndefined();
16
+ expect(output.warnings).toEqual([]);
17
+ } finally {
18
+ fs.rmSync(tmpDir, { recursive: true, force: true });
19
+ }
20
+ });
21
+
22
+ it('returns status degraded with reason/nextAction when warnings present', () => {
23
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pd-features-test-'));
24
+ const configDir = path.join(tmpDir, '.pd');
25
+ fs.mkdirSync(configDir, { recursive: true });
26
+ fs.writeFileSync(path.join(configDir, 'feature-flags.yaml'), 'gfi:\n enabled: "yes"\n', 'utf8');
27
+
28
+ try {
29
+ const output = buildFeatureFlagsStatus(tmpDir);
30
+ expect(output.status).toBe('degraded');
31
+ expect(output.reason).toBeDefined();
32
+ expect(output.nextAction).toBeDefined();
33
+ expect(output.warnings.length).toBeGreaterThan(0);
34
+ } finally {
35
+ fs.rmSync(tmpDir, { recursive: true, force: true });
36
+ }
37
+ });
38
+
39
+ it('JSON output contains all required fields', () => {
40
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pd-features-test-'));
41
+ try {
42
+ const output = buildFeatureFlagsStatus(tmpDir);
43
+ const json = JSON.stringify(output);
44
+ const parsed = JSON.parse(json);
45
+
46
+ expect(parsed.status).toBe('ok');
47
+ expect(parsed.source).toBe('defaults');
48
+ expect(parsed.configPath).toBeTruthy();
49
+ expect(parsed.flags).toBeInstanceOf(Array);
50
+ expect(parsed.warnings).toBeInstanceOf(Array);
51
+ expect(typeof parsed.totalFlags).toBe('number');
52
+ expect(typeof parsed.enabledCount).toBe('number');
53
+ expect(typeof parsed.disabledCount).toBe('number');
54
+ } finally {
55
+ fs.rmSync(tmpDir, { recursive: true, force: true });
56
+ }
57
+ });
58
+ });
59
+
60
+ describe('YAML loader integration', () => {
61
+ it('rejects __proto__ key in YAML', () => {
62
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pd-features-test-'));
63
+ const configDir = path.join(tmpDir, '.pd');
64
+ fs.mkdirSync(configDir, { recursive: true });
65
+ fs.writeFileSync(
66
+ path.join(configDir, 'feature-flags.yaml'),
67
+ '"__proto__":\n enabled: true\n',
68
+ 'utf8',
69
+ );
70
+
71
+ try {
72
+ const result = loadEffectiveFeatureFlags(tmpDir);
73
+ expect(result.warnings.some(w => w.includes('__proto__'))).toBe(true);
74
+ } finally {
75
+ fs.rmSync(tmpDir, { recursive: true, force: true });
76
+ }
77
+ });
78
+
79
+ it('handles malformed YAML gracefully', () => {
80
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pd-features-test-'));
81
+ const configDir = path.join(tmpDir, '.pd');
82
+ fs.mkdirSync(configDir, { recursive: true });
83
+ fs.writeFileSync(path.join(configDir, 'feature-flags.yaml'), 'gfi: [unterminated', 'utf8');
84
+
85
+ try {
86
+ const result = loadEffectiveFeatureFlags(tmpDir);
87
+ expect(result.warnings.some(w => w.includes('YAML'))).toBe(true);
88
+ } finally {
89
+ fs.rmSync(tmpDir, { recursive: true, force: true });
90
+ }
91
+ });
92
+
93
+ it('enables GFI via valid YAML config', () => {
94
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pd-features-test-'));
95
+ const configDir = path.join(tmpDir, '.pd');
96
+ fs.mkdirSync(configDir, { recursive: true });
97
+ fs.writeFileSync(
98
+ path.join(configDir, 'feature-flags.yaml'),
99
+ 'gfi:\n enabled: true\n',
100
+ 'utf8',
101
+ );
102
+
103
+ try {
104
+ const result = loadEffectiveFeatureFlags(tmpDir);
105
+ expect(result.flags.gfi).toBeDefined();
106
+ if (result.flags.gfi) {
107
+ expect(result.flags.gfi.enabled).toBe(true);
108
+ }
109
+ expect(result.source).toBe('workspace_file');
110
+ } finally {
111
+ fs.rmSync(tmpDir, { recursive: true, force: true });
112
+ }
113
+ });
114
+ });
@@ -0,0 +1,357 @@
1
+ /**
2
+ * pd runtime health snapshot CLI unit tests.
3
+ *
4
+ * Tests the CLI adapter layer: validation, OperatorHealthReadModel delegation,
5
+ * JSON/text output formatting, and exit code behavior.
6
+ * OperatorHealthReadModel is mocked — its own contract is tested in principles-core.
7
+ */
8
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
9
+
10
+ // ── Mock setup ──────────────────────────────────────────────────────────────
11
+
12
+ const mockSnapshotFn = vi.fn();
13
+ const mockCloseFn = vi.fn().mockResolvedValue(undefined);
14
+
15
+ vi.mock('../../src/resolve-workspace.js', () => ({
16
+ resolveWorkspaceDir: vi.fn().mockReturnValue('/fake/workspace'),
17
+ }));
18
+
19
+ vi.mock('@principles/core/runtime-v2', () => ({
20
+ OperatorHealthReadModel: vi.fn().mockImplementation(function () {
21
+ return { getSnapshot: mockSnapshotFn, close: mockCloseFn };
22
+ }),
23
+ }));
24
+
25
+ import { handleRuntimeHealthSnapshot } from '../../src/commands/runtime-health-snapshot.js';
26
+
27
+ // ── Helpers ─────────────────────────────────────────────────────────────────
28
+
29
+ const WS = '/fake/workspace';
30
+
31
+ function healthySnapshot() {
32
+ return {
33
+ generatedAt: '2026-05-03T12:00:00.000Z',
34
+ workspace: WS,
35
+ painChain: {
36
+ lastSuccessfulChain: {
37
+ painId: 'pain_001',
38
+ taskId: 'task_001',
39
+ runId: 'run_001',
40
+ artifactId: 'art_001',
41
+ candidateIds: ['c1'],
42
+ ledgerEntryIds: ['l1'],
43
+ status: 'succeeded',
44
+ latencyMs: { painToTask: 100 },
45
+ failureCategory: null,
46
+ checkedAt: '2026-05-03T12:00:00.000Z',
47
+ missingLinks: [],
48
+ },
49
+ failureCategory: null,
50
+ },
51
+ candidateLedger: {
52
+ auditStatus: 'ok' as const,
53
+ orphanCandidateCount: 0,
54
+ missingLedgerCount: 0,
55
+ },
56
+ pruning: {
57
+ watchCount: 0,
58
+ reviewCount: 0,
59
+ orphanDerivedCandidateCount: 0,
60
+ },
61
+ gfi: {
62
+ active: null,
63
+ staleSessionCount: 0,
64
+ staleGfiRange: null,
65
+ totalSessionCount: 0,
66
+ activeSessionCount: 0,
67
+ generatedAt: '2026-05-03T12:00:00.000Z',
68
+ },
69
+ overallStatus: 'healthy' as const,
70
+ recommendedActions: [],
71
+ totalTaskCount: 1,
72
+ };
73
+ }
74
+
75
+ function mockProcessExit() {
76
+ return vi.spyOn(process, 'exit').mockImplementation((() => undefined) as () => never);
77
+ }
78
+
79
+ // ── Tests ───────────────────────────────────────────────────────────────────
80
+
81
+ describe('handleRuntimeHealthSnapshot', () => {
82
+ let consoleLogSpy: ReturnType<typeof vi.spyOn>;
83
+ let consoleErrorSpy: ReturnType<typeof vi.spyOn>;
84
+
85
+ beforeEach(() => {
86
+ vi.clearAllMocks();
87
+ consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
88
+ consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
89
+ });
90
+
91
+ afterEach(() => {
92
+ consoleLogSpy.mockRestore();
93
+ consoleErrorSpy.mockRestore();
94
+ });
95
+
96
+ // ── Healthy ──────────────────────────────────────────────────────────────
97
+
98
+ it('outputs healthy JSON snapshot with all required fields (--json)', async () => {
99
+ mockSnapshotFn.mockResolvedValue(healthySnapshot());
100
+
101
+ await handleRuntimeHealthSnapshot({ workspace: WS, json: true });
102
+
103
+ expect(consoleLogSpy).toHaveBeenCalled();
104
+ const jsonOutput = JSON.parse(consoleLogSpy.mock.calls[0][0]);
105
+ expect(jsonOutput.generatedAt).toBe('2026-05-03T12:00:00.000Z');
106
+ expect(jsonOutput.workspace).toBe(WS);
107
+ expect(jsonOutput.painChain).toBeDefined();
108
+ expect(jsonOutput.painChain.lastSuccessfulChain).toBeDefined();
109
+ expect(jsonOutput.candidateLedger.auditStatus).toBe('ok');
110
+ expect(jsonOutput.pruning.watchCount).toBe(0);
111
+ expect(jsonOutput.overallStatus).toBe('healthy');
112
+ expect(jsonOutput.recommendedActions).toEqual([]);
113
+ });
114
+
115
+ it('outputs readable text for healthy snapshot', async () => {
116
+ mockSnapshotFn.mockResolvedValue(healthySnapshot());
117
+
118
+ await handleRuntimeHealthSnapshot({ workspace: WS, json: false });
119
+
120
+ const allOutput = consoleLogSpy.mock.calls.map(c => c.join(' ')).join('\n');
121
+ expect(allOutput).toContain('HEALTHY');
122
+ expect(allOutput).toContain('No actions recommended');
123
+ });
124
+
125
+ // ── Degraded: candidate audit ────────────────────────────────────────────
126
+
127
+ it('reflects candidate audit degraded in overallStatus', async () => {
128
+ const snapshot = {
129
+ ...healthySnapshot(),
130
+ candidateLedger: {
131
+ auditStatus: 'degraded' as const,
132
+ orphanCandidateCount: 2,
133
+ missingLedgerCount: 2,
134
+ },
135
+ overallStatus: 'degraded' as const,
136
+ recommendedActions: [
137
+ 'Run `pd candidate audit --workspace <path> --json` for details.',
138
+ ],
139
+ };
140
+ mockSnapshotFn.mockResolvedValue(snapshot);
141
+ const exitSpy = mockProcessExit();
142
+
143
+ await handleRuntimeHealthSnapshot({ workspace: WS, json: true });
144
+
145
+ const jsonOutput = JSON.parse(consoleLogSpy.mock.calls[0][0]);
146
+ expect(jsonOutput.overallStatus).toBe('degraded');
147
+ expect(jsonOutput.recommendedActions).toContain(
148
+ 'Run `pd candidate audit --workspace <path> --json` for details.',
149
+ );
150
+ expect(process.exitCode).toBe(1);
151
+
152
+ exitSpy.mockRestore();
153
+ });
154
+
155
+ // ── Degraded: pruning signals ────────────────────────────────────────────
156
+
157
+ it('reflects pruning watch/review signals in recommendedActions', async () => {
158
+ const snapshot = {
159
+ ...healthySnapshot(),
160
+ pruning: {
161
+ watchCount: 2,
162
+ reviewCount: 1,
163
+ orphanDerivedCandidateCount: 0,
164
+ },
165
+ overallStatus: 'degraded' as const,
166
+ recommendedActions: [
167
+ 'Run `pd runtime pruning report --workspace <path> --json` for lifecycle signals.',
168
+ ],
169
+ };
170
+ mockSnapshotFn.mockResolvedValue(snapshot);
171
+ const exitSpy = mockProcessExit();
172
+
173
+ await handleRuntimeHealthSnapshot({ workspace: WS, json: true });
174
+
175
+ const jsonOutput = JSON.parse(consoleLogSpy.mock.calls[0][0]);
176
+ expect(jsonOutput.overallStatus).toBe('degraded');
177
+ expect(jsonOutput.pruning.watchCount).toBe(2);
178
+ expect(jsonOutput.recommendedActions).toContain(
179
+ 'Run `pd runtime pruning report --workspace <path> --json` for lifecycle signals.',
180
+ );
181
+
182
+ exitSpy.mockRestore();
183
+ });
184
+
185
+ // ── Degraded: no successful chain ────────────────────────────────────────
186
+
187
+ it('recommends UAT baseline when no successful chain exists', async () => {
188
+ const snapshot = {
189
+ ...healthySnapshot(),
190
+ painChain: {
191
+ lastSuccessfulChain: null,
192
+ failureCategory: null,
193
+ },
194
+ overallStatus: 'degraded' as const,
195
+ recommendedActions: [
196
+ 'Run `pd runtime uat --workspace <path> --count 3` to establish baseline.',
197
+ ],
198
+ };
199
+ mockSnapshotFn.mockResolvedValue(snapshot);
200
+ const exitSpy = mockProcessExit();
201
+
202
+ await handleRuntimeHealthSnapshot({ workspace: WS, json: true });
203
+
204
+ const jsonOutput = JSON.parse(consoleLogSpy.mock.calls[0][0]);
205
+ expect(jsonOutput.painChain.lastSuccessfulChain).toBeNull();
206
+ expect(jsonOutput.recommendedActions).toContain(
207
+ 'Run `pd runtime uat --workspace <path> --count 3` to establish baseline.',
208
+ );
209
+
210
+ exitSpy.mockRestore();
211
+ });
212
+
213
+ // ── Error ────────────────────────────────────────────────────────────────
214
+
215
+ it('handles error status from missing state.db', async () => {
216
+ const snapshot = {
217
+ ...healthySnapshot(),
218
+ candidateLedger: {
219
+ auditStatus: 'error' as const,
220
+ orphanCandidateCount: 0,
221
+ missingLedgerCount: 0,
222
+ },
223
+ overallStatus: 'error' as const,
224
+ recommendedActions: ['Initialize workspace with `pd pain record`.'],
225
+ };
226
+ mockSnapshotFn.mockResolvedValue(snapshot);
227
+ const exitSpy = mockProcessExit();
228
+
229
+ await handleRuntimeHealthSnapshot({ workspace: WS, json: true });
230
+
231
+ const jsonOutput = JSON.parse(consoleLogSpy.mock.calls[0][0]);
232
+ expect(jsonOutput.overallStatus).toBe('error');
233
+ expect(jsonOutput.candidateLedger.auditStatus).toBe('error');
234
+ expect(process.exitCode).toBe(1);
235
+
236
+ exitSpy.mockRestore();
237
+ });
238
+
239
+ // ── Multiple degraded conditions ─────────────────────────────────────────
240
+
241
+ it('accumulates multiple recommendedActions for multiple issues', async () => {
242
+ const snapshot = {
243
+ ...healthySnapshot(),
244
+ painChain: { lastSuccessfulChain: null, failureCategory: null },
245
+ candidateLedger: {
246
+ auditStatus: 'degraded' as const,
247
+ orphanCandidateCount: 2,
248
+ missingLedgerCount: 2,
249
+ },
250
+ pruning: { watchCount: 3, reviewCount: 1, orphanDerivedCandidateCount: 0 },
251
+ overallStatus: 'degraded' as const,
252
+ recommendedActions: [
253
+ 'Run `pd candidate audit --workspace <path> --json` for details.',
254
+ 'Run `pd runtime pruning report --workspace <path> --json` for lifecycle signals.',
255
+ 'Run `pd runtime uat --workspace <path> --count 3` to establish baseline.',
256
+ ],
257
+ };
258
+ mockSnapshotFn.mockResolvedValue(snapshot);
259
+ const exitSpy = mockProcessExit();
260
+
261
+ await handleRuntimeHealthSnapshot({ workspace: WS, json: true });
262
+
263
+ const jsonOutput = JSON.parse(consoleLogSpy.mock.calls[0][0]);
264
+ expect(jsonOutput.recommendedActions).toHaveLength(3);
265
+
266
+ exitSpy.mockRestore();
267
+ });
268
+
269
+ // ── GFI section (PRI-83) ──────────────────────────────────────────────────
270
+
271
+ it('includes gfi section in JSON output with active session data', async () => {
272
+ const snapshot = {
273
+ ...healthySnapshot(),
274
+ gfi: {
275
+ active: {
276
+ currentGfi: 42,
277
+ stage: 'elevated',
278
+ dominantSource: 'tool_failure',
279
+ consecutiveErrors: 2,
280
+ dailyGfiPeak: 55,
281
+ consumers: {
282
+ attitudeMode: 'conciliatory',
283
+ painDiagnosticReason: 'high_gfi',
284
+ },
285
+ },
286
+ staleSessionCount: 1,
287
+ staleGfiRange: { min: 10, max: 20 },
288
+ totalSessionCount: 2,
289
+ activeSessionCount: 1,
290
+ generatedAt: '2026-05-03T12:00:00.000Z',
291
+ },
292
+ };
293
+ mockSnapshotFn.mockResolvedValue(snapshot);
294
+
295
+ await handleRuntimeHealthSnapshot({ workspace: WS, json: true });
296
+
297
+ const jsonOutput = JSON.parse(consoleLogSpy.mock.calls[0][0]);
298
+ expect(jsonOutput.gfi).toBeDefined();
299
+ expect(jsonOutput.gfi.active).not.toBeNull();
300
+ expect(jsonOutput.gfi.active.currentGfi).toBe(42);
301
+ expect(jsonOutput.gfi.active.stage).toBe('elevated');
302
+ expect(jsonOutput.gfi.active.dominantSource).toBe('tool_failure');
303
+ expect(jsonOutput.gfi.activeSessionCount).toBe(1);
304
+ expect(jsonOutput.gfi.staleSessionCount).toBe(1);
305
+ });
306
+
307
+ it('includes compact GFI line in text output', async () => {
308
+ const snapshot = {
309
+ ...healthySnapshot(),
310
+ gfi: {
311
+ active: {
312
+ currentGfi: 42,
313
+ stage: 'elevated',
314
+ dominantSource: 'tool_failure',
315
+ consecutiveErrors: 2,
316
+ dailyGfiPeak: 55,
317
+ consumers: {
318
+ attitudeMode: 'conciliatory',
319
+ painDiagnosticReason: 'high_gfi',
320
+ },
321
+ },
322
+ staleSessionCount: 0,
323
+ staleGfiRange: null,
324
+ totalSessionCount: 1,
325
+ activeSessionCount: 1,
326
+ generatedAt: '2026-05-03T12:00:00.000Z',
327
+ },
328
+ };
329
+ mockSnapshotFn.mockResolvedValue(snapshot);
330
+
331
+ await handleRuntimeHealthSnapshot({ workspace: WS, json: false });
332
+
333
+ const allOutput = consoleLogSpy.mock.calls.map(c => c.join(' ')).join('\n');
334
+ expect(allOutput).toContain('gfi:');
335
+ expect(allOutput).toContain('elevated');
336
+ expect(allOutput).toContain('42');
337
+ });
338
+
339
+ it('shows no active sessions in text when gfi.active is null', async () => {
340
+ mockSnapshotFn.mockResolvedValue(healthySnapshot());
341
+
342
+ await handleRuntimeHealthSnapshot({ workspace: WS, json: false });
343
+
344
+ const allOutput = consoleLogSpy.mock.calls.map(c => c.join(' ')).join('\n');
345
+ expect(allOutput).toContain('no active sessions');
346
+ });
347
+
348
+ it('does not mark runtime unhealthy solely for missing GFI data', async () => {
349
+ mockSnapshotFn.mockResolvedValue(healthySnapshot());
350
+
351
+ await handleRuntimeHealthSnapshot({ workspace: WS, json: true });
352
+
353
+ const jsonOutput = JSON.parse(consoleLogSpy.mock.calls[0][0]);
354
+ expect(jsonOutput.gfi.active).toBeNull();
355
+ expect(jsonOutput.overallStatus).toBe('healthy');
356
+ });
357
+ });