@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,792 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import { Command } from 'commander';
3
+
4
+ const { MockRuntimeStateManager, mockGetCandidatesByTaskId, mockUpdateCandidateStatus } = vi.hoisted(() => {
5
+ const mockGetCandidatesByTaskId = vi.fn().mockResolvedValue([]);
6
+ const mockUpdateCandidateStatus = vi.fn().mockResolvedValue(undefined);
7
+
8
+ class MockRuntimeStateManager {
9
+ initialize = vi.fn().mockResolvedValue(undefined);
10
+ close = vi.fn().mockResolvedValue(undefined);
11
+ getTask = vi.fn().mockResolvedValue({
12
+ taskId: 'test-task-1',
13
+ status: 'pending',
14
+ attemptCount: 0,
15
+ maxAttempts: 3,
16
+ lastError: null,
17
+ });
18
+ getCandidatesByTaskId = mockGetCandidatesByTaskId;
19
+ updateCandidateStatus = mockUpdateCandidateStatus;
20
+ connection = {} as Record<string, unknown>;
21
+ taskStore = {};
22
+ runStore = {};
23
+ }
24
+ return { MockRuntimeStateManager, mockGetCandidatesByTaskId, mockUpdateCandidateStatus };
25
+ }, { validateType: true });
26
+
27
+ const { mockIntake, MockCandidateIntakeService } = vi.hoisted(() => {
28
+ const mockIntake = vi.fn();
29
+ function MockCandidateIntakeService(this: any) {
30
+ return { intake: mockIntake };
31
+ }
32
+ MockCandidateIntakeService.prototype = {};
33
+ return { mockIntake, MockCandidateIntakeService };
34
+ });
35
+
36
+ const { MockPrincipleTreeLedgerAdapter } = vi.hoisted(() => {
37
+ function MockPrincipleTreeLedgerAdapter(this: any) {
38
+ return {};
39
+ }
40
+ MockPrincipleTreeLedgerAdapter.prototype = {};
41
+ return { MockPrincipleTreeLedgerAdapter };
42
+ });
43
+
44
+ vi.mock('../../src/resolve-workspace.js', () => ({
45
+ resolveWorkspaceDir: vi.fn().mockReturnValue('/tmp/fake-workspace'),
46
+ }));
47
+
48
+ vi.mock('@principles/core/runtime-v2', () => {
49
+ return {
50
+ RuntimeStateManager: vi.fn().mockImplementation(function () {
51
+ return new MockRuntimeStateManager();
52
+ }),
53
+ SqliteHistoryQuery: vi.fn().mockImplementation(function () { return {}; }),
54
+ SqliteContextAssembler: vi.fn().mockImplementation(function () { return {}; }),
55
+ SqliteDiagnosticianCommitter: vi.fn().mockImplementation(function () { return {}; }),
56
+ SqliteTrajectoryLocator: vi.fn().mockImplementation(function () { return {}; }),
57
+ SqliteSourceTraceLocator: vi.fn().mockImplementation(function () { return {}; }),
58
+ StoreEventEmitter: vi.fn().mockImplementation(function () { return {}; }),
59
+ storeEmitter: { emitTelemetry: vi.fn() },
60
+ DiagnosticianRunner: vi.fn().mockImplementation(function () { return {}; }),
61
+ PassThroughValidator: vi.fn().mockImplementation(function () { return {}; }),
62
+ DefaultDiagnosticianValidator: vi.fn().mockImplementation(function () { return {}; }),
63
+ TestDoubleRuntimeAdapter: vi.fn().mockImplementation(function () { return {}; }),
64
+ OpenClawCliRuntimeAdapter: vi.fn().mockImplementation(function () { return {}; }),
65
+ PDRuntimeError: class PDRuntimeError extends Error {
66
+ constructor(public category: string, message: string) {
67
+ super(message);
68
+ this.name = 'PDRuntimeError';
69
+ }
70
+ },
71
+ CandidateIntakeService: MockCandidateIntakeService,
72
+ resolveRuntimeConfig: vi.fn().mockReturnValue({
73
+ runtimeKind: 'pi-ai',
74
+ provider: 'test-provider',
75
+ model: 'test-model',
76
+ apiKeyEnv: 'TEST_KEY',
77
+ timeoutMs: 300000,
78
+ agentId: 'main',
79
+ }),
80
+ isRuntimeConfigError: vi.fn().mockReturnValue(false),
81
+ run: vi.fn().mockResolvedValue({
82
+ status: 'succeeded',
83
+ taskId: 'test-task-1',
84
+ output: {
85
+ valid: true,
86
+ diagnosisId: 'diag-123',
87
+ taskId: 'test-task-1',
88
+ summary: 'Test diagnosis summary',
89
+ rootCause: 'Test: test root cause',
90
+ violatedPrinciples: [],
91
+ evidence: [],
92
+ recommendations: [],
93
+ confidence: 0.9,
94
+ },
95
+ }),
96
+ status: vi.fn(),
97
+ };
98
+ });
99
+
100
+ vi.mock('../../src/principle-tree-ledger-adapter.js', () => ({
101
+ PrincipleTreeLedgerAdapter: MockPrincipleTreeLedgerAdapter,
102
+ }));
103
+
104
+ import { handleDiagnoseRun, type DiagnoseRunOptions } from '../../src/commands/diagnose.js';
105
+
106
+ const SUCCEEDED_RESULT = {
107
+ status: 'succeeded' as const,
108
+ taskId: 'test-task-1',
109
+ output: {
110
+ valid: true,
111
+ diagnosisId: 'diag-123',
112
+ taskId: 'test-task-1',
113
+ summary: 'Test diagnosis summary',
114
+ rootCause: 'Test: test root cause',
115
+ violatedPrinciples: [],
116
+ evidence: [],
117
+ recommendations: [
118
+ { kind: 'principle', description: 'Always validate tool arguments before execution' },
119
+ ],
120
+ confidence: 0.9,
121
+ },
122
+ attemptCount: 1,
123
+ };
124
+
125
+ describe('pd diagnose run --runtime routing', () => {
126
+ let mockResolveRuntimeConfig: ReturnType<typeof vi.fn>;
127
+ let mockIsRuntimeConfigError: ReturnType<typeof vi.fn>;
128
+
129
+ beforeEach(async () => {
130
+ vi.clearAllMocks();
131
+ const runtimeV2 = await import('@principles/core/runtime-v2');
132
+ mockResolveRuntimeConfig = vi.mocked(runtimeV2.resolveRuntimeConfig);
133
+ mockIsRuntimeConfigError = vi.mocked(runtimeV2.isRuntimeConfigError);
134
+ const { run } = runtimeV2;
135
+ vi.mocked(run).mockResolvedValue(SUCCEEDED_RESULT);
136
+ mockGetCandidatesByTaskId.mockResolvedValue([]);
137
+ mockUpdateCandidateStatus.mockResolvedValue(undefined);
138
+ mockIntake.mockReset();
139
+ });
140
+
141
+ it('CLI-01: --runtime test-double routes to TestDoubleRuntimeAdapter (regression)', async () => {
142
+ const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
143
+ const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => undefined) as () => never);
144
+
145
+ await handleDiagnoseRun({
146
+ taskId: 'test-task-1',
147
+ workspace: '/tmp/fake-workspace',
148
+ runtime: 'test-double',
149
+ json: false,
150
+ } as DiagnoseRunOptions);
151
+
152
+ expect(consoleSpy).toHaveBeenCalled();
153
+ expect(exitSpy).not.toHaveBeenCalledWith(1);
154
+
155
+ consoleSpy.mockRestore();
156
+ exitSpy.mockRestore();
157
+ });
158
+
159
+ it('HG-03: --runtime openclaw-cli without mode (no file config) fails via resolveRuntimeConfig', async () => {
160
+ mockResolveRuntimeConfig.mockReturnValueOnce({
161
+ ok: false,
162
+ reason: 'missing_openclaw_mode',
163
+ message: 'runtimeKind is openclaw-cli but no mode specified',
164
+ nextAction: 'Provide exactly one mode',
165
+ });
166
+ mockIsRuntimeConfigError.mockReturnValueOnce(true);
167
+
168
+ const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
169
+ const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => undefined) as () => never);
170
+
171
+ await handleDiagnoseRun({
172
+ taskId: 'test-task-1',
173
+ workspace: '/tmp/fake-workspace',
174
+ runtime: 'openclaw-cli',
175
+ json: false,
176
+ } as DiagnoseRunOptions);
177
+
178
+ expect(consoleErrorSpy).toHaveBeenCalledWith(expect.stringContaining('no mode specified'));
179
+ expect(exitSpy).toHaveBeenCalledWith(1);
180
+
181
+ consoleErrorSpy.mockRestore();
182
+ exitSpy.mockRestore();
183
+ });
184
+
185
+ it('HG-03: both --openclaw-local and --openclaw-gateway exits with error (mutually exclusive)', async () => {
186
+ const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
187
+ const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => undefined) as () => never);
188
+
189
+ await handleDiagnoseRun({
190
+ taskId: 'test-task-1',
191
+ workspace: '/tmp/fake-workspace',
192
+ runtime: 'openclaw-cli',
193
+ openclawLocal: true,
194
+ openclawGateway: true,
195
+ json: false,
196
+ } as DiagnoseRunOptions);
197
+
198
+ expect(consoleErrorSpy).toHaveBeenCalledWith(
199
+ 'error: --openclaw-local and --openclaw-gateway are mutually exclusive'
200
+ );
201
+ expect(exitSpy).toHaveBeenCalledWith(1);
202
+
203
+ consoleErrorSpy.mockRestore();
204
+ exitSpy.mockRestore();
205
+ });
206
+
207
+ it('DPB-09: openclaw-cli with file config openclawMode succeeds without CLI flag', async () => {
208
+ mockResolveRuntimeConfig.mockReturnValueOnce({
209
+ runtimeKind: 'openclaw-cli',
210
+ openclawMode: 'local',
211
+ timeoutMs: 300000,
212
+ agentId: 'main',
213
+ });
214
+ mockIsRuntimeConfigError.mockReturnValueOnce(false);
215
+
216
+ const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
217
+ const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => undefined) as () => never);
218
+
219
+ await handleDiagnoseRun({
220
+ taskId: 'test-task-1',
221
+ workspace: '/tmp/fake-workspace',
222
+ runtime: 'openclaw-cli',
223
+ json: false,
224
+ } as DiagnoseRunOptions);
225
+
226
+ expect(exitSpy).not.toHaveBeenCalledWith(1);
227
+
228
+ consoleSpy.mockRestore();
229
+ exitSpy.mockRestore();
230
+ });
231
+
232
+ it('DPB-09: openclaw-cli flag overrides file config mode', async () => {
233
+ mockResolveRuntimeConfig.mockReturnValueOnce({
234
+ runtimeKind: 'openclaw-cli',
235
+ openclawMode: 'gateway',
236
+ timeoutMs: 300000,
237
+ agentId: 'main',
238
+ });
239
+ mockIsRuntimeConfigError.mockReturnValueOnce(false);
240
+
241
+ const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
242
+ const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => undefined) as () => never);
243
+
244
+ await handleDiagnoseRun({
245
+ taskId: 'test-task-1',
246
+ workspace: '/tmp/fake-workspace',
247
+ runtime: 'openclaw-cli',
248
+ openclawLocal: true,
249
+ json: false,
250
+ } as DiagnoseRunOptions);
251
+
252
+ expect(exitSpy).not.toHaveBeenCalledWith(1);
253
+
254
+ consoleSpy.mockRestore();
255
+ exitSpy.mockRestore();
256
+ });
257
+
258
+ it('DPB-09: openclaw-cli missing mode (--json) outputs JSON error', async () => {
259
+ mockResolveRuntimeConfig.mockReturnValueOnce({
260
+ ok: false,
261
+ reason: 'missing_openclaw_mode',
262
+ message: 'runtimeKind is openclaw-cli but no mode specified',
263
+ nextAction: 'Provide exactly one mode',
264
+ });
265
+ mockIsRuntimeConfigError.mockReturnValueOnce(true);
266
+
267
+ const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
268
+ const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => undefined) as () => never);
269
+
270
+ await handleDiagnoseRun({
271
+ taskId: 'test-task-1',
272
+ workspace: '/tmp/fake-workspace',
273
+ runtime: 'openclaw-cli',
274
+ json: true,
275
+ } as DiagnoseRunOptions);
276
+
277
+ const jsonOutput = JSON.parse(logSpy.mock.calls[0][0]);
278
+ expect(jsonOutput.ok).toBe(false);
279
+ expect(jsonOutput.reason).toBe('missing_openclaw_mode');
280
+ expect(exitSpy).toHaveBeenCalledWith(1);
281
+
282
+ logSpy.mockRestore();
283
+ exitSpy.mockRestore();
284
+ });
285
+
286
+ it('CLI-04: unknown runtime kind exits with error and exit code 1', async () => {
287
+ const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
288
+ const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => undefined) as () => never);
289
+
290
+ await handleDiagnoseRun({
291
+ taskId: 'test-task-1',
292
+ workspace: '/tmp/fake-workspace',
293
+ runtime: 'invalid-runtime',
294
+ json: true,
295
+ } as DiagnoseRunOptions);
296
+
297
+ expect(consoleErrorSpy).toHaveBeenCalledWith("error: unknown runtime kind 'invalid-runtime' (supported: openclaw-cli, test-double, pi-ai)");
298
+ expect(exitSpy).toHaveBeenCalledWith(1);
299
+
300
+ consoleErrorSpy.mockRestore();
301
+ exitSpy.mockRestore();
302
+ });
303
+
304
+ it('DPB-09: openclaw-cli --openclaw-gateway constructs adapter with runtimeMode=gateway', async () => {
305
+ mockResolveRuntimeConfig.mockReturnValueOnce({
306
+ runtimeKind: 'openclaw-cli',
307
+ openclawMode: 'gateway',
308
+ timeoutMs: 300000,
309
+ agentId: 'main',
310
+ });
311
+ mockIsRuntimeConfigError.mockReturnValueOnce(false);
312
+
313
+ const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
314
+ const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => undefined) as () => never);
315
+
316
+ await handleDiagnoseRun({
317
+ taskId: 'test-task-1',
318
+ workspace: '/tmp/fake-workspace',
319
+ runtime: 'openclaw-cli',
320
+ openclawGateway: true,
321
+ json: false,
322
+ } as DiagnoseRunOptions);
323
+
324
+ const OpenClawCliMock = vi.mocked(
325
+ await import('@principles/core/runtime-v2').then(m => m.OpenClawCliRuntimeAdapter),
326
+ );
327
+ expect(OpenClawCliMock).toHaveBeenCalledWith(
328
+ expect.objectContaining({ runtimeMode: 'gateway' }),
329
+ );
330
+ expect(exitSpy).not.toHaveBeenCalledWith(1);
331
+
332
+ consoleSpy.mockRestore();
333
+ exitSpy.mockRestore();
334
+ });
335
+
336
+ it('DPB-09: openclaw-cli --openclaw-local constructs adapter with runtimeMode=local', async () => {
337
+ mockResolveRuntimeConfig.mockReturnValueOnce({
338
+ runtimeKind: 'openclaw-cli',
339
+ openclawMode: 'local',
340
+ timeoutMs: 300000,
341
+ agentId: 'main',
342
+ });
343
+ mockIsRuntimeConfigError.mockReturnValueOnce(false);
344
+
345
+ const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
346
+ const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => undefined) as () => never);
347
+
348
+ await handleDiagnoseRun({
349
+ taskId: 'test-task-1',
350
+ workspace: '/tmp/fake-workspace',
351
+ runtime: 'openclaw-cli',
352
+ openclawLocal: true,
353
+ json: false,
354
+ } as DiagnoseRunOptions);
355
+
356
+ const OpenClawCliMock = vi.mocked(
357
+ await import('@principles/core/runtime-v2').then(m => m.OpenClawCliRuntimeAdapter),
358
+ );
359
+ expect(OpenClawCliMock).toHaveBeenCalledWith(
360
+ expect.objectContaining({ runtimeMode: 'local' }),
361
+ );
362
+ expect(exitSpy).not.toHaveBeenCalledWith(1);
363
+
364
+ consoleSpy.mockRestore();
365
+ exitSpy.mockRestore();
366
+ });
367
+ });
368
+
369
+ describe('pd diagnose run — auto-intake after success', () => {
370
+ beforeEach(async () => {
371
+ vi.clearAllMocks();
372
+ const { run } = await import('@principles/core/runtime-v2');
373
+ vi.mocked(run).mockResolvedValue(SUCCEEDED_RESULT);
374
+ mockGetCandidatesByTaskId.mockResolvedValue([]);
375
+ mockUpdateCandidateStatus.mockResolvedValue(undefined);
376
+ mockIntake.mockReset();
377
+ });
378
+
379
+ it('INTAKE-01: successful diagnose + intake — candidates consumed, JSON includes intake evidence', async () => {
380
+ const candidates = [
381
+ { candidateId: 'cand-1', artifactId: 'art-1', taskId: 'test-task-1', status: 'pending' },
382
+ { candidateId: 'cand-2', artifactId: 'art-2', taskId: 'test-task-1', status: 'pending' },
383
+ ];
384
+ mockGetCandidatesByTaskId.mockResolvedValue(candidates);
385
+ mockIntake
386
+ .mockResolvedValueOnce({ id: 'ledger-1', title: 'Principle 1', status: 'probation' })
387
+ .mockResolvedValueOnce({ id: 'ledger-2', title: 'Principle 2', status: 'probation' });
388
+
389
+ const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
390
+ const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => undefined) as () => never);
391
+
392
+ await handleDiagnoseRun({
393
+ taskId: 'test-task-1',
394
+ workspace: '/tmp/fake-workspace',
395
+ runtime: 'test-double',
396
+ json: true,
397
+ } as DiagnoseRunOptions);
398
+
399
+ expect(mockIntake).toHaveBeenCalledTimes(2);
400
+ expect(mockIntake).toHaveBeenCalledWith('cand-1');
401
+ expect(mockIntake).toHaveBeenCalledWith('cand-2');
402
+ expect(mockUpdateCandidateStatus).toHaveBeenCalledTimes(2);
403
+ expect(mockUpdateCandidateStatus).toHaveBeenCalledWith('cand-1', { status: 'consumed' });
404
+ expect(mockUpdateCandidateStatus).toHaveBeenCalledWith('cand-2', { status: 'consumed' });
405
+
406
+ const jsonOutput = consoleSpy.mock.calls.find(call => {
407
+ try {
408
+ const parsed = JSON.parse(call[0] as string);
409
+ return parsed.intake && Array.isArray(parsed.intake.candidates);
410
+ } catch { return false; }
411
+ });
412
+ expect(jsonOutput).toBeDefined();
413
+ const parsed = JSON.parse((jsonOutput as [string])[0]);
414
+ expect(parsed.intake.enabled).toBe(true);
415
+ expect(parsed.intake.candidates).toHaveLength(2);
416
+ expect(parsed.intake.candidates[0].candidateId).toBe('cand-1');
417
+ expect(parsed.intake.candidates[0].status).toBe('consumed');
418
+ expect(parsed.intake.candidates[0].ledgerEntryId).toBe('ledger-1');
419
+ expect(parsed.intake.candidates[1].candidateId).toBe('cand-2');
420
+ expect(parsed.intake.candidates[1].status).toBe('consumed');
421
+ expect(parsed.intake.candidates[1].ledgerEntryId).toBe('ledger-2');
422
+ expect(exitSpy).not.toHaveBeenCalledWith(1);
423
+
424
+ consoleSpy.mockRestore();
425
+ exitSpy.mockRestore();
426
+ });
427
+
428
+ it('INTAKE-02: successful diagnose + intake failure — exits non-zero with nextAction', async () => {
429
+ const candidates = [
430
+ { candidateId: 'cand-ok', artifactId: 'art-1', taskId: 'test-task-1', status: 'pending' },
431
+ { candidateId: 'cand-fail', artifactId: 'art-2', taskId: 'test-task-1', status: 'pending' },
432
+ ];
433
+ mockGetCandidatesByTaskId.mockResolvedValue(candidates);
434
+ mockIntake
435
+ .mockResolvedValueOnce({ id: 'ledger-ok', title: 'OK', status: 'probation' })
436
+ .mockImplementationOnce(() => { throw new Error('Ledger write failed'); });
437
+
438
+ const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
439
+ const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => undefined) as () => never);
440
+
441
+ await handleDiagnoseRun({
442
+ taskId: 'test-task-1',
443
+ workspace: '/tmp/fake-workspace',
444
+ runtime: 'test-double',
445
+ json: true,
446
+ } as DiagnoseRunOptions);
447
+
448
+ const jsonOutput = consoleSpy.mock.calls.find(call => {
449
+ try {
450
+ const parsed = JSON.parse(call[0] as string);
451
+ return parsed.intake && Array.isArray(parsed.intake.candidates);
452
+ } catch { return false; }
453
+ });
454
+ expect(jsonOutput).toBeDefined();
455
+ const parsed = JSON.parse((jsonOutput as [string])[0]);
456
+ expect(parsed.intake.candidates).toHaveLength(2);
457
+ expect(parsed.intake.candidates[0].status).toBe('consumed');
458
+ expect(parsed.intake.candidates[1].status).toBe('intake_failed');
459
+ expect(parsed.intake.candidates[1].error).toContain('Ledger write failed');
460
+ expect(parsed.intake.candidates[1].nextAction).toContain('pd candidate intake --candidate-id cand-fail');
461
+ expect(exitSpy).toHaveBeenCalledWith(1);
462
+
463
+ consoleSpy.mockRestore();
464
+ exitSpy.mockRestore();
465
+ });
466
+
467
+ it('INTAKE-03: --no-intake skips intake, candidates remain pending with advisory', async () => {
468
+ const candidates = [
469
+ { candidateId: 'cand-1', artifactId: 'art-1', taskId: 'test-task-1', status: 'pending' },
470
+ ];
471
+ mockGetCandidatesByTaskId.mockResolvedValue(candidates);
472
+
473
+ const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
474
+ const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => undefined) as () => never);
475
+
476
+ await handleDiagnoseRun({
477
+ taskId: 'test-task-1',
478
+ workspace: '/tmp/fake-workspace',
479
+ runtime: 'test-double',
480
+ json: true,
481
+ intake: false,
482
+ } as DiagnoseRunOptions);
483
+
484
+ expect(mockIntake).not.toHaveBeenCalled();
485
+ expect(mockUpdateCandidateStatus).not.toHaveBeenCalled();
486
+
487
+ const jsonOutput = consoleSpy.mock.calls.find(call => {
488
+ try {
489
+ const parsed = JSON.parse(call[0] as string);
490
+ return parsed.intake && Array.isArray(parsed.intake.candidates);
491
+ } catch { return false; }
492
+ });
493
+ expect(jsonOutput).toBeDefined();
494
+ const parsed = JSON.parse((jsonOutput as [string])[0]);
495
+ expect(parsed.intake.enabled).toBe(false);
496
+ expect(parsed.intake.candidates).toHaveLength(1);
497
+ expect(parsed.intake.candidates[0].candidateId).toBe('cand-1');
498
+ expect(parsed.intake.candidates[0].status).toBe('skipped');
499
+ expect(exitSpy).not.toHaveBeenCalledWith(1);
500
+
501
+ consoleSpy.mockRestore();
502
+ exitSpy.mockRestore();
503
+ });
504
+
505
+ it('INTAKE-04: --no-intake human-readable output shows advisory with manual intake commands', async () => {
506
+ const candidates = [
507
+ { candidateId: 'cand-1', artifactId: 'art-1', taskId: 'test-task-1', status: 'pending' },
508
+ ];
509
+ mockGetCandidatesByTaskId.mockResolvedValue(candidates);
510
+
511
+ const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
512
+ const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => undefined) as () => never);
513
+
514
+ await handleDiagnoseRun({
515
+ taskId: 'test-task-1',
516
+ workspace: '/tmp/fake-workspace',
517
+ runtime: 'test-double',
518
+ json: false,
519
+ intake: false,
520
+ } as DiagnoseRunOptions);
521
+
522
+ const allOutput = consoleSpy.mock.calls.map(call => call[0]).join('\n');
523
+ expect(allOutput).toContain('--no-intake');
524
+ expect(allOutput).toContain('pd candidate intake --candidate-id cand-1');
525
+ expect(exitSpy).not.toHaveBeenCalledWith(1);
526
+
527
+ consoleSpy.mockRestore();
528
+ exitSpy.mockRestore();
529
+ });
530
+
531
+ it('INTAKE-05: intake failure human-readable output shows nextAction', async () => {
532
+ const candidates = [
533
+ { candidateId: 'cand-fail', artifactId: 'art-1', taskId: 'test-task-1', status: 'pending' },
534
+ ];
535
+ mockGetCandidatesByTaskId.mockResolvedValue(candidates);
536
+ mockIntake.mockImplementation(() => { throw new Error('Ledger write failed'); });
537
+
538
+ const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
539
+ const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => undefined) as () => never);
540
+
541
+ await handleDiagnoseRun({
542
+ taskId: 'test-task-1',
543
+ workspace: '/tmp/fake-workspace',
544
+ runtime: 'test-double',
545
+ json: false,
546
+ } as DiagnoseRunOptions);
547
+
548
+ const allOutput = consoleSpy.mock.calls.map(call => call[0]).join('\n');
549
+ expect(allOutput).toContain('INTAKE FAILED');
550
+ expect(allOutput).toContain('Next action: pd candidate intake --candidate-id cand-fail');
551
+ expect(exitSpy).toHaveBeenCalledWith(1);
552
+
553
+ consoleSpy.mockRestore();
554
+ exitSpy.mockRestore();
555
+ });
556
+
557
+ it('INTAKE-06: does not bypass ledger — consumed only set after intake succeeds', async () => {
558
+ const candidates = [
559
+ { candidateId: 'cand-1', artifactId: 'art-1', taskId: 'test-task-1', status: 'pending' },
560
+ ];
561
+ mockGetCandidatesByTaskId.mockResolvedValue(candidates);
562
+ mockIntake.mockImplementation(() => { throw new Error('Ledger write failed'); });
563
+
564
+ const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
565
+ const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => undefined) as () => never);
566
+
567
+ await handleDiagnoseRun({
568
+ taskId: 'test-task-1',
569
+ workspace: '/tmp/fake-workspace',
570
+ runtime: 'test-double',
571
+ json: true,
572
+ } as DiagnoseRunOptions);
573
+
574
+ expect(mockIntake).toHaveBeenCalledWith('cand-1');
575
+ expect(mockUpdateCandidateStatus).not.toHaveBeenCalled();
576
+
577
+ consoleSpy.mockRestore();
578
+ exitSpy.mockRestore();
579
+ });
580
+
581
+ it('INTAKE-07: already-consumed candidate — intake called but updateCandidateStatus skipped', async () => {
582
+ const candidates = [
583
+ { candidateId: 'cand-consumed', artifactId: 'art-1', taskId: 'test-task-1', status: 'consumed' },
584
+ ];
585
+ mockGetCandidatesByTaskId.mockResolvedValue(candidates);
586
+ mockIntake.mockResolvedValue({ id: 'ledger-existing', title: 'Existing', status: 'probation' });
587
+
588
+ const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
589
+ const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => undefined) as () => never);
590
+
591
+ await handleDiagnoseRun({
592
+ taskId: 'test-task-1',
593
+ workspace: '/tmp/fake-workspace',
594
+ runtime: 'test-double',
595
+ json: true,
596
+ } as DiagnoseRunOptions);
597
+
598
+ expect(mockIntake).toHaveBeenCalledWith('cand-consumed');
599
+ expect(mockUpdateCandidateStatus).not.toHaveBeenCalled();
600
+
601
+ const jsonOutput = consoleSpy.mock.calls.find(call => {
602
+ try {
603
+ const parsed = JSON.parse(call[0] as string);
604
+ return parsed.intake && Array.isArray(parsed.intake.candidates);
605
+ } catch { return false; }
606
+ });
607
+ const parsed = JSON.parse((jsonOutput as [string])[0]);
608
+ expect(parsed.intake.candidates[0].status).toBe('consumed');
609
+ expect(exitSpy).not.toHaveBeenCalledWith(1);
610
+
611
+ consoleSpy.mockRestore();
612
+ exitSpy.mockRestore();
613
+ });
614
+
615
+ it('INTAKE-08: no candidates produced — intake not called, output shows empty intake', async () => {
616
+ mockGetCandidatesByTaskId.mockResolvedValue([]);
617
+
618
+ const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
619
+ const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => undefined) as () => never);
620
+
621
+ await handleDiagnoseRun({
622
+ taskId: 'test-task-1',
623
+ workspace: '/tmp/fake-workspace',
624
+ runtime: 'test-double',
625
+ json: true,
626
+ } as DiagnoseRunOptions);
627
+
628
+ expect(mockIntake).not.toHaveBeenCalled();
629
+ expect(mockUpdateCandidateStatus).not.toHaveBeenCalled();
630
+
631
+ const jsonOutput = consoleSpy.mock.calls.find(call => {
632
+ try {
633
+ const parsed = JSON.parse(call[0] as string);
634
+ return parsed.intake;
635
+ } catch { return false; }
636
+ });
637
+ const parsed = JSON.parse((jsonOutput as [string])[0]);
638
+ expect(parsed.intake.candidates).toHaveLength(0);
639
+ expect(parsed.intake.enabled).toBe(true);
640
+ expect(exitSpy).not.toHaveBeenCalledWith(1);
641
+
642
+ consoleSpy.mockRestore();
643
+ exitSpy.mockRestore();
644
+ });
645
+
646
+ it('INTAKE-09: successful diagnose + intake — human-readable output shows consumed candidates', async () => {
647
+ const candidates = [
648
+ { candidateId: 'cand-1', artifactId: 'art-1', taskId: 'test-task-1', status: 'pending' },
649
+ ];
650
+ mockGetCandidatesByTaskId.mockResolvedValue(candidates);
651
+ mockIntake.mockResolvedValue({ id: 'ledger-1', title: 'Principle 1', status: 'probation' });
652
+
653
+ const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
654
+ const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => undefined) as () => never);
655
+
656
+ await handleDiagnoseRun({
657
+ taskId: 'test-task-1',
658
+ workspace: '/tmp/fake-workspace',
659
+ runtime: 'test-double',
660
+ json: false,
661
+ } as DiagnoseRunOptions);
662
+
663
+ const allOutput = consoleSpy.mock.calls.map(call => call[0]).join('\n');
664
+ expect(allOutput).toContain('Candidate Intake');
665
+ expect(allOutput).toContain('cand-1: consumed');
666
+ expect(allOutput).toContain('ledger-1');
667
+ expect(exitSpy).not.toHaveBeenCalledWith(1);
668
+
669
+ consoleSpy.mockRestore();
670
+ exitSpy.mockRestore();
671
+ });
672
+
673
+ it('INTAKE-10: --json mode outputs exactly one console.log with parseable JSON (no text header leak)', async () => {
674
+ mockGetCandidatesByTaskId.mockResolvedValue([]);
675
+
676
+ const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
677
+ const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => undefined) as () => never);
678
+
679
+ await handleDiagnoseRun({
680
+ taskId: 'test-task-1',
681
+ workspace: '/tmp/fake-workspace',
682
+ runtime: 'test-double',
683
+ json: true,
684
+ } as DiagnoseRunOptions);
685
+
686
+ expect(consoleSpy).toHaveBeenCalledTimes(1);
687
+ const rawOutput = consoleSpy.mock.calls[0][0] as string;
688
+ const parsed = JSON.parse(rawOutput);
689
+ expect(parsed.status).toBe('succeeded');
690
+ expect(parsed.intake).toBeDefined();
691
+ expect(exitSpy).not.toHaveBeenCalledWith(1);
692
+
693
+ consoleSpy.mockRestore();
694
+ exitSpy.mockRestore();
695
+ });
696
+
697
+ it('INTAKE-11: --json intake failure nextAction contains executable command with workspace', async () => {
698
+ const candidates = [
699
+ { candidateId: 'cand-err', artifactId: 'art-1', taskId: 'test-task-1', status: 'pending' },
700
+ ];
701
+ mockGetCandidatesByTaskId.mockResolvedValue(candidates);
702
+ mockIntake.mockImplementation(() => { throw new Error('DB locked'); });
703
+
704
+ const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
705
+ const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => undefined) as () => never);
706
+
707
+ await handleDiagnoseRun({
708
+ taskId: 'test-task-1',
709
+ workspace: '/tmp/fake-workspace',
710
+ runtime: 'test-double',
711
+ json: true,
712
+ } as DiagnoseRunOptions);
713
+
714
+ const rawOutput = consoleSpy.mock.calls[0][0] as string;
715
+ const parsed = JSON.parse(rawOutput);
716
+ const failed = parsed.intake.candidates[0];
717
+ expect(failed.status).toBe('intake_failed');
718
+ expect(failed.nextAction).toMatch(/^pd candidate intake --candidate-id cand-err --workspace "/);
719
+ expect(failed.nextAction).toContain('/tmp/fake-workspace');
720
+ expect(exitSpy).toHaveBeenCalledWith(1);
721
+
722
+ consoleSpy.mockRestore();
723
+ exitSpy.mockRestore();
724
+ });
725
+
726
+ it('INTAKE-12: failed diagnosis does not trigger intake (process.exit stubbed)', async () => {
727
+ const { run } = await import('@principles/core/runtime-v2');
728
+ vi.mocked(run).mockResolvedValueOnce({
729
+ status: 'failed',
730
+ taskId: 'test-task-1',
731
+ errorCategory: 'timeout',
732
+ failureReason: 'LLM call timed out',
733
+ attemptCount: 3,
734
+ });
735
+
736
+ const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
737
+ const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => undefined) as () => never);
738
+
739
+ await handleDiagnoseRun({
740
+ taskId: 'test-task-1',
741
+ workspace: '/tmp/fake-workspace',
742
+ runtime: 'test-double',
743
+ json: true,
744
+ } as DiagnoseRunOptions);
745
+
746
+ expect(mockGetCandidatesByTaskId).not.toHaveBeenCalled();
747
+ expect(mockIntake).not.toHaveBeenCalled();
748
+ expect(mockUpdateCandidateStatus).not.toHaveBeenCalled();
749
+ expect(exitSpy).toHaveBeenCalledWith(1);
750
+
751
+ consoleSpy.mockRestore();
752
+ exitSpy.mockRestore();
753
+ });
754
+ });
755
+
756
+ describe('Commander wiring for --no-intake', () => {
757
+ function createDiagnoseProgram(): { program: Command; capturedOpts: Record<string, unknown> } {
758
+ const program = new Command();
759
+ program.exitOverride();
760
+ const capturedOpts: Record<string, unknown> = {};
761
+
762
+ program
763
+ .command('diagnose')
764
+ .command('run')
765
+ .option('--no-intake', 'Skip candidate intake after successful diagnosis')
766
+ .option('--json', 'Output raw JSON')
767
+ .action(async (opts) => {
768
+ Object.assign(capturedOpts, opts);
769
+ });
770
+
771
+ return { program, capturedOpts };
772
+ }
773
+
774
+ it('CMD-01: --no-intake accepted, sets opts.intake === false', async () => {
775
+ const { program, capturedOpts } = createDiagnoseProgram();
776
+ await program.parseAsync(['node', 'pd', 'diagnose', 'run', '--no-intake']);
777
+ expect(capturedOpts.intake).toBe(false);
778
+ });
779
+
780
+ it('CMD-02: default (no flag) → opts.intake === true', async () => {
781
+ const { program, capturedOpts } = createDiagnoseProgram();
782
+ await program.parseAsync(['node', 'pd', 'diagnose', 'run']);
783
+ expect(capturedOpts.intake).toBe(true);
784
+ });
785
+
786
+ it('CMD-03: --intake is not a valid option (Commander rejects it)', async () => {
787
+ const { program } = createDiagnoseProgram();
788
+ await expect(
789
+ program.parseAsync(['node', 'pd', 'diagnose', 'run', '--intake'])
790
+ ).rejects.toThrow();
791
+ });
792
+ });