@principles/pd-cli 1.152.11 → 1.152.13

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 (303) hide show
  1. package/dist/commands/candidate.d.ts +3 -3
  2. package/dist/commands/candidate.d.ts.map +1 -1
  3. package/dist/commands/candidate.js +9 -21
  4. package/dist/commands/candidate.js.map +1 -1
  5. package/dist/index.js +0 -0
  6. package/dist/services/rulehost-pipeline-runner.d.ts.map +1 -1
  7. package/dist/services/rulehost-pipeline-runner.js +4 -17
  8. package/dist/services/rulehost-pipeline-runner.js.map +1 -1
  9. package/package.json +13 -7
  10. package/dist/commands/__tests__/intent-flag-wiring.test.d.ts +0 -9
  11. package/dist/commands/__tests__/intent-flag-wiring.test.d.ts.map +0 -1
  12. package/dist/commands/__tests__/intent-flag-wiring.test.js +0 -237
  13. package/dist/commands/__tests__/intent-flag-wiring.test.js.map +0 -1
  14. package/dist/commands/__tests__/legacy-cleanup.test.d.ts +0 -18
  15. package/dist/commands/__tests__/legacy-cleanup.test.d.ts.map +0 -1
  16. package/dist/commands/__tests__/legacy-cleanup.test.js +0 -467
  17. package/dist/commands/__tests__/legacy-cleanup.test.js.map +0 -1
  18. package/dist/commands/__tests__/mvp-smoke.test.d.ts +0 -15
  19. package/dist/commands/__tests__/mvp-smoke.test.d.ts.map +0 -1
  20. package/dist/commands/__tests__/mvp-smoke.test.js +0 -262
  21. package/dist/commands/__tests__/mvp-smoke.test.js.map +0 -1
  22. package/dist/commands/__tests__/pain-retry-maxTokens-flag.test.d.ts +0 -2
  23. package/dist/commands/__tests__/pain-retry-maxTokens-flag.test.d.ts.map +0 -1
  24. package/dist/commands/__tests__/pain-retry-maxTokens-flag.test.js +0 -60
  25. package/dist/commands/__tests__/pain-retry-maxTokens-flag.test.js.map +0 -1
  26. package/dist/commands/__tests__/rulecode-flag-wiring.test.d.ts +0 -21
  27. package/dist/commands/__tests__/rulecode-flag-wiring.test.d.ts.map +0 -1
  28. package/dist/commands/__tests__/rulecode-flag-wiring.test.js +0 -186
  29. package/dist/commands/__tests__/rulecode-flag-wiring.test.js.map +0 -1
  30. package/dist/commands/__tests__/rulecode-handler.test.d.ts +0 -16
  31. package/dist/commands/__tests__/rulecode-handler.test.d.ts.map +0 -1
  32. package/dist/commands/__tests__/rulecode-handler.test.js +0 -285
  33. package/dist/commands/__tests__/rulecode-handler.test.js.map +0 -1
  34. package/dist/commands/__tests__/run-rulehost-flag-wiring.test.d.ts +0 -24
  35. package/dist/commands/__tests__/run-rulehost-flag-wiring.test.d.ts.map +0 -1
  36. package/dist/commands/__tests__/run-rulehost-flag-wiring.test.js +0 -232
  37. package/dist/commands/__tests__/run-rulehost-flag-wiring.test.js.map +0 -1
  38. package/dist/commands/__tests__/runtime-activation-nextaction.test.d.ts +0 -15
  39. package/dist/commands/__tests__/runtime-activation-nextaction.test.d.ts.map +0 -1
  40. package/dist/commands/__tests__/runtime-activation-nextaction.test.js +0 -73
  41. package/dist/commands/__tests__/runtime-activation-nextaction.test.js.map +0 -1
  42. package/dist/commands/__tests__/runtime-probe-config.test.d.ts +0 -20
  43. package/dist/commands/__tests__/runtime-probe-config.test.d.ts.map +0 -1
  44. package/dist/commands/__tests__/runtime-probe-config.test.js +0 -389
  45. package/dist/commands/__tests__/runtime-probe-config.test.js.map +0 -1
  46. package/dist/commands/__tests__/telemetry-flag-wiring.test.d.ts +0 -9
  47. package/dist/commands/__tests__/telemetry-flag-wiring.test.d.ts.map +0 -1
  48. package/dist/commands/__tests__/telemetry-flag-wiring.test.js +0 -76
  49. package/dist/commands/__tests__/telemetry-flag-wiring.test.js.map +0 -1
  50. package/dist/commands/__tests__/version.test.d.ts +0 -2
  51. package/dist/commands/__tests__/version.test.d.ts.map +0 -1
  52. package/dist/commands/__tests__/version.test.js +0 -238
  53. package/dist/commands/__tests__/version.test.js.map +0 -1
  54. package/dist/commands/runtime-uat.guard.test.d.ts +0 -2
  55. package/dist/commands/runtime-uat.guard.test.d.ts.map +0 -1
  56. package/dist/commands/runtime-uat.guard.test.js +0 -153
  57. package/dist/commands/runtime-uat.guard.test.js.map +0 -1
  58. package/dist/services/__tests__/cli-output.test.d.ts +0 -18
  59. package/dist/services/__tests__/cli-output.test.d.ts.map +0 -1
  60. package/dist/services/__tests__/cli-output.test.js +0 -103
  61. package/dist/services/__tests__/cli-output.test.js.map +0 -1
  62. package/dist/services/__tests__/evaluator-runner-deps.test.d.ts +0 -2
  63. package/dist/services/__tests__/evaluator-runner-deps.test.d.ts.map +0 -1
  64. package/dist/services/__tests__/evaluator-runner-deps.test.js +0 -343
  65. package/dist/services/__tests__/evaluator-runner-deps.test.js.map +0 -1
  66. package/dist/services/__tests__/rulehost-readiness.test.d.ts +0 -2
  67. package/dist/services/__tests__/rulehost-readiness.test.d.ts.map +0 -1
  68. package/dist/services/__tests__/rulehost-readiness.test.js +0 -311
  69. package/dist/services/__tests__/rulehost-readiness.test.js.map +0 -1
  70. package/dist/services/__tests__/runtime-adapter-resolver.test.d.ts +0 -18
  71. package/dist/services/__tests__/runtime-adapter-resolver.test.d.ts.map +0 -1
  72. package/dist/services/__tests__/runtime-adapter-resolver.test.js +0 -673
  73. package/dist/services/__tests__/runtime-adapter-resolver.test.js.map +0 -1
  74. package/dist/utils/production-workspace-guard.test.d.ts +0 -2
  75. package/dist/utils/production-workspace-guard.test.d.ts.map +0 -1
  76. package/dist/utils/production-workspace-guard.test.js +0 -137
  77. package/dist/utils/production-workspace-guard.test.js.map +0 -1
  78. package/scripts/llm-dogfood.ts +0 -456
  79. package/scripts/migrate-illegal-expected-decision.ts +0 -252
  80. package/src/commands/__tests__/intent-flag-wiring.test.ts +0 -282
  81. package/src/commands/__tests__/legacy-cleanup.test.ts +0 -602
  82. package/src/commands/__tests__/mvp-smoke.test.ts +0 -300
  83. package/src/commands/__tests__/pain-retry-maxTokens-flag.test.ts +0 -73
  84. package/src/commands/__tests__/rulecode-flag-wiring.test.ts +0 -237
  85. package/src/commands/__tests__/rulecode-handler.test.ts +0 -369
  86. package/src/commands/__tests__/run-rulehost-flag-wiring.test.ts +0 -292
  87. package/src/commands/__tests__/runtime-activation-nextaction.test.ts +0 -81
  88. package/src/commands/__tests__/runtime-probe-config.test.ts +0 -432
  89. package/src/commands/__tests__/telemetry-flag-wiring.test.ts +0 -84
  90. package/src/commands/__tests__/version.test.ts +0 -251
  91. package/src/commands/admission-gate.ts +0 -30
  92. package/src/commands/artifact.ts +0 -82
  93. package/src/commands/build-trajectory-evidence.ts +0 -298
  94. package/src/commands/candidate.ts +0 -1260
  95. package/src/commands/codex-ingest-catchup.ts +0 -114
  96. package/src/commands/codex-ingest-quarantine.ts +0 -127
  97. package/src/commands/codex-reconcile.ts +0 -84
  98. package/src/commands/codex-setup.ts +0 -476
  99. package/src/commands/codex-worker.ts +0 -185
  100. package/src/commands/command-helpers.ts +0 -24
  101. package/src/commands/config-doctor.ts +0 -193
  102. package/src/commands/console.ts +0 -797
  103. package/src/commands/context.ts +0 -72
  104. package/src/commands/demo-story-a.ts +0 -241
  105. package/src/commands/diagnose.ts +0 -756
  106. package/src/commands/errors-list.ts +0 -441
  107. package/src/commands/evolution-tasks-list.ts +0 -48
  108. package/src/commands/evolution-tasks-show.ts +0 -62
  109. package/src/commands/flow.ts +0 -60
  110. package/src/commands/health-codex.ts +0 -576
  111. package/src/commands/health.ts +0 -315
  112. package/src/commands/history.ts +0 -63
  113. package/src/commands/intent.ts +0 -457
  114. package/src/commands/legacy-cleanup.ts +0 -531
  115. package/src/commands/legacy-import.ts +0 -104
  116. package/src/commands/mvp-smoke.ts +0 -160
  117. package/src/commands/pain-evidence.ts +0 -226
  118. package/src/commands/pain-list.ts +0 -239
  119. package/src/commands/pain-record.ts +0 -612
  120. package/src/commands/pain-retry.ts +0 -916
  121. package/src/commands/principles-stats.ts +0 -700
  122. package/src/commands/proven-channel-baseline.ts +0 -115
  123. package/src/commands/quality-scorecard.ts +0 -272
  124. package/src/commands/remediation-output.ts +0 -66
  125. package/src/commands/rulecode.ts +0 -465
  126. package/src/commands/run.ts +0 -89
  127. package/src/commands/runtime-activation.ts +0 -1629
  128. package/src/commands/runtime-artifact-repair.ts +0 -524
  129. package/src/commands/runtime-canary.ts +0 -395
  130. package/src/commands/runtime-compatibility-scan.ts +0 -105
  131. package/src/commands/runtime-diagnostics-export.ts +0 -229
  132. package/src/commands/runtime-features.ts +0 -153
  133. package/src/commands/runtime-gfi-snapshot.ts +0 -135
  134. package/src/commands/runtime-health-snapshot.ts +0 -108
  135. package/src/commands/runtime-init.ts +0 -429
  136. package/src/commands/runtime-internalization-enqueue-successors.ts +0 -530
  137. package/src/commands/runtime-internalization-integrity-repair.ts +0 -79
  138. package/src/commands/runtime-internalization-integrity.ts +0 -107
  139. package/src/commands/runtime-internalization-queue.ts +0 -135
  140. package/src/commands/runtime-internalization-retry.ts +0 -191
  141. package/src/commands/runtime-internalization-run-once.ts +0 -791
  142. package/src/commands/runtime-internalization-run-rulehost.ts +0 -749
  143. package/src/commands/runtime-internalization-wake-once.ts +0 -87
  144. package/src/commands/runtime-pain-flood-simulation.ts +0 -121
  145. package/src/commands/runtime-pruning.ts +0 -438
  146. package/src/commands/runtime-recovery-failed-tasks.ts +0 -258
  147. package/src/commands/runtime-recovery.ts +0 -109
  148. package/src/commands/runtime-synthetic-baseline.ts +0 -70
  149. package/src/commands/runtime-uat.guard.test.ts +0 -175
  150. package/src/commands/runtime-uat.ts +0 -380
  151. package/src/commands/runtime.ts +0 -473
  152. package/src/commands/samples-list.ts +0 -39
  153. package/src/commands/samples-review.ts +0 -36
  154. package/src/commands/task.ts +0 -271
  155. package/src/commands/telemetry.ts +0 -349
  156. package/src/commands/trace.ts +0 -174
  157. package/src/commands/trajectory-db-unavailable.ts +0 -19
  158. package/src/commands/trajectory.ts +0 -64
  159. package/src/commands/version.ts +0 -49
  160. package/src/config-reader.ts +0 -122
  161. package/src/index.ts +0 -1276
  162. package/src/legacy/legacy-import.ts +0 -179
  163. package/src/legacy/session-history-import.ts +0 -203
  164. package/src/resolve-workspace.ts +0 -106
  165. package/src/services/__tests__/cli-output.test.ts +0 -130
  166. package/src/services/__tests__/evaluator-runner-deps.test.ts +0 -382
  167. package/src/services/__tests__/rulehost-readiness.test.ts +0 -363
  168. package/src/services/__tests__/runtime-adapter-resolver.test.ts +0 -793
  169. package/src/services/cli-output.ts +0 -95
  170. package/src/services/config-doctor.ts +0 -697
  171. package/src/services/console-launcher.ts +0 -537
  172. package/src/services/demo-rule-compiler.ts +0 -40
  173. package/src/services/demo-story-a-runner.ts +0 -471
  174. package/src/services/mainline-snapshot-assembler.ts +0 -579
  175. package/src/services/pain-flood-simulation-runner.ts +0 -462
  176. package/src/services/pd-config-loader.ts +0 -433
  177. package/src/services/proven-channel-baseline-runner.ts +0 -157
  178. package/src/services/quality-scorecard/data-extractor.ts +0 -150
  179. package/src/services/quality-scorecard/local-evaluator.ts +0 -142
  180. package/src/services/quality-scorecard/strong-model-gate.ts +0 -202
  181. package/src/services/resolve-runtime-from-pd-config.ts +0 -230
  182. package/src/services/rulehost-pipeline-runner.ts +0 -1216
  183. package/src/services/rulehost-readiness.ts +0 -326
  184. package/src/services/runtime-adapter-resolver.ts +0 -364
  185. package/src/services/synthetic-baseline-runner.ts +0 -400
  186. package/src/services/version-report.ts +0 -253
  187. package/src/services/workspace-tool-semantics.ts +0 -26
  188. package/src/utils/path-security.ts +0 -96
  189. package/src/utils/production-workspace-guard.test.ts +0 -157
  190. package/src/utils/production-workspace-guard.ts +0 -210
  191. package/tests/bdd/cli-contract.steps.ts +0 -346
  192. package/tests/bdd/principles-stats.steps.ts +0 -211
  193. package/tests/bdd/support/gherkin-loader.ts +0 -147
  194. package/tests/bdd/support/repo-root.ts +0 -62
  195. package/tests/bdd/support/vitest-bdd.ts +0 -179
  196. package/tests/commands/admission-gate-coverage.test.ts +0 -79
  197. package/tests/commands/build-trajectory-evidence.test.ts +0 -508
  198. package/tests/commands/candidate-audit-repair.test.ts +0 -421
  199. package/tests/commands/candidate-intake.test.ts +0 -732
  200. package/tests/commands/candidate-internalization-backfill.test.ts +0 -548
  201. package/tests/commands/candidate-internalize-lineage.test.ts +0 -565
  202. package/tests/commands/candidate-internalize.test.ts +0 -299
  203. package/tests/commands/candidate-route.test.ts +0 -329
  204. package/tests/commands/candidate-show.test.ts +0 -95
  205. package/tests/commands/cli-command-tree.test.ts +0 -144
  206. package/tests/commands/cli-help-snapshot.test.ts +0 -155
  207. package/tests/commands/cli-skill-contract.test.ts +0 -121
  208. package/tests/commands/codex-consent-reversibility.steps.test.ts +0 -299
  209. package/tests/commands/codex-ingest-quarantine.test.ts +0 -136
  210. package/tests/commands/codex-setup.test.ts +0 -297
  211. package/tests/commands/codex-worker-daemon.test.ts +0 -95
  212. package/tests/commands/codex-worker-registration.test.ts +0 -207
  213. package/tests/commands/config-doctor.test.ts +0 -336
  214. package/tests/commands/console-launcher-edge-cases.test.ts +0 -388
  215. package/tests/commands/console-open.test.ts +0 -1381
  216. package/tests/commands/context.test.ts +0 -114
  217. package/tests/commands/demo-story-a.test.ts +0 -318
  218. package/tests/commands/diagnose.test.ts +0 -1803
  219. package/tests/commands/errors-list.test.ts +0 -679
  220. package/tests/commands/evolution-tasks-list.test.ts +0 -41
  221. package/tests/commands/evolution-tasks-show.test.ts +0 -46
  222. package/tests/commands/health-codex.test.ts +0 -349
  223. package/tests/commands/health-host-wiring.test.ts +0 -76
  224. package/tests/commands/health-reliability.test.ts +0 -99
  225. package/tests/commands/health.test.ts +0 -413
  226. package/tests/commands/intent.test.ts +0 -321
  227. package/tests/commands/legacy-cleanup.test.ts +0 -148
  228. package/tests/commands/pain-evidence.test.ts +0 -518
  229. package/tests/commands/pain-list.test.ts +0 -219
  230. package/tests/commands/pain-record-async.test.ts +0 -335
  231. package/tests/commands/pain-record-session-parser.test.ts +0 -228
  232. package/tests/commands/pain-record.test.ts +0 -851
  233. package/tests/commands/pain-retry.test.ts +0 -1121
  234. package/tests/commands/plugin-config-resolution-cutover.test.ts +0 -229
  235. package/tests/commands/pri-393-runtime-config-unification.test.ts +0 -443
  236. package/tests/commands/principles-stats-wiring.test.ts +0 -135
  237. package/tests/commands/principles-stats.test.ts +0 -274
  238. package/tests/commands/product-path-regression.test.ts +0 -86
  239. package/tests/commands/proven-channel-baseline.test.ts +0 -441
  240. package/tests/commands/rulecode.test.ts +0 -177
  241. package/tests/commands/run-rulehost-handler.test.ts +0 -908
  242. package/tests/commands/runtime-activation-approve-flag-wiring.test.ts +0 -227
  243. package/tests/commands/runtime-activation-deactivate-flag-wiring.test.ts +0 -167
  244. package/tests/commands/runtime-activation-dispatch-flag-wiring.test.ts +0 -262
  245. package/tests/commands/runtime-activation-governance-audit.test.ts +0 -95
  246. package/tests/commands/runtime-activation-list-flag-wiring.test.ts +0 -193
  247. package/tests/commands/runtime-activation-promote-flag-wiring.test.ts +0 -248
  248. package/tests/commands/runtime-activation-shadow-telemetry.test.ts +0 -130
  249. package/tests/commands/runtime-activation.test.ts +0 -2052
  250. package/tests/commands/runtime-artifact-repair-registration.test.ts +0 -74
  251. package/tests/commands/runtime-artifact-repair.test.ts +0 -404
  252. package/tests/commands/runtime-canary.test.ts +0 -430
  253. package/tests/commands/runtime-compatibility-scan.test.ts +0 -146
  254. package/tests/commands/runtime-diagnostics-export.test.ts +0 -175
  255. package/tests/commands/runtime-features.test.ts +0 -249
  256. package/tests/commands/runtime-health-snapshot.test.ts +0 -358
  257. package/tests/commands/runtime-init-empty-workspace.test.ts +0 -252
  258. package/tests/commands/runtime-init.test.ts +0 -420
  259. package/tests/commands/runtime-internalization-enqueue-successors.test.ts +0 -782
  260. package/tests/commands/runtime-internalization-integrity-repair.test.ts +0 -239
  261. package/tests/commands/runtime-internalization-integrity.test.ts +0 -140
  262. package/tests/commands/runtime-internalization-queue.test.ts +0 -380
  263. package/tests/commands/runtime-internalization-retry-owner-authority.test.ts +0 -431
  264. package/tests/commands/runtime-internalization-run-once-evaluator-parity.test.ts +0 -269
  265. package/tests/commands/runtime-internalization-run-once-language.test.ts +0 -303
  266. package/tests/commands/runtime-internalization-run-once-real-config.test.ts +0 -237
  267. package/tests/commands/runtime-internalization-run-once-rollout-parity.test.ts +0 -706
  268. package/tests/commands/runtime-internalization-run-once.test.ts +0 -1791
  269. package/tests/commands/runtime-internalization-wake-once.test.ts +0 -171
  270. package/tests/commands/runtime-internalization.test.ts +0 -53
  271. package/tests/commands/runtime-pain-flood-simulation.test.ts +0 -418
  272. package/tests/commands/runtime-pruning.test.ts +0 -694
  273. package/tests/commands/runtime-recovery-failed-tasks.test.ts +0 -328
  274. package/tests/commands/runtime-recovery.test.ts +0 -120
  275. package/tests/commands/runtime-synthetic-baseline.test.ts +0 -249
  276. package/tests/commands/runtime-uat.test.ts +0 -397
  277. package/tests/commands/runtime.test.ts +0 -392
  278. package/tests/commands/samples-list.test.ts +0 -43
  279. package/tests/commands/samples-review.test.ts +0 -41
  280. package/tests/commands/task.test.ts +0 -153
  281. package/tests/commands/telemetry.test.ts +0 -190
  282. package/tests/commands/trace.test.ts +0 -316
  283. package/tests/config-reader.test.ts +0 -143
  284. package/tests/e2e/candidate-intake-e2e.test.ts +0 -338
  285. package/tests/e2e/cli-full-flow.test.ts +0 -233
  286. package/tests/e2e/cross-package-acceptance.test.ts +0 -635
  287. package/tests/helpers/pd-cli-path.ts +0 -5
  288. package/tests/pd-cli-smoke.test.ts +0 -13
  289. package/tests/resolve-workspace.test.ts +0 -196
  290. package/tests/services/config-doctor-signal-health.test.ts +0 -109
  291. package/tests/services/console-launcher.test.ts +0 -114
  292. package/tests/services/demo-rule-compiler.test.ts +0 -242
  293. package/tests/services/mainline-snapshot-assembler.test.ts +0 -572
  294. package/tests/services/pd-config-loader.test.ts +0 -505
  295. package/tests/services/proven-channel-baseline-runner.test.ts +0 -30
  296. package/tests/services/quality-scorecard/strong-model-gate.test.ts +0 -133
  297. package/tests/services/resolve-runtime-from-pd-config.test.ts +0 -485
  298. package/tests/services/rulehost-pipeline-e2e.test.ts +0 -538
  299. package/tests/services/rulehost-pipeline-runner.test.ts +0 -1175
  300. package/tests/services/workspace-tool-semantics.test.ts +0 -76
  301. package/tests/utils/path-security.test.ts +0 -180
  302. package/tsconfig.json +0 -26
  303. package/vitest.config.ts +0 -9
@@ -1,1803 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach, afterEach, beforeAll } from 'vitest';
2
- import { Command } from 'commander';
3
-
4
- const { MockRuntimeStateManager, mockGetCandidatesByTaskId, mockUpdateCandidateStatus, mockCreateTask, mockGetTask } = vi.hoisted(() => {
5
- const mockGetCandidatesByTaskId = vi.fn().mockResolvedValue([]);
6
- const mockUpdateCandidateStatus = vi.fn().mockResolvedValue(undefined);
7
- const mockCreateTask = vi.fn().mockResolvedValue(undefined);
8
- const mockGetTask = vi.fn().mockResolvedValue({
9
- taskId: 'test-task-1',
10
- status: 'pending',
11
- attemptCount: 0,
12
- maxAttempts: 3,
13
- lastError: null,
14
- });
15
-
16
- class MockRuntimeStateManager {
17
- initialize = vi.fn().mockResolvedValue(undefined);
18
- close = vi.fn().mockResolvedValue(undefined);
19
- getTask = mockGetTask;
20
- createTask = mockCreateTask;
21
- getCandidatesByTaskId = mockGetCandidatesByTaskId;
22
- updateCandidateStatus = mockUpdateCandidateStatus;
23
- connection = {} as Record<string, unknown>;
24
- taskStore = {};
25
- runStore = {};
26
- piArtifactStore = {};
27
- getRetryPolicy = vi.fn().mockReturnValue({
28
- shouldRetry: () => true,
29
- calculateBackoff: () => 10,
30
- });
31
- }
32
- return { MockRuntimeStateManager, mockGetCandidatesByTaskId, mockUpdateCandidateStatus, mockCreateTask, mockGetTask };
33
- }, { validateType: true });
34
-
35
- const { mockIntake, MockCandidateIntakeService } = vi.hoisted(() => {
36
- const mockIntake = vi.fn();
37
- function MockCandidateIntakeService(this: any) {
38
- return { intake: mockIntake };
39
- }
40
- MockCandidateIntakeService.prototype = {};
41
- return { mockIntake, MockCandidateIntakeService };
42
- });
43
-
44
- const { MockPrincipleTreeLedgerAdapter } = vi.hoisted(() => {
45
- function MockPrincipleTreeLedgerAdapter(this: any) {
46
- return {};
47
- }
48
- MockPrincipleTreeLedgerAdapter.prototype = {};
49
- return { MockPrincipleTreeLedgerAdapter };
50
- });
51
-
52
- const { mockResolveRuntimeFromPdConfig } = vi.hoisted(() => {
53
- const mockResolveRuntimeFromPdConfig = vi.fn().mockReturnValue({
54
- result: {
55
- runtimeKind: 'pi-ai',
56
- provider: 'test-provider',
57
- model: 'test-model',
58
- apiKeyEnv: 'TEST_KEY',
59
- timeoutMs: 300000,
60
- agentId: 'main',
61
- },
62
- legacyWarnings: [],
63
- configSource: '.pd/config.yaml',
64
- configLoadResult: { ok: true, effective: {}, defaults: {}, legacyFilesDetected: [] },
65
- });
66
- return { mockResolveRuntimeFromPdConfig };
67
- });
68
-
69
- // BUG-1: capture runner constructor args to verify effectiveConfig wiring
70
- const { diagRootCauseRunnerCtor, diagDistillerRunnerCtor, diagRouterRunnerCtor } = vi.hoisted(() => {
71
- const diagRootCauseRunnerCtor = vi.fn().mockImplementation(function () { return {}; });
72
- const diagDistillerRunnerCtor = vi.fn().mockImplementation(function () { return {}; });
73
- const diagRouterRunnerCtor = vi.fn().mockImplementation(function () { return {}; });
74
- return { diagRootCauseRunnerCtor, diagDistillerRunnerCtor, diagRouterRunnerCtor };
75
- });
76
-
77
- // BUG-2: capture buildDreamerSeedFromCandidate sourcePainId arg + mock resolver
78
- const { buildDreamerSeedCalls, mockResolveSourcePainId } = vi.hoisted(() => {
79
- const buildDreamerSeedCalls: Array<{ candidateId: string; sourcePainId?: string }> = [];
80
- const mockResolveSourcePainId = vi.fn().mockResolvedValue('pain_test-source-1');
81
- return { buildDreamerSeedCalls, mockResolveSourcePainId };
82
- });
83
-
84
- vi.mock('../../src/resolve-workspace.js', () => ({
85
- resolveWorkspaceDir: vi.fn().mockReturnValue('/tmp/fake-workspace'),
86
- }));
87
-
88
- vi.mock('@principles/core/runtime-v2', () => {
89
- return {
90
- RuntimeStateManager: vi.fn().mockImplementation(function () {
91
- return new MockRuntimeStateManager();
92
- }),
93
- SqliteHistoryQuery: vi.fn().mockImplementation(function () { return {}; }),
94
- SqliteContextAssembler: vi.fn().mockImplementation(function () { return {}; }),
95
- SqliteDiagnosticianCommitter: vi.fn().mockImplementation(function () { return {}; }),
96
- SqliteTrajectoryLocator: vi.fn().mockImplementation(function () { return {}; }),
97
- SqliteSourceTraceLocator: vi.fn().mockImplementation(function () { return {}; }),
98
- StoreEventEmitter: vi.fn().mockImplementation(function () { return {}; }),
99
- storeEmitter: { emitTelemetry: vi.fn() },
100
- SplitDiagnosticianRunner: vi.fn().mockImplementation(function () { return {}; }),
101
- DiagRootCauseRunner: diagRootCauseRunnerCtor,
102
- DiagDistillerRunner: diagDistillerRunnerCtor,
103
- DiagRouterRunner: diagRouterRunnerCtor,
104
- DefaultDiagRootCauseValidator: vi.fn().mockImplementation(function () { return {}; }),
105
- DefaultDiagDistillerValidator: vi.fn().mockImplementation(function () { return {}; }),
106
- DisabledDiagnosticianRunner: vi.fn().mockImplementation(function () { return {}; }),
107
- TestDoubleRuntimeAdapter: vi.fn().mockImplementation(function () { return {}; }),
108
- OpenClawCliRuntimeAdapter: vi.fn().mockImplementation(function () { return {}; }),
109
- PiAiRuntimeAdapter: vi.fn().mockImplementation(function () { return {}; }),
110
- SPLIT_PIPELINE_TOTAL_TIMEOUT_MS: 300000,
111
- // PRI-638: capability gate — available by default; disabled cases override this.
112
- resolveDiagnosticianCapability: vi.fn((): { available: boolean; reason?: string; message?: string; nextAction?: string } => ({ available: true })),
113
- PDRuntimeError: class PDRuntimeError extends Error {
114
- constructor(public category: string, message: string) {
115
- super(message);
116
- this.name = 'PDRuntimeError';
117
- }
118
- },
119
- CandidateIntakeService: MockCandidateIntakeService,
120
- // PRI-503: admission gate mock — admit by default so existing diagnose/intake
121
- // tests keep their original flow. Tests that need to assert refusal behavior
122
- // can override this mock per-test.
123
- evaluateCandidateAdmissionFromRecord: vi.fn().mockReturnValue({
124
- decision: 'admitted',
125
- reason: 'mock_admitted',
126
- nextAction: 'none',
127
- evidenceStatus: 'unknown',
128
- }),
129
- resolveRuntimeConfig: vi.fn().mockReturnValue({
130
- runtimeKind: 'pi-ai',
131
- provider: 'test-provider',
132
- model: 'test-model',
133
- apiKeyEnv: 'TEST_KEY',
134
- timeoutMs: 300000,
135
- agentId: 'main',
136
- }),
137
- isRuntimeConfigError: vi.fn().mockReturnValue(false),
138
- isFeatureEnabled: vi.fn().mockReturnValue(true),
139
- resolveOutputLanguage: vi.fn().mockReturnValue({ outputLanguage: 'zh-CN' }),
140
- validatePdConfig: vi.fn().mockReturnValue({ valid: true, errors: [] }),
141
- computeEffectivePdConfig: vi.fn().mockReturnValue({ config: {}, source: 'defaults', warnings: [] }),
142
- computeFeatureFlagsFromConfig: vi.fn().mockReturnValue({}),
143
- redactPdConfig: vi.fn().mockImplementation((c) => c),
144
- run: vi.fn().mockResolvedValue({
145
- status: 'succeeded',
146
- taskId: 'test-task-1',
147
- output: {
148
- valid: true,
149
- diagnosisId: 'diag-123',
150
- taskId: 'test-task-1',
151
- summary: 'Test diagnosis summary',
152
- rootCause: 'Test: test root cause',
153
- violatedPrinciples: [],
154
- evidence: [],
155
- recommendations: [{ kind: 'implementation', description: 'Test recommendation' }],
156
- confidence: 0.9,
157
- },
158
- }),
159
- status: vi.fn(),
160
- PrincipleTreeLedgerAdapter: MockPrincipleTreeLedgerAdapter,
161
- // Defect-004 dreamer seed mocks — minimal stubs that mirror real behavior
162
- // BUG-2: capture sourcePainId arg to verify lineage wiring
163
- buildDreamerSeedFromCandidate: vi.fn().mockImplementation((candidate: { candidateId: string }, opts: { route: string; ready: boolean; sourcePainId?: string }) => {
164
- buildDreamerSeedCalls.push({ candidateId: candidate.candidateId, sourcePainId: opts?.sourcePainId });
165
- return {
166
- taskId: `dreamer-${candidate.candidateId}`,
167
- taskKind: 'dreamer' as const,
168
- channel: 'prompt' as const,
169
- diagnosticJson: JSON.stringify({ candidateId: candidate.candidateId }),
170
- status: 'pending' as const,
171
- attemptCount: 0,
172
- maxAttempts: 3,
173
- };
174
- }),
175
- // PRI-720 C6: candidate-level dedup — tests assume a clean store (no
176
- // pre-existing dreamer task under any channel suffix).
177
- findExistingDreamerTask: vi.fn().mockResolvedValue(null),
178
- CANDIDATE_KIND_TO_ROUTE: {
179
- principle: 'principle-candidate',
180
- rule: 'rule-candidate',
181
- prompt: 'prompt-candidate',
182
- },
183
- ROUTE_CHANNEL_MAP: {
184
- 'principle-candidate': 'prompt',
185
- 'rule-candidate': 'code_tool_hook',
186
- 'prompt-candidate': 'prompt',
187
- },
188
- MVP_ENABLED_CHANNELS: new Set(['prompt', 'code_tool_hook', 'defer_archive']),
189
- };
190
- });
191
-
192
- vi.mock('../../src/config-reader.js', () => ({
193
- readOutputLanguageFromWorkspace: vi.fn().mockReturnValue({ outputLanguage: 'zh-CN' }),
194
- }));
195
-
196
- vi.mock('../../src/services/pd-config-loader.js', () => ({
197
- loadPdConfig: vi.fn().mockReturnValue({
198
- ok: true,
199
- effective: { config: { featureFlags: { diagnostician_llm_degradation: true } }, source: 'file', warnings: [] },
200
- defaults: { config: {}, source: 'defaults', warnings: [] },
201
- }),
202
- computeFlagsFromLoadResult: vi.fn().mockReturnValue({}),
203
- // PRI-720: full-chain override resolution — undefined (standard topology) in tests.
204
- resolvePromptFullPipelineSeedMode: vi.fn().mockReturnValue(undefined),
205
- }));
206
-
207
- vi.mock('../../src/services/resolve-runtime-from-pd-config.js', () => ({
208
- resolveRuntimeFromPdConfig: mockResolveRuntimeFromPdConfig,
209
- }));
210
-
211
- // BUG-2: mock resolveSourcePainIdFromDiagnostician imported by diagnose.ts from candidate.js
212
- vi.mock('../../src/commands/candidate.js', () => ({
213
- resolveSourcePainIdFromDiagnostician: mockResolveSourcePainId,
214
- }));
215
-
216
- import { handleDiagnoseRun, handleDiagnoseStatus, type DiagnoseRunOptions } from '../../src/commands/diagnose.js';
217
-
218
- const SUCCEEDED_RESULT = {
219
- status: 'succeeded' as const,
220
- taskId: 'test-task-1',
221
- output: {
222
- valid: true,
223
- diagnosisId: 'diag-123',
224
- taskId: 'test-task-1',
225
- summary: 'Test diagnosis summary',
226
- rootCause: 'Test: test root cause',
227
- violatedPrinciples: [],
228
- evidence: [],
229
- recommendations: [
230
- { kind: 'principle', description: 'Always validate tool arguments before execution' },
231
- ],
232
- confidence: 0.9,
233
- },
234
- attemptCount: 1,
235
- };
236
-
237
- // ERR-067: reset the mocked run() to a clean successful default after each test
238
- // so later tests don't see leftover mockResolvedValueOnce() chains or call counts.
239
- const DEFAULT_SUCCEEDED_RUN_RESULT = {
240
- status: 'succeeded' as const,
241
- taskId: 'test-task-1',
242
- output: SUCCEEDED_RESULT.output,
243
- };
244
-
245
-
246
-
247
- describe('pd diagnose run --runtime routing', () => {
248
- let mockResolveRuntimeConfig: ReturnType<typeof vi.fn>;
249
- let mockIsRuntimeConfigError: ReturnType<typeof vi.fn>;
250
-
251
- beforeEach(async () => {
252
- vi.clearAllMocks();
253
- const runtimeV2 = await import('@principles/core/runtime-v2');
254
- mockResolveRuntimeConfig = vi.mocked(runtimeV2.resolveRuntimeConfig);
255
- mockIsRuntimeConfigError = vi.mocked(runtimeV2.isRuntimeConfigError);
256
- const { run } = runtimeV2;
257
- vi.mocked(run).mockResolvedValue(SUCCEEDED_RESULT);
258
- mockGetCandidatesByTaskId.mockResolvedValue([]);
259
- mockUpdateCandidateStatus.mockResolvedValue(undefined);
260
- mockIntake.mockReset();
261
- });
262
-
263
- it('CLI-01: --runtime test-double routes to TestDoubleRuntimeAdapter (regression)', async () => {
264
- const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
265
- const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => undefined) as () => never);
266
-
267
- await handleDiagnoseRun({
268
- taskId: 'test-task-1',
269
- workspace: '/tmp/fake-workspace',
270
- runtime: 'test-double',
271
- json: false,
272
- } as DiagnoseRunOptions);
273
-
274
- expect(consoleSpy).toHaveBeenCalled();
275
- expect(exitSpy).not.toHaveBeenCalledWith(1);
276
-
277
- consoleSpy.mockRestore();
278
- exitSpy.mockRestore();
279
- });
280
-
281
- it('HG-03: --runtime openclaw-cli without mode (no file config) fails via resolveRuntimeFromPdConfig', async () => {
282
- mockResolveRuntimeFromPdConfig.mockReturnValueOnce({
283
- result: {
284
- ok: false,
285
- reason: 'missing_openclaw_mode',
286
- message: 'runtimeKind is openclaw-cli but no mode specified',
287
- nextAction: 'Provide exactly one mode',
288
- },
289
- legacyWarnings: [],
290
- configSource: '.pd/config.yaml',
291
- configLoadResult: { ok: true, effective: {}, defaults: {}, legacyFilesDetected: [] },
292
- });
293
-
294
- const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
295
- const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => undefined) as () => never);
296
-
297
- await handleDiagnoseRun({
298
- taskId: 'test-task-1',
299
- workspace: '/tmp/fake-workspace',
300
- runtime: 'openclaw-cli',
301
- json: false,
302
- } as DiagnoseRunOptions);
303
-
304
- expect(consoleErrorSpy).toHaveBeenCalledWith(expect.stringContaining('no mode resolved'));
305
- expect(exitSpy).toHaveBeenCalledWith(1);
306
-
307
- consoleErrorSpy.mockRestore();
308
- exitSpy.mockRestore();
309
- });
310
-
311
- it('HG-03: both --openclaw-local and --openclaw-gateway exits with error (mutually exclusive)', async () => {
312
- const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
313
- const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => undefined) as () => never);
314
-
315
- await handleDiagnoseRun({
316
- taskId: 'test-task-1',
317
- workspace: '/tmp/fake-workspace',
318
- runtime: 'openclaw-cli',
319
- openclawLocal: true,
320
- openclawGateway: true,
321
- json: false,
322
- } as DiagnoseRunOptions);
323
-
324
- expect(consoleErrorSpy).toHaveBeenCalledWith(
325
- 'error: --openclaw-local and --openclaw-gateway are mutually exclusive'
326
- );
327
- expect(exitSpy).toHaveBeenCalledWith(1);
328
-
329
- consoleErrorSpy.mockRestore();
330
- exitSpy.mockRestore();
331
- });
332
-
333
- it('DPB-09: openclaw-cli with file config openclawMode succeeds without CLI flag', async () => {
334
- mockResolveRuntimeFromPdConfig.mockReturnValueOnce({
335
- result: {
336
- runtimeKind: 'openclaw-cli',
337
- openclawMode: 'local',
338
- timeoutMs: 300000,
339
- agentId: 'main',
340
- },
341
- legacyWarnings: [],
342
- configSource: '.pd/config.yaml',
343
- configLoadResult: { ok: true, effective: {}, defaults: {}, legacyFilesDetected: [] },
344
- });
345
-
346
- const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
347
- const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => undefined) as () => never);
348
-
349
- await handleDiagnoseRun({
350
- taskId: 'test-task-1',
351
- workspace: '/tmp/fake-workspace',
352
- runtime: 'openclaw-cli',
353
- json: false,
354
- } as DiagnoseRunOptions);
355
-
356
- expect(exitSpy).not.toHaveBeenCalledWith(1);
357
-
358
- consoleSpy.mockRestore();
359
- exitSpy.mockRestore();
360
- });
361
-
362
- it('DPB-09: openclaw-cli flag overrides file config mode (config=gateway, flag=local → runtimeMode=local)', async () => {
363
- mockResolveRuntimeFromPdConfig.mockReturnValueOnce({
364
- result: {
365
- runtimeKind: 'openclaw-cli',
366
- openclawMode: 'gateway',
367
- timeoutMs: 300000,
368
- agentId: 'main',
369
- },
370
- legacyWarnings: [],
371
- configSource: '.pd/config.yaml',
372
- configLoadResult: { ok: true, effective: {}, defaults: {}, legacyFilesDetected: [] },
373
- });
374
-
375
- const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
376
- const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => undefined) as () => never);
377
-
378
- await handleDiagnoseRun({
379
- taskId: 'test-task-1',
380
- workspace: '/tmp/fake-workspace',
381
- runtime: 'openclaw-cli',
382
- openclawLocal: true,
383
- json: false,
384
- } as DiagnoseRunOptions);
385
-
386
- // Flag override: config says gateway, flag says local → adapter gets local
387
- const OpenClawCliMock = vi.mocked(
388
- await import('@principles/core/runtime-v2').then(m => m.OpenClawCliRuntimeAdapter),
389
- );
390
- expect(OpenClawCliMock).toHaveBeenCalledWith(
391
- expect.objectContaining({ runtimeMode: 'local' }),
392
- );
393
- expect(exitSpy).not.toHaveBeenCalledWith(1);
394
-
395
- consoleSpy.mockRestore();
396
- exitSpy.mockRestore();
397
- });
398
-
399
- it('DPB-09: openclaw-cli missing mode (--json) outputs JSON error', async () => {
400
- mockResolveRuntimeFromPdConfig.mockReturnValueOnce({
401
- result: {
402
- runtimeKind: 'openclaw-cli',
403
- openclawMode: undefined,
404
- timeoutMs: 300000,
405
- agentId: 'main',
406
- },
407
- legacyWarnings: [],
408
- configSource: '.pd/config.yaml',
409
- configLoadResult: { ok: true, effective: {}, defaults: {}, legacyFilesDetected: [] },
410
- });
411
-
412
- const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
413
- const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => undefined) as () => never);
414
-
415
- await handleDiagnoseRun({
416
- taskId: 'test-task-1',
417
- workspace: '/tmp/fake-workspace',
418
- runtime: 'openclaw-cli',
419
- json: true,
420
- } as DiagnoseRunOptions);
421
-
422
- const jsonOutput = JSON.parse(logSpy.mock.calls[0][0]);
423
- expect(jsonOutput.ok).toBe(false);
424
- expect(jsonOutput.reason).toBe('missing_openclaw_mode');
425
- expect(exitSpy).toHaveBeenCalledWith(1);
426
-
427
- logSpy.mockRestore();
428
- exitSpy.mockRestore();
429
- });
430
-
431
- it('CLI-04: unknown runtime kind exits with error and exit code 1', async () => {
432
- const consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
433
- const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => undefined) as () => never);
434
-
435
- await handleDiagnoseRun({
436
- taskId: 'test-task-1',
437
- workspace: '/tmp/fake-workspace',
438
- runtime: 'invalid-runtime',
439
- json: true,
440
- } as DiagnoseRunOptions);
441
-
442
- expect(consoleLogSpy).toHaveBeenCalledTimes(1);
443
- const jsonOutput = JSON.parse(consoleLogSpy.mock.calls[0][0] as string);
444
- expect(jsonOutput.ok).toBe(false);
445
- expect(jsonOutput.reason).toBe('unsupported_runtime_kind: invalid-runtime');
446
- expect(jsonOutput.nextAction).toContain('openclaw-cli');
447
- expect(exitSpy).toHaveBeenCalledWith(1);
448
-
449
- consoleLogSpy.mockRestore();
450
- exitSpy.mockRestore();
451
- });
452
-
453
- it('P1: missing --runtime with no config binding refuses with missing_runtime (no test-double default)', async () => {
454
- // Regression for the silent test-double default bug. Without --runtime and
455
- // with no usable .pd/config.yaml binding, the command must refuse loudly
456
- // (rc-9-no-silent-fallback) instead of defaulting to test-double and
457
- // producing fake/failed diagnostic data in a real workspace.
458
- mockResolveRuntimeFromPdConfig.mockReturnValueOnce({
459
- result: { ok: false, reason: 'no_runtime_binding', message: 'no binding', nextAction: 'set runtime' },
460
- legacyWarnings: [],
461
- configSource: '.pd/config.yaml',
462
- configLoadResult: { ok: true, effective: {}, defaults: {}, legacyFilesDetected: [] } as never,
463
- });
464
- mockIsRuntimeConfigError.mockReturnValueOnce(true);
465
-
466
- const consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
467
- const consoleErrSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
468
- const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => undefined) as () => never);
469
-
470
- await handleDiagnoseRun({
471
- taskId: 'test-task-1',
472
- workspace: '/tmp/fake-workspace',
473
- // runtime intentionally omitted
474
- json: true,
475
- } as DiagnoseRunOptions);
476
-
477
- // Config was consulted (not silently skipped).
478
- expect(mockResolveRuntimeFromPdConfig).toHaveBeenCalledWith('/tmp/fake-workspace');
479
- expect(exitSpy).toHaveBeenCalledWith(1);
480
- const jsonOutput = JSON.parse(consoleLogSpy.mock.calls[0][0] as string);
481
- expect(jsonOutput.ok).toBe(false);
482
- expect(jsonOutput.reason).toBe('missing_runtime');
483
- expect(jsonOutput.nextAction).toContain('--runtime');
484
-
485
- consoleLogSpy.mockRestore();
486
- consoleErrSpy.mockRestore();
487
- exitSpy.mockRestore();
488
- });
489
-
490
- it('P1: missing --runtime resolves kind from .pd/config.yaml (honors config, no test-double default)', async () => {
491
- // When --runtime is absent but .pd/config.yaml binds openclaw-cli, the
492
- // resolved kind must come from config — proving test-double is no longer
493
- // the default. Mirrors DPB-09 but omits --runtime to exercise the new
494
- // config-resolution path.
495
- mockResolveRuntimeFromPdConfig.mockReturnValueOnce({
496
- result: {
497
- runtimeKind: 'openclaw-cli',
498
- openclawMode: 'gateway',
499
- timeoutMs: 300000,
500
- agentId: 'main',
501
- },
502
- legacyWarnings: [],
503
- configSource: '.pd/config.yaml',
504
- configLoadResult: { ok: true, effective: {}, defaults: {}, legacyFilesDetected: [] } as never,
505
- });
506
-
507
- const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
508
- const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => undefined) as () => never);
509
-
510
- await handleDiagnoseRun({
511
- taskId: 'test-task-1',
512
- workspace: '/tmp/fake-workspace',
513
- // runtime intentionally omitted — must be resolved from config
514
- openclawGateway: true,
515
- json: false,
516
- } as DiagnoseRunOptions);
517
-
518
- expect(mockResolveRuntimeFromPdConfig).toHaveBeenCalledWith('/tmp/fake-workspace');
519
- const OpenClawCliMock = vi.mocked(
520
- await import('@principles/core/runtime-v2').then(m => m.OpenClawCliRuntimeAdapter),
521
- );
522
- expect(OpenClawCliMock).toHaveBeenCalledWith(
523
- expect.objectContaining({ runtimeMode: 'gateway' }),
524
- );
525
- expect(exitSpy).not.toHaveBeenCalledWith(1);
526
-
527
- consoleSpy.mockRestore();
528
- exitSpy.mockRestore();
529
- });
530
-
531
- it('DPB-09: openclaw-cli --openclaw-gateway constructs adapter with runtimeMode=gateway', async () => {
532
- mockResolveRuntimeFromPdConfig.mockReturnValueOnce({
533
- result: {
534
- runtimeKind: 'openclaw-cli',
535
- openclawMode: 'gateway',
536
- timeoutMs: 300000,
537
- agentId: 'main',
538
- },
539
- legacyWarnings: [],
540
- configSource: '.pd/config.yaml',
541
- configLoadResult: { ok: true, effective: {}, defaults: {}, legacyFilesDetected: [] },
542
- });
543
-
544
- const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
545
- const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => undefined) as () => never);
546
-
547
- await handleDiagnoseRun({
548
- taskId: 'test-task-1',
549
- workspace: '/tmp/fake-workspace',
550
- runtime: 'openclaw-cli',
551
- openclawGateway: true,
552
- json: false,
553
- } as DiagnoseRunOptions);
554
-
555
- const OpenClawCliMock = vi.mocked(
556
- await import('@principles/core/runtime-v2').then(m => m.OpenClawCliRuntimeAdapter),
557
- );
558
- expect(OpenClawCliMock).toHaveBeenCalledWith(
559
- expect.objectContaining({ runtimeMode: 'gateway' }),
560
- );
561
- expect(exitSpy).not.toHaveBeenCalledWith(1);
562
-
563
- consoleSpy.mockRestore();
564
- exitSpy.mockRestore();
565
- });
566
-
567
- it('DPB-09: openclaw-cli --openclaw-local constructs adapter with runtimeMode=local', async () => {
568
- mockResolveRuntimeFromPdConfig.mockReturnValueOnce({
569
- result: {
570
- runtimeKind: 'openclaw-cli',
571
- openclawMode: 'local',
572
- timeoutMs: 300000,
573
- agentId: 'main',
574
- },
575
- legacyWarnings: [],
576
- configSource: '.pd/config.yaml',
577
- configLoadResult: { ok: true, effective: {}, defaults: {}, legacyFilesDetected: [] },
578
- });
579
-
580
- const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
581
- const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => undefined) as () => never);
582
-
583
- await handleDiagnoseRun({
584
- taskId: 'test-task-1',
585
- workspace: '/tmp/fake-workspace',
586
- runtime: 'openclaw-cli',
587
- openclawLocal: true,
588
- json: false,
589
- } as DiagnoseRunOptions);
590
-
591
- const OpenClawCliMock = vi.mocked(
592
- await import('@principles/core/runtime-v2').then(m => m.OpenClawCliRuntimeAdapter),
593
- );
594
- expect(OpenClawCliMock).toHaveBeenCalledWith(
595
- expect.objectContaining({ runtimeMode: 'local' }),
596
- );
597
- expect(exitSpy).not.toHaveBeenCalledWith(1);
598
-
599
- consoleSpy.mockRestore();
600
- exitSpy.mockRestore();
601
- });
602
- });
603
-
604
- describe('pd diagnose run — auto-intake after success', () => {
605
- beforeEach(async () => {
606
- vi.clearAllMocks();
607
- const { run } = await import('@principles/core/runtime-v2');
608
- vi.mocked(run).mockResolvedValue(SUCCEEDED_RESULT);
609
- mockGetCandidatesByTaskId.mockResolvedValue([]);
610
- mockUpdateCandidateStatus.mockResolvedValue(undefined);
611
- mockIntake.mockReset();
612
- });
613
-
614
- it('INTAKE-01: successful diagnose + intake — candidates consumed, JSON includes intake evidence', async () => {
615
- const candidates = [
616
- { candidateId: 'cand-1', artifactId: 'art-1', taskId: 'test-task-1', status: 'pending' },
617
- { candidateId: 'cand-2', artifactId: 'art-2', taskId: 'test-task-1', status: 'pending' },
618
- ];
619
- mockGetCandidatesByTaskId.mockResolvedValue(candidates);
620
- mockIntake
621
- .mockResolvedValueOnce({ id: 'ledger-1', title: 'Principle 1', status: 'probation' })
622
- .mockResolvedValueOnce({ id: 'ledger-2', title: 'Principle 2', status: 'probation' });
623
-
624
- const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
625
- const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => undefined) as () => never);
626
-
627
- await handleDiagnoseRun({
628
- taskId: 'test-task-1',
629
- workspace: '/tmp/fake-workspace',
630
- runtime: 'test-double',
631
- json: true,
632
- } as DiagnoseRunOptions);
633
-
634
- expect(mockIntake).toHaveBeenCalledTimes(2);
635
- expect(mockIntake).toHaveBeenCalledWith('cand-1');
636
- expect(mockIntake).toHaveBeenCalledWith('cand-2');
637
- expect(mockUpdateCandidateStatus).toHaveBeenCalledTimes(2);
638
- expect(mockUpdateCandidateStatus).toHaveBeenCalledWith('cand-1', { status: 'consumed' });
639
- expect(mockUpdateCandidateStatus).toHaveBeenCalledWith('cand-2', { status: 'consumed' });
640
-
641
- const jsonOutput = consoleSpy.mock.calls.find(call => {
642
- try {
643
- const parsed = JSON.parse(call[0] as string);
644
- return parsed.intake && Array.isArray(parsed.intake.candidates);
645
- } catch { return false; }
646
- });
647
- expect(jsonOutput).toBeDefined();
648
- const parsed = JSON.parse((jsonOutput as [string])[0]);
649
- expect(parsed.intake.enabled).toBe(true);
650
- expect(parsed.intake.candidates).toHaveLength(2);
651
- expect(parsed.intake.candidates[0].candidateId).toBe('cand-1');
652
- expect(parsed.intake.candidates[0].status).toBe('consumed');
653
- expect(parsed.intake.candidates[0].ledgerEntryId).toBe('ledger-1');
654
- expect(parsed.intake.candidates[1].candidateId).toBe('cand-2');
655
- expect(parsed.intake.candidates[1].status).toBe('consumed');
656
- expect(parsed.intake.candidates[1].ledgerEntryId).toBe('ledger-2');
657
- expect(exitSpy).not.toHaveBeenCalledWith(1);
658
-
659
- consoleSpy.mockRestore();
660
- exitSpy.mockRestore();
661
- });
662
-
663
- it('INTAKE-02: successful diagnose + intake failure — exits non-zero with nextAction', async () => {
664
- const candidates = [
665
- { candidateId: 'cand-ok', artifactId: 'art-1', taskId: 'test-task-1', status: 'pending' },
666
- { candidateId: 'cand-fail', artifactId: 'art-2', taskId: 'test-task-1', status: 'pending' },
667
- ];
668
- mockGetCandidatesByTaskId.mockResolvedValue(candidates);
669
- mockIntake
670
- .mockResolvedValueOnce({ id: 'ledger-ok', title: 'OK', status: 'probation' })
671
- .mockImplementationOnce(() => { throw new Error('Ledger write failed'); });
672
-
673
- const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
674
- const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => undefined) as () => never);
675
-
676
- await handleDiagnoseRun({
677
- taskId: 'test-task-1',
678
- workspace: '/tmp/fake-workspace',
679
- runtime: 'test-double',
680
- json: true,
681
- } as DiagnoseRunOptions);
682
-
683
- const jsonOutput = consoleSpy.mock.calls.find(call => {
684
- try {
685
- const parsed = JSON.parse(call[0] as string);
686
- return parsed.intake && Array.isArray(parsed.intake.candidates);
687
- } catch { return false; }
688
- });
689
- expect(jsonOutput).toBeDefined();
690
- const parsed = JSON.parse((jsonOutput as [string])[0]);
691
- expect(parsed.intake.candidates).toHaveLength(2);
692
- expect(parsed.intake.candidates[0].status).toBe('consumed');
693
- expect(parsed.intake.candidates[1].status).toBe('intake_failed');
694
- expect(parsed.intake.candidates[1].error).toContain('Ledger write failed');
695
- expect(parsed.intake.candidates[1].nextAction).toContain('pd candidate intake --candidate-id cand-fail');
696
- expect(exitSpy).toHaveBeenCalledWith(1);
697
-
698
- consoleSpy.mockRestore();
699
- exitSpy.mockRestore();
700
- });
701
-
702
- it('INTAKE-03: --no-intake skips intake, candidates remain pending with advisory', async () => {
703
- const candidates = [
704
- { candidateId: 'cand-1', artifactId: 'art-1', taskId: 'test-task-1', status: 'pending' },
705
- ];
706
- mockGetCandidatesByTaskId.mockResolvedValue(candidates);
707
-
708
- const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
709
- const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => undefined) as () => never);
710
-
711
- await handleDiagnoseRun({
712
- taskId: 'test-task-1',
713
- workspace: '/tmp/fake-workspace',
714
- runtime: 'test-double',
715
- json: true,
716
- intake: false,
717
- } as DiagnoseRunOptions);
718
-
719
- expect(mockIntake).not.toHaveBeenCalled();
720
- expect(mockUpdateCandidateStatus).not.toHaveBeenCalled();
721
-
722
- const jsonOutput = consoleSpy.mock.calls.find(call => {
723
- try {
724
- const parsed = JSON.parse(call[0] as string);
725
- return parsed.intake && Array.isArray(parsed.intake.candidates);
726
- } catch { return false; }
727
- });
728
- expect(jsonOutput).toBeDefined();
729
- const parsed = JSON.parse((jsonOutput as [string])[0]);
730
- expect(parsed.intake.enabled).toBe(false);
731
- expect(parsed.intake.candidates).toHaveLength(1);
732
- expect(parsed.intake.candidates[0].candidateId).toBe('cand-1');
733
- expect(parsed.intake.candidates[0].status).toBe('skipped');
734
- expect(exitSpy).not.toHaveBeenCalledWith(1);
735
-
736
- consoleSpy.mockRestore();
737
- exitSpy.mockRestore();
738
- });
739
-
740
- it('INTAKE-04: --no-intake human-readable output shows advisory with manual intake commands', async () => {
741
- const candidates = [
742
- { candidateId: 'cand-1', artifactId: 'art-1', taskId: 'test-task-1', status: 'pending' },
743
- ];
744
- mockGetCandidatesByTaskId.mockResolvedValue(candidates);
745
-
746
- const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
747
- const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => undefined) as () => never);
748
-
749
- await handleDiagnoseRun({
750
- taskId: 'test-task-1',
751
- workspace: '/tmp/fake-workspace',
752
- runtime: 'test-double',
753
- json: false,
754
- intake: false,
755
- } as DiagnoseRunOptions);
756
-
757
- const allOutput = consoleSpy.mock.calls.map(call => call[0]).join('\n');
758
- expect(allOutput).toContain('--no-intake');
759
- expect(allOutput).toContain('pd candidate intake --candidate-id cand-1');
760
- expect(exitSpy).not.toHaveBeenCalledWith(1);
761
-
762
- consoleSpy.mockRestore();
763
- exitSpy.mockRestore();
764
- });
765
-
766
- it('INTAKE-05: intake failure human-readable output shows nextAction', async () => {
767
- const candidates = [
768
- { candidateId: 'cand-fail', artifactId: 'art-1', taskId: 'test-task-1', status: 'pending' },
769
- ];
770
- mockGetCandidatesByTaskId.mockResolvedValue(candidates);
771
- mockIntake.mockImplementation(() => { throw new Error('Ledger write failed'); });
772
-
773
- const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
774
- const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => undefined) as () => never);
775
-
776
- await handleDiagnoseRun({
777
- taskId: 'test-task-1',
778
- workspace: '/tmp/fake-workspace',
779
- runtime: 'test-double',
780
- json: false,
781
- } as DiagnoseRunOptions);
782
-
783
- const allOutput = consoleSpy.mock.calls.map(call => call[0]).join('\n');
784
- expect(allOutput).toContain('INTAKE FAILED');
785
- expect(allOutput).toContain('Next action: pd candidate intake --candidate-id cand-fail');
786
- expect(exitSpy).toHaveBeenCalledWith(1);
787
-
788
- consoleSpy.mockRestore();
789
- exitSpy.mockRestore();
790
- });
791
-
792
- it('INTAKE-06: does not bypass ledger — consumed only set after intake succeeds', async () => {
793
- const candidates = [
794
- { candidateId: 'cand-1', artifactId: 'art-1', taskId: 'test-task-1', status: 'pending' },
795
- ];
796
- mockGetCandidatesByTaskId.mockResolvedValue(candidates);
797
- mockIntake.mockImplementation(() => { throw new Error('Ledger write failed'); });
798
-
799
- const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
800
- const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => undefined) as () => never);
801
-
802
- await handleDiagnoseRun({
803
- taskId: 'test-task-1',
804
- workspace: '/tmp/fake-workspace',
805
- runtime: 'test-double',
806
- json: true,
807
- } as DiagnoseRunOptions);
808
-
809
- expect(mockIntake).toHaveBeenCalledWith('cand-1');
810
- expect(mockUpdateCandidateStatus).not.toHaveBeenCalled();
811
-
812
- consoleSpy.mockRestore();
813
- exitSpy.mockRestore();
814
- });
815
-
816
- it('INTAKE-07: already-consumed candidate — intake called but updateCandidateStatus skipped', async () => {
817
- const candidates = [
818
- { candidateId: 'cand-consumed', artifactId: 'art-1', taskId: 'test-task-1', status: 'consumed' },
819
- ];
820
- mockGetCandidatesByTaskId.mockResolvedValue(candidates);
821
- mockIntake.mockResolvedValue({ id: 'ledger-existing', title: 'Existing', status: 'probation' });
822
-
823
- const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
824
- const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => undefined) as () => never);
825
-
826
- await handleDiagnoseRun({
827
- taskId: 'test-task-1',
828
- workspace: '/tmp/fake-workspace',
829
- runtime: 'test-double',
830
- json: true,
831
- } as DiagnoseRunOptions);
832
-
833
- expect(mockIntake).toHaveBeenCalledWith('cand-consumed');
834
- expect(mockUpdateCandidateStatus).not.toHaveBeenCalled();
835
-
836
- const jsonOutput = consoleSpy.mock.calls.find(call => {
837
- try {
838
- const parsed = JSON.parse(call[0] as string);
839
- return parsed.intake && Array.isArray(parsed.intake.candidates);
840
- } catch { return false; }
841
- });
842
- const parsed = JSON.parse((jsonOutput as [string])[0]);
843
- expect(parsed.intake.candidates[0].status).toBe('consumed');
844
- expect(exitSpy).not.toHaveBeenCalledWith(1);
845
-
846
- consoleSpy.mockRestore();
847
- exitSpy.mockRestore();
848
- });
849
-
850
- it('INTAKE-08: no candidates produced — intake not called, output shows empty intake', async () => {
851
- mockGetCandidatesByTaskId.mockResolvedValue([]);
852
-
853
- const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
854
- const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => undefined) as () => never);
855
-
856
- await handleDiagnoseRun({
857
- taskId: 'test-task-1',
858
- workspace: '/tmp/fake-workspace',
859
- runtime: 'test-double',
860
- json: true,
861
- } as DiagnoseRunOptions);
862
-
863
- expect(mockIntake).not.toHaveBeenCalled();
864
- expect(mockUpdateCandidateStatus).not.toHaveBeenCalled();
865
-
866
- const jsonOutput = consoleSpy.mock.calls.find(call => {
867
- try {
868
- const parsed = JSON.parse(call[0] as string);
869
- return parsed.intake;
870
- } catch { return false; }
871
- });
872
- const parsed = JSON.parse((jsonOutput as [string])[0]);
873
- expect(parsed.intake.candidates).toHaveLength(0);
874
- expect(parsed.intake.enabled).toBe(true);
875
- expect(exitSpy).not.toHaveBeenCalledWith(1);
876
-
877
- consoleSpy.mockRestore();
878
- exitSpy.mockRestore();
879
- });
880
-
881
- it('INTAKE-09: successful diagnose + intake — human-readable output shows consumed candidates', async () => {
882
- const candidates = [
883
- { candidateId: 'cand-1', artifactId: 'art-1', taskId: 'test-task-1', status: 'pending' },
884
- ];
885
- mockGetCandidatesByTaskId.mockResolvedValue(candidates);
886
- mockIntake.mockResolvedValue({ id: 'ledger-1', title: 'Principle 1', status: 'probation' });
887
-
888
- const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
889
- const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => undefined) as () => never);
890
-
891
- await handleDiagnoseRun({
892
- taskId: 'test-task-1',
893
- workspace: '/tmp/fake-workspace',
894
- runtime: 'test-double',
895
- json: false,
896
- } as DiagnoseRunOptions);
897
-
898
- const allOutput = consoleSpy.mock.calls.map(call => call[0]).join('\n');
899
- expect(allOutput).toContain('Candidate Intake');
900
- expect(allOutput).toContain('cand-1: consumed');
901
- expect(allOutput).toContain('ledger-1');
902
- expect(exitSpy).not.toHaveBeenCalledWith(1);
903
-
904
- consoleSpy.mockRestore();
905
- exitSpy.mockRestore();
906
- });
907
-
908
- it('INTAKE-10: --json mode outputs exactly one console.log with parseable JSON (no text header leak)', async () => {
909
- mockGetCandidatesByTaskId.mockResolvedValue([]);
910
-
911
- const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
912
- const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => undefined) as () => never);
913
-
914
- await handleDiagnoseRun({
915
- taskId: 'test-task-1',
916
- workspace: '/tmp/fake-workspace',
917
- runtime: 'test-double',
918
- json: true,
919
- } as DiagnoseRunOptions);
920
-
921
- expect(consoleSpy).toHaveBeenCalledTimes(1);
922
- const rawOutput = consoleSpy.mock.calls[0][0] as string;
923
- const parsed = JSON.parse(rawOutput);
924
- expect(parsed.status).toBe('succeeded');
925
- expect(parsed.intake).toBeDefined();
926
- expect(exitSpy).not.toHaveBeenCalledWith(1);
927
-
928
- consoleSpy.mockRestore();
929
- exitSpy.mockRestore();
930
- });
931
-
932
- it('INTAKE-11: --json intake failure nextAction contains executable command with workspace', async () => {
933
- const candidates = [
934
- { candidateId: 'cand-err', artifactId: 'art-1', taskId: 'test-task-1', status: 'pending' },
935
- ];
936
- mockGetCandidatesByTaskId.mockResolvedValue(candidates);
937
- mockIntake.mockImplementation(() => { throw new Error('DB locked'); });
938
-
939
- const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
940
- const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => undefined) as () => never);
941
-
942
- await handleDiagnoseRun({
943
- taskId: 'test-task-1',
944
- workspace: '/tmp/fake-workspace',
945
- runtime: 'test-double',
946
- json: true,
947
- } as DiagnoseRunOptions);
948
-
949
- const rawOutput = consoleSpy.mock.calls[0][0] as string;
950
- const parsed = JSON.parse(rawOutput);
951
- const failed = parsed.intake.candidates[0];
952
- expect(failed.status).toBe('intake_failed');
953
- expect(failed.nextAction).toMatch(/^pd candidate intake --candidate-id cand-err --workspace "/);
954
- expect(failed.nextAction).toContain('/tmp/fake-workspace');
955
- expect(exitSpy).toHaveBeenCalledWith(1);
956
-
957
- consoleSpy.mockRestore();
958
- exitSpy.mockRestore();
959
- });
960
-
961
- it('INTAKE-12: failed diagnosis does not trigger intake (process.exit stubbed)', async () => {
962
- const { run } = await import('@principles/core/runtime-v2');
963
- vi.mocked(run).mockResolvedValueOnce({
964
- status: 'failed',
965
- taskId: 'test-task-1',
966
- errorCategory: 'timeout',
967
- failureReason: 'LLM call timed out',
968
- attemptCount: 3,
969
- });
970
-
971
- const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
972
- const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => undefined) as () => never);
973
-
974
- await handleDiagnoseRun({
975
- taskId: 'test-task-1',
976
- workspace: '/tmp/fake-workspace',
977
- runtime: 'test-double',
978
- json: true,
979
- } as DiagnoseRunOptions);
980
-
981
- expect(mockGetCandidatesByTaskId).not.toHaveBeenCalled();
982
- expect(mockIntake).not.toHaveBeenCalled();
983
- expect(mockUpdateCandidateStatus).not.toHaveBeenCalled();
984
- expect(exitSpy).toHaveBeenCalledWith(1);
985
-
986
- consoleSpy.mockRestore();
987
- exitSpy.mockRestore();
988
- });
989
- });
990
-
991
- describe('Commander wiring for --no-intake', () => {
992
- function createDiagnoseProgram(): { program: Command; capturedOpts: Record<string, unknown> } {
993
- const program = new Command();
994
- program.exitOverride();
995
- const capturedOpts: Record<string, unknown> = {};
996
-
997
- program
998
- .command('diagnose')
999
- .command('run')
1000
- .option('--no-intake', 'Skip candidate intake after successful diagnosis')
1001
- .option('--json', 'Output raw JSON')
1002
- .action(async (opts) => {
1003
- Object.assign(capturedOpts, opts);
1004
- });
1005
-
1006
- return { program, capturedOpts };
1007
- }
1008
-
1009
- it('CMD-01: --no-intake accepted, sets opts.intake === false', async () => {
1010
- const { program, capturedOpts } = createDiagnoseProgram();
1011
- await program.parseAsync(['node', 'pd', 'diagnose', 'run', '--no-intake']);
1012
- expect(capturedOpts.intake).toBe(false);
1013
- });
1014
-
1015
- it('CMD-02: default (no flag) → opts.intake === true', async () => {
1016
- const { program, capturedOpts } = createDiagnoseProgram();
1017
- await program.parseAsync(['node', 'pd', 'diagnose', 'run']);
1018
- expect(capturedOpts.intake).toBe(true);
1019
- });
1020
-
1021
- it('CMD-03: --intake is not a valid option (Commander rejects it)', async () => {
1022
- const { program } = createDiagnoseProgram();
1023
- await expect(
1024
- program.parseAsync(['node', 'pd', 'diagnose', 'run', '--intake'])
1025
- ).rejects.toThrow();
1026
- });
1027
- });
1028
-
1029
- describe('pd status stalled-threshold validation', () => {
1030
- it('accepts valid positive integers', async () => {
1031
- const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => undefined) as () => never);
1032
- const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
1033
-
1034
- const runtimeV2 = await import('@principles/core/runtime-v2');
1035
- vi.mocked(runtimeV2.status).mockResolvedValueOnce({
1036
- taskId: 'test-task-1',
1037
- status: 'pending',
1038
- attemptCount: 0,
1039
- maxAttempts: 3,
1040
- lastError: null,
1041
- commitId: null,
1042
- artifactId: null,
1043
- candidateCount: null,
1044
- });
1045
-
1046
- await handleDiagnoseStatus({
1047
- taskId: 'test-task-1',
1048
- stalledThreshold: '123',
1049
- });
1050
-
1051
- expect(exitSpy).not.toHaveBeenCalled();
1052
- exitSpy.mockRestore();
1053
- consoleSpy.mockRestore();
1054
- });
1055
-
1056
- it('rejects invalid inputs (0, negative, decimals, NaN, empty)', async () => {
1057
- const invalidInputs = ['0', '-10', '1.5', 'abc', 'NaN', ''];
1058
- for (const input of invalidInputs) {
1059
- const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => undefined) as () => never);
1060
- const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
1061
-
1062
- await handleDiagnoseStatus({
1063
- taskId: 'test-task-1',
1064
- stalledThreshold: input,
1065
- });
1066
-
1067
- expect(exitSpy).toHaveBeenCalledWith(1);
1068
- expect(consoleErrorSpy).toHaveBeenCalledWith(expect.stringContaining('positive integer'));
1069
-
1070
- exitSpy.mockRestore();
1071
- consoleErrorSpy.mockRestore();
1072
- }
1073
- });
1074
-
1075
- it('rejects invalid inputs in JSON mode', async () => {
1076
- const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => undefined) as () => never);
1077
- const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
1078
-
1079
- await handleDiagnoseStatus({
1080
- taskId: 'test-task-1',
1081
- stalledThreshold: '0',
1082
- json: true,
1083
- });
1084
-
1085
- expect(exitSpy).toHaveBeenCalledWith(1);
1086
- expect(consoleSpy).toHaveBeenCalled();
1087
- const output = JSON.parse((consoleSpy.mock.calls[0] as string[])[0]);
1088
- expect(output.ok).toBe(false);
1089
- expect(output.reason).toBe('invalid_stalled_threshold');
1090
- expect(output.nextAction).toContain('positive integer');
1091
-
1092
- exitSpy.mockRestore();
1093
- consoleSpy.mockRestore();
1094
- });
1095
- });
1096
-
1097
- // ERR-067: 1 initial run + maxRetryLoops(10) before the safety limit converts retried to failed.
1098
- const EXPECTED_MAX_RETRY_CALLS = 11;
1099
-
1100
- describe('ERR-067: pd diagnose run retry loop for retried status', () => {
1101
- beforeEach(async () => {
1102
- vi.clearAllMocks();
1103
- const { run } = await import('@principles/core/runtime-v2');
1104
- vi.mocked(run).mockResolvedValue(DEFAULT_SUCCEEDED_RUN_RESULT);
1105
- });
1106
-
1107
- afterEach(async () => {
1108
- const { run } = await import('@principles/core/runtime-v2');
1109
- vi.mocked(run).mockResolvedValue(DEFAULT_SUCCEEDED_RUN_RESULT);
1110
- });
1111
-
1112
- it('ERR-067-01: retried status triggers retry loop, succeeds on second attempt', async () => {
1113
- const { run } = await import('@principles/core/runtime-v2');
1114
- const runMock = vi.mocked(run);
1115
- runMock
1116
- .mockResolvedValueOnce({
1117
- status: 'retried' as const,
1118
- taskId: 'test-task-1',
1119
- attemptCount: 1,
1120
- })
1121
- .mockResolvedValueOnce(SUCCEEDED_RESULT);
1122
-
1123
- const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
1124
- const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => undefined) as () => never);
1125
-
1126
- await handleDiagnoseRun({
1127
- taskId: 'test-task-1',
1128
- workspace: '/tmp/fake-workspace',
1129
- runtime: 'test-double',
1130
- json: false,
1131
- } as DiagnoseRunOptions);
1132
-
1133
- expect(runMock).toHaveBeenCalledTimes(2);
1134
- expect(exitSpy).not.toHaveBeenCalledWith(1);
1135
-
1136
- consoleSpy.mockRestore();
1137
- exitSpy.mockRestore();
1138
- runMock.mockResolvedValue(DEFAULT_SUCCEEDED_RUN_RESULT);
1139
- });
1140
-
1141
- it('ERR-067-02: retried status loops until maxRetryLoops, then converts to failed (P0-1 fix)', async () => {
1142
- const { run } = await import('@principles/core/runtime-v2');
1143
- const runMock = vi.mocked(run);
1144
- runMock.mockResolvedValue({
1145
- status: 'retried' as const,
1146
- taskId: 'test-task-1',
1147
- attemptCount: 1,
1148
- });
1149
-
1150
- const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
1151
- const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => undefined) as () => never);
1152
-
1153
- await handleDiagnoseRun({
1154
- taskId: 'test-task-1',
1155
- workspace: '/tmp/fake-workspace',
1156
- runtime: 'test-double',
1157
- json: false,
1158
- } as DiagnoseRunOptions);
1159
-
1160
- expect(runMock).toHaveBeenCalledTimes(EXPECTED_MAX_RETRY_CALLS);
1161
- expect(exitSpy).toHaveBeenCalledWith(1);
1162
-
1163
- consoleSpy.mockRestore();
1164
- exitSpy.mockRestore();
1165
- runMock.mockResolvedValue(DEFAULT_SUCCEEDED_RUN_RESULT);
1166
- });
1167
-
1168
- it('ERR-067-03: retried with failureReason propagates to failed status (P0-1 fix)', async () => {
1169
- const { run } = await import('@principles/core/runtime-v2');
1170
- const runMock = vi.mocked(run);
1171
- runMock.mockResolvedValue({
1172
- status: 'retried' as const,
1173
- taskId: 'test-task-1',
1174
- attemptCount: 1,
1175
- failureReason: 'Schema validation failed',
1176
- });
1177
-
1178
- const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
1179
- const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => undefined) as () => never);
1180
-
1181
- await handleDiagnoseRun({
1182
- taskId: 'test-task-1',
1183
- workspace: '/tmp/fake-workspace',
1184
- runtime: 'test-double',
1185
- json: true,
1186
- } as DiagnoseRunOptions);
1187
-
1188
- expect(runMock).toHaveBeenCalledTimes(EXPECTED_MAX_RETRY_CALLS);
1189
-
1190
- const jsonOutput = consoleSpy.mock.calls.find(call => {
1191
- try {
1192
- const parsed = JSON.parse(call[0] as string);
1193
- return parsed.status === 'failed';
1194
- } catch { return false; }
1195
- });
1196
- expect(jsonOutput).toBeDefined();
1197
- const parsed = JSON.parse((jsonOutput as [string])[0]);
1198
- expect(parsed.status).toBe('failed');
1199
- expect(parsed.failureReason).toContain('Max retry loops');
1200
- expect(parsed.failureReason).toContain('Schema validation failed');
1201
-
1202
- consoleSpy.mockRestore();
1203
- exitSpy.mockRestore();
1204
- runMock.mockResolvedValue(DEFAULT_SUCCEEDED_RUN_RESULT);
1205
- });
1206
-
1207
- it('ERR-067-04: succeeded status does NOT trigger retry loop', async () => {
1208
- const { run } = await import('@principles/core/runtime-v2');
1209
- const runMock = vi.mocked(run);
1210
- runMock.mockResolvedValue(SUCCEEDED_RESULT);
1211
-
1212
- const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
1213
- const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => undefined) as () => never);
1214
-
1215
- await handleDiagnoseRun({
1216
- taskId: 'test-task-1',
1217
- workspace: '/tmp/fake-workspace',
1218
- runtime: 'test-double',
1219
- json: false,
1220
- } as DiagnoseRunOptions);
1221
-
1222
- expect(runMock).toHaveBeenCalledTimes(1);
1223
- expect(exitSpy).not.toHaveBeenCalledWith(1);
1224
-
1225
- consoleSpy.mockRestore();
1226
- exitSpy.mockRestore();
1227
- runMock.mockResolvedValue(DEFAULT_SUCCEEDED_RUN_RESULT);
1228
- });
1229
-
1230
- it('ERR-067-05: failed status does NOT trigger retry loop', async () => {
1231
- const { run } = await import('@principles/core/runtime-v2');
1232
- const runMock = vi.mocked(run);
1233
- runMock.mockResolvedValue({
1234
- status: 'failed' as const,
1235
- taskId: 'test-task-1',
1236
- attemptCount: 3,
1237
- failureReason: 'Max attempts exceeded',
1238
- });
1239
-
1240
- const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
1241
- const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => undefined) as () => never);
1242
-
1243
- await handleDiagnoseRun({
1244
- taskId: 'test-task-1',
1245
- workspace: '/tmp/fake-workspace',
1246
- runtime: 'test-double',
1247
- json: false,
1248
- } as DiagnoseRunOptions);
1249
-
1250
- expect(runMock).toHaveBeenCalledTimes(1);
1251
- expect(exitSpy).toHaveBeenCalledWith(1);
1252
-
1253
- consoleSpy.mockRestore();
1254
- exitSpy.mockRestore();
1255
- runMock.mockResolvedValue(DEFAULT_SUCCEEDED_RUN_RESULT);
1256
- });
1257
- });
1258
-
1259
- // Defect-004 Part 2: dreamer seed after intake
1260
- describe('Defect-004: pd diagnose run — dreamer seed after intake', () => {
1261
- beforeEach(async () => {
1262
- vi.clearAllMocks();
1263
- const { run } = await import('@principles/core/runtime-v2');
1264
- vi.mocked(run).mockResolvedValue(SUCCEEDED_RESULT);
1265
- mockGetCandidatesByTaskId.mockResolvedValue([]);
1266
- mockUpdateCandidateStatus.mockResolvedValue(undefined);
1267
- mockCreateTask.mockResolvedValue(undefined);
1268
- // For dreamer seed tests, getTask should return null (dreamer task doesn't exist yet)
1269
- // so createTask gets called. Individual tests can override this if needed.
1270
- mockGetTask.mockResolvedValue(null);
1271
- mockIntake.mockReset();
1272
- mockIntake.mockResolvedValue({ id: 'ledger-1', title: 'P1', status: 'probation' });
1273
- });
1274
-
1275
- it('DREAMER-01: principle candidate — dreamer task created via createTask', async () => {
1276
- const candidates = [
1277
- { candidateId: 'cand-1', artifactId: 'art-1', taskId: 'test-task-1', status: 'pending', recommendationKind: 'principle' },
1278
- ];
1279
- mockGetCandidatesByTaskId.mockResolvedValue(candidates);
1280
-
1281
- const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
1282
- const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => undefined) as () => never);
1283
-
1284
- await handleDiagnoseRun({
1285
- taskId: 'test-task-1',
1286
- workspace: '/tmp/fake-workspace',
1287
- runtime: 'test-double',
1288
- json: true,
1289
- } as DiagnoseRunOptions);
1290
-
1291
- // Verify createTask was called for the dreamer task
1292
- expect(mockCreateTask).toHaveBeenCalled();
1293
- const createCall = mockCreateTask.mock.calls[0]?.[0];
1294
- expect(createCall?.taskKind).toBe('dreamer');
1295
- expect(createCall?.taskId).toBe('dreamer-cand-1');
1296
- expect(createCall?.status).toBe('pending');
1297
-
1298
- consoleSpy.mockRestore();
1299
- exitSpy.mockRestore();
1300
- });
1301
-
1302
- it('DREAMER-02: defer candidate — no dreamer task created', async () => {
1303
- const candidates = [
1304
- { candidateId: 'cand-defer', artifactId: 'art-1', taskId: 'test-task-1', status: 'pending', recommendationKind: 'defer' },
1305
- ];
1306
- mockGetCandidatesByTaskId.mockResolvedValue(candidates);
1307
-
1308
- const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
1309
- const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => undefined) as () => never);
1310
-
1311
- await handleDiagnoseRun({
1312
- taskId: 'test-task-1',
1313
- workspace: '/tmp/fake-workspace',
1314
- runtime: 'test-double',
1315
- json: true,
1316
- } as DiagnoseRunOptions);
1317
-
1318
- // No dreamer task should be created for defer candidates
1319
- expect(mockCreateTask).not.toHaveBeenCalled();
1320
-
1321
- consoleSpy.mockRestore();
1322
- exitSpy.mockRestore();
1323
- });
1324
-
1325
- it('DREAMER-03: implementation candidate — no dreamer task created', async () => {
1326
- const candidates = [
1327
- { candidateId: 'cand-impl', artifactId: 'art-1', taskId: 'test-task-1', status: 'pending', recommendationKind: 'implementation' },
1328
- ];
1329
- mockGetCandidatesByTaskId.mockResolvedValue(candidates);
1330
-
1331
- const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
1332
- const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => undefined) as () => never);
1333
-
1334
- await handleDiagnoseRun({
1335
- taskId: 'test-task-1',
1336
- workspace: '/tmp/fake-workspace',
1337
- runtime: 'test-double',
1338
- json: true,
1339
- } as DiagnoseRunOptions);
1340
-
1341
- // No dreamer task should be created for implementation candidates
1342
- expect(mockCreateTask).not.toHaveBeenCalled();
1343
-
1344
- consoleSpy.mockRestore();
1345
- exitSpy.mockRestore();
1346
- });
1347
-
1348
- it('DREAMER-04: --no-intake skips dreamer seed', async () => {
1349
- const candidates = [
1350
- { candidateId: 'cand-1', artifactId: 'art-1', taskId: 'test-task-1', status: 'pending', recommendationKind: 'principle' },
1351
- ];
1352
- mockGetCandidatesByTaskId.mockResolvedValue(candidates);
1353
-
1354
- const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
1355
- const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => undefined) as () => never);
1356
-
1357
- await handleDiagnoseRun({
1358
- taskId: 'test-task-1',
1359
- workspace: '/tmp/fake-workspace',
1360
- runtime: 'test-double',
1361
- json: true,
1362
- intake: false,
1363
- } as DiagnoseRunOptions);
1364
-
1365
- // No dreamer task should be created when intake is disabled
1366
- expect(mockCreateTask).not.toHaveBeenCalled();
1367
-
1368
- consoleSpy.mockRestore();
1369
- exitSpy.mockRestore();
1370
- });
1371
-
1372
- it('DREAMER-05: dreamer seed failure surfaces in intake results but does not fail the command', async () => {
1373
- const candidates = [
1374
- { candidateId: 'cand-1', artifactId: 'art-1', taskId: 'test-task-1', status: 'pending', recommendationKind: 'principle' },
1375
- ];
1376
- mockGetCandidatesByTaskId.mockResolvedValue(candidates);
1377
- // Make createTask throw to simulate seed failure
1378
- mockCreateTask.mockRejectedValueOnce(new Error('DB write failed'));
1379
-
1380
- const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
1381
- const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => undefined) as () => never);
1382
-
1383
- await handleDiagnoseRun({
1384
- taskId: 'test-task-1',
1385
- workspace: '/tmp/fake-workspace',
1386
- runtime: 'test-double',
1387
- json: true,
1388
- } as DiagnoseRunOptions);
1389
-
1390
- // Command should still succeed (exit 0) — dreamer seed failure is best-effort
1391
- expect(exitSpy).not.toHaveBeenCalledWith(1);
1392
-
1393
- // JSON output should include dreamer_seed_failed status
1394
- const jsonOutput = consoleSpy.mock.calls.find(call => {
1395
- try {
1396
- const parsed = JSON.parse(call[0] as string);
1397
- return parsed.intake && Array.isArray(parsed.intake.candidates);
1398
- } catch { return false; }
1399
- });
1400
- expect(jsonOutput).toBeDefined();
1401
- const parsed = JSON.parse((jsonOutput as [string])[0]);
1402
- const seedResult = parsed.intake.candidates.find((c: { candidateId: string; status: string }) => c.candidateId === 'cand-1' && c.status === 'dreamer_seed_failed');
1403
- expect(seedResult).toBeDefined();
1404
- expect(seedResult.error).toContain('DB write failed');
1405
-
1406
- consoleSpy.mockRestore();
1407
- exitSpy.mockRestore();
1408
- });
1409
- });
1410
-
1411
- // DEFECT-005 (PRI-514): A single defer candidate's intake_failed must NOT poison
1412
- // the dreamer seed loop for other successfully-consumed candidates. Previously
1413
- // `intakeFailed` was a global flag gating the ENTIRE dreamer seed loop — one
1414
- // defer candidate (admission gate deferred) set it true, skipping dreamer seed
1415
- // for ALL candidates including ones that succeeded intake (EP-03 / ERR-089
1416
- // sibling-branch defect: one branch's failure punished all sibling branches).
1417
- describe('DEFECT-005 (PRI-514): defer intake_failed must not poison other candidates dreamer seed', () => {
1418
- beforeEach(async () => {
1419
- vi.clearAllMocks();
1420
- const { run, evaluateCandidateAdmissionFromRecord } = await import('@principles/core/runtime-v2');
1421
- vi.mocked(run).mockResolvedValue(SUCCEEDED_RESULT);
1422
- mockGetCandidatesByTaskId.mockResolvedValue([]);
1423
- mockUpdateCandidateStatus.mockResolvedValue(undefined);
1424
- mockCreateTask.mockResolvedValue(undefined);
1425
- mockGetTask.mockResolvedValue(null);
1426
- mockIntake.mockReset();
1427
- mockIntake.mockResolvedValue({ id: 'ledger-1', title: 'P1', status: 'probation' });
1428
- // Default: admit all candidates. Individual tests override for defer.
1429
- vi.mocked(evaluateCandidateAdmissionFromRecord).mockReturnValue({
1430
- decision: 'admitted',
1431
- reason: 'mock_admitted',
1432
- nextAction: 'none',
1433
- evidenceStatus: 'unknown',
1434
- });
1435
- });
1436
-
1437
- // Restore the factory-default admission decision so later describe blocks
1438
- // (which do `vi.clearAllMocks()` but do not re-set the implementation) see
1439
- // `admitted` again. Without this, a `mockReturnValue({deferred})` from
1440
- // DEFECT-005-B would leak into BUG-2 and refuse all its candidates.
1441
- afterEach(async () => {
1442
- const { evaluateCandidateAdmissionFromRecord } = await import('@principles/core/runtime-v2');
1443
- vi.mocked(evaluateCandidateAdmissionFromRecord).mockReturnValue({
1444
- decision: 'admitted',
1445
- reason: 'mock_admitted',
1446
- nextAction: 'none',
1447
- evidenceStatus: 'unknown',
1448
- });
1449
- });
1450
-
1451
- it('DEFECT-005-A: defer deferred + principle/rule/prompt consumed — principle/rule/prompt still dreamer_seeded', async () => {
1452
- // Admission gate defers ONLY defer candidates; admits everything else.
1453
- const { evaluateCandidateAdmissionFromRecord } = await import('@principles/core/runtime-v2');
1454
- vi.mocked(evaluateCandidateAdmissionFromRecord).mockImplementation((record) => {
1455
- if (record.recommendationKind === 'defer') {
1456
- return {
1457
- decision: 'deferred',
1458
- reason: 'recommendation_kind_defer_not_actionable',
1459
- nextAction: 'review_defer_disposition_manually',
1460
- evidenceStatus: 'unknown',
1461
- };
1462
- }
1463
- return {
1464
- decision: 'admitted',
1465
- reason: 'mock_admitted',
1466
- nextAction: 'none',
1467
- evidenceStatus: 'unknown',
1468
- };
1469
- });
1470
-
1471
- const candidates = [
1472
- { candidateId: 'cand-defer', artifactId: 'art-d', taskId: 'test-task-1', status: 'pending', recommendationKind: 'defer', confidence: 0.9 },
1473
- { candidateId: 'cand-principle', artifactId: 'art-p', taskId: 'test-task-1', status: 'pending', recommendationKind: 'principle', confidence: 0.9 },
1474
- { candidateId: 'cand-rule', artifactId: 'art-r', taskId: 'test-task-1', status: 'pending', recommendationKind: 'rule', confidence: 0.9 },
1475
- { candidateId: 'cand-prompt', artifactId: 'art-pp', taskId: 'test-task-1', status: 'pending', recommendationKind: 'prompt', confidence: 0.9 },
1476
- { candidateId: 'cand-impl', artifactId: 'art-i', taskId: 'test-task-1', status: 'pending', recommendationKind: 'implementation', confidence: 0.9 },
1477
- ];
1478
- mockGetCandidatesByTaskId.mockResolvedValue(candidates);
1479
-
1480
- const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
1481
- const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => undefined) as () => never);
1482
-
1483
- await handleDiagnoseRun({
1484
- taskId: 'test-task-1',
1485
- workspace: '/tmp/fake-workspace',
1486
- runtime: 'test-double',
1487
- json: true,
1488
- } as DiagnoseRunOptions);
1489
-
1490
- const jsonOutput = consoleSpy.mock.calls.find(call => {
1491
- try {
1492
- const parsed = JSON.parse(call[0] as string);
1493
- return parsed.intake && Array.isArray(parsed.intake.candidates);
1494
- } catch { return false; }
1495
- });
1496
- expect(jsonOutput).toBeDefined();
1497
- const parsed = JSON.parse((jsonOutput as [string])[0]);
1498
- const results = parsed.intake.candidates as Array<{ candidateId: string; status: string }>;
1499
-
1500
- // defer candidate: intake_failed (admission gate deferred) — correct.
1501
- // intakeResults may carry multiple entries per candidate (intake + seed),
1502
- // so we match on BOTH candidateId and status (same pattern as DREAMER-05).
1503
- const deferFailed = results.find(r => r.candidateId === 'cand-defer' && r.status === 'intake_failed');
1504
- expect(deferFailed).toBeDefined();
1505
-
1506
- // principle/rule/prompt: dreamer_seeded — MUST NOT be poisoned by defer's intake_failed
1507
- const principleSeeded = results.find(r => r.candidateId === 'cand-principle' && r.status === 'dreamer_seeded');
1508
- expect(principleSeeded).toBeDefined();
1509
-
1510
- const ruleSeeded = results.find(r => r.candidateId === 'cand-rule' && r.status === 'dreamer_seeded');
1511
- expect(ruleSeeded).toBeDefined();
1512
-
1513
- const promptSeeded = results.find(r => r.candidateId === 'cand-prompt' && r.status === 'dreamer_seeded');
1514
- expect(promptSeeded).toBeDefined();
1515
-
1516
- // 3 dreamer tasks created (principle + rule + prompt).
1517
- // defer was refused at the gate (no intake, no seed).
1518
- // implementation is skipped by the dreamer seed loop (`if kind === 'implementation' continue`).
1519
- expect(mockCreateTask).toHaveBeenCalledTimes(3);
1520
-
1521
- consoleSpy.mockRestore();
1522
- exitSpy.mockRestore();
1523
- });
1524
-
1525
- it('DEFECT-005-B: all candidates intake_failed — 0 dreamer_seeded (boundary)', async () => {
1526
- // Refuse everything via admission gate
1527
- const { evaluateCandidateAdmissionFromRecord } = await import('@principles/core/runtime-v2');
1528
- vi.mocked(evaluateCandidateAdmissionFromRecord).mockReturnValue({
1529
- decision: 'deferred',
1530
- reason: 'mock_deferred',
1531
- nextAction: 'none',
1532
- evidenceStatus: 'unknown',
1533
- });
1534
-
1535
- const candidates = [
1536
- { candidateId: 'cand-1', artifactId: 'art-1', taskId: 'test-task-1', status: 'pending', recommendationKind: 'principle', confidence: 0.9 },
1537
- { candidateId: 'cand-2', artifactId: 'art-2', taskId: 'test-task-1', status: 'pending', recommendationKind: 'rule', confidence: 0.9 },
1538
- ];
1539
- mockGetCandidatesByTaskId.mockResolvedValue(candidates);
1540
-
1541
- const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
1542
- const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => undefined) as () => never);
1543
-
1544
- await handleDiagnoseRun({
1545
- taskId: 'test-task-1',
1546
- workspace: '/tmp/fake-workspace',
1547
- runtime: 'test-double',
1548
- json: true,
1549
- } as DiagnoseRunOptions);
1550
-
1551
- // No dreamer tasks should be created when no candidate was consumed
1552
- expect(mockCreateTask).not.toHaveBeenCalled();
1553
-
1554
- const jsonOutput = consoleSpy.mock.calls.find(call => {
1555
- try {
1556
- const parsed = JSON.parse(call[0] as string);
1557
- return parsed.intake && Array.isArray(parsed.intake.candidates);
1558
- } catch { return false; }
1559
- });
1560
- expect(jsonOutput).toBeDefined();
1561
- const parsed = JSON.parse((jsonOutput as [string])[0]);
1562
- const results = parsed.intake.candidates as Array<{ candidateId: string; status: string }>;
1563
- expect(results.every(r => r.status === 'intake_failed')).toBe(true);
1564
-
1565
- consoleSpy.mockRestore();
1566
- exitSpy.mockRestore();
1567
- });
1568
- });
1569
-
1570
- // BUG-1 (PRI-442): effectiveConfig must be passed to split-pipeline runners
1571
- // so that ADR-0019 LLM rate-limit degradation (isDegradationEnabled) can fire.
1572
- // Without this wiring, isDegradationEnabled() always returns false because
1573
- // this.config.effectiveConfig is undefined. EP-02: production path wiring.
1574
- describe('BUG-1 (PRI-442): effectiveConfig wiring to split-pipeline runners', () => {
1575
- beforeEach(async () => {
1576
- vi.clearAllMocks();
1577
- const { run } = await import('@principles/core/runtime-v2');
1578
- vi.mocked(run).mockResolvedValue(SUCCEEDED_RESULT);
1579
- mockGetCandidatesByTaskId.mockResolvedValue([]);
1580
- mockUpdateCandidateStatus.mockResolvedValue(undefined);
1581
- mockIntake.mockReset();
1582
- });
1583
-
1584
- it('passes effectiveConfig to DiagRootCauseRunner when split pipeline is enabled', async () => {
1585
- const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
1586
- const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => undefined) as () => never);
1587
-
1588
- await handleDiagnoseRun({
1589
- taskId: 'test-task-1',
1590
- workspace: '/tmp/fake-workspace',
1591
- runtime: 'test-double',
1592
- json: true,
1593
- } as DiagnoseRunOptions);
1594
-
1595
- expect(diagRootCauseRunnerCtor).toHaveBeenCalled();
1596
- const optionsArg = diagRootCauseRunnerCtor.mock.calls[0]?.[1];
1597
- expect(optionsArg?.effectiveConfig).toBeDefined();
1598
- expect(optionsArg?.effectiveConfig).toEqual(
1599
- expect.objectContaining({ config: expect.anything(), source: 'file' }),
1600
- );
1601
-
1602
- consoleSpy.mockRestore();
1603
- exitSpy.mockRestore();
1604
- });
1605
-
1606
- it('passes effectiveConfig to DiagDistillerRunner and DiagRouterRunner', async () => {
1607
- const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
1608
- const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => undefined) as () => never);
1609
-
1610
- await handleDiagnoseRun({
1611
- taskId: 'test-task-1',
1612
- workspace: '/tmp/fake-workspace',
1613
- runtime: 'test-double',
1614
- json: true,
1615
- } as DiagnoseRunOptions);
1616
-
1617
- const distillerOptions = diagDistillerRunnerCtor.mock.calls[0]?.[1];
1618
- expect(distillerOptions?.effectiveConfig).toBeDefined();
1619
-
1620
- const routerOptions = diagRouterRunnerCtor.mock.calls[0]?.[1];
1621
- expect(routerOptions?.effectiveConfig).toBeDefined();
1622
-
1623
- consoleSpy.mockRestore();
1624
- exitSpy.mockRestore();
1625
- });
1626
-
1627
- // CR-2 (CodeRabbit P2): loadPdConfig mock only covered ok:true. The
1628
- // ok:false fallback branch (effectiveConfig = defaults) was untested.
1629
- // This test locks in the fallback behavior + asserts rc-9 observability.
1630
- it('warns and falls back to defaults when configLoadResult.ok is false', async () => {
1631
- // Override the module-level mock for this test only
1632
- const { loadPdConfig } = await import('../../src/services/pd-config-loader.js');
1633
- vi.mocked(loadPdConfig).mockReturnValueOnce({
1634
- ok: false,
1635
- source: 'malformed',
1636
- configPath: '/tmp/fake-workspace/.pd/config.yaml',
1637
- errors: [
1638
- { path: 'featureFlags.diagnostician_llm_degradation', reason: 'expected boolean, got string', nextAction: 'Fix the value to be a boolean' },
1639
- ],
1640
- defaults: { config: {}, source: 'defaults', warnings: [] },
1641
- warnings: [],
1642
- legacyFilesDetected: [],
1643
- legacyFileNextActions: [],
1644
- });
1645
-
1646
- const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
1647
- const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
1648
- const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => undefined) as () => never);
1649
-
1650
- await handleDiagnoseRun({
1651
- taskId: 'test-task-1',
1652
- workspace: '/tmp/fake-workspace',
1653
- runtime: 'test-double',
1654
- json: true,
1655
- } as DiagnoseRunOptions);
1656
-
1657
- // Assert warning was emitted (rc-9: no silent fallback)
1658
- expect(warnSpy).toHaveBeenCalledWith(
1659
- expect.stringContaining('[pd diagnose]'),
1660
- );
1661
- expect(warnSpy).toHaveBeenCalledWith(
1662
- expect.stringContaining('malformed'),
1663
- );
1664
-
1665
- // Assert runners still received effectiveConfig (the defaults-based one)
1666
- expect(diagRootCauseRunnerCtor).toHaveBeenCalled();
1667
- const optionsArg = diagRootCauseRunnerCtor.mock.calls[0]?.[1];
1668
- expect(optionsArg?.effectiveConfig).toBeDefined();
1669
- expect(optionsArg?.effectiveConfig).toEqual(
1670
- expect.objectContaining({ source: 'defaults' }),
1671
- );
1672
-
1673
- warnSpy.mockRestore();
1674
- consoleSpy.mockRestore();
1675
- exitSpy.mockRestore();
1676
- });
1677
- });
1678
-
1679
- // BUG-2 (PRI-442): sourcePainId must be resolved from the diagnostician task
1680
- // chain and passed to buildDreamerSeedFromCandidate. Without this, the dreamer
1681
- // task's diagnosticJson omits sourcePainId, breaking run-rulehost lineage.
1682
- // rc-6: lineage consistency. ERR-004: never invent lineage.
1683
- describe('BUG-2 (PRI-442): sourcePainId resolution for dreamer seed', () => {
1684
- beforeEach(async () => {
1685
- vi.clearAllMocks();
1686
- buildDreamerSeedCalls.length = 0;
1687
- const { run } = await import('@principles/core/runtime-v2');
1688
- vi.mocked(run).mockResolvedValue(SUCCEEDED_RESULT);
1689
- mockGetCandidatesByTaskId.mockResolvedValue([]);
1690
- mockUpdateCandidateStatus.mockResolvedValue(undefined);
1691
- mockCreateTask.mockResolvedValue(undefined);
1692
- mockGetTask.mockResolvedValue(null);
1693
- mockIntake.mockReset();
1694
- mockIntake.mockResolvedValue({ id: 'ledger-1', title: 'P1', status: 'probation' });
1695
- mockResolveSourcePainId.mockResolvedValue('pain_test-source-1');
1696
- });
1697
-
1698
- it('passes resolved sourcePainId to buildDreamerSeedFromCandidate (lineage consistency)', async () => {
1699
- const candidates = [
1700
- { candidateId: 'cand-1', artifactId: 'art-1', taskId: 'test-task-1', status: 'pending', recommendationKind: 'principle' },
1701
- ];
1702
- mockGetCandidatesByTaskId.mockResolvedValue(candidates);
1703
-
1704
- const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
1705
- const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => undefined) as () => never);
1706
-
1707
- await handleDiagnoseRun({
1708
- taskId: 'test-task-1',
1709
- workspace: '/tmp/fake-workspace',
1710
- runtime: 'test-double',
1711
- json: true,
1712
- } as DiagnoseRunOptions);
1713
-
1714
- // resolveSourcePainIdFromDiagnostician should have been called
1715
- expect(mockResolveSourcePainId).toHaveBeenCalled();
1716
- // buildDreamerSeedFromCandidate should have received the resolved sourcePainId
1717
- expect(buildDreamerSeedCalls.length).toBeGreaterThan(0);
1718
- expect(buildDreamerSeedCalls[0].sourcePainId).toBe('pain_test-source-1');
1719
-
1720
- consoleSpy.mockRestore();
1721
- exitSpy.mockRestore();
1722
- });
1723
- });
1724
-
1725
- // ── PRI-638: unified capability-disabled semantics ───────────────────────────
1726
- //
1727
- // The CLI owns no kill switch of its own: it reads the canonical authority
1728
- // (internalAgents.agents.diagnostician.enabled) through the same resolver the
1729
- // runtime factory uses. Owner-disabled must come out as a structured
1730
- // `capability_disabled` result — never as missing_runtime / config failure —
1731
- // with no adapter constructed and no provider contacted.
1732
-
1733
- describe('PRI-638: pd diagnose run when Diagnostician capability is disabled', () => {
1734
- beforeEach(async () => {
1735
- vi.clearAllMocks();
1736
- const runtimeV2 = await import('@principles/core/runtime-v2');
1737
- vi.mocked(runtimeV2.resolveDiagnosticianCapability).mockReturnValue({
1738
- available: false,
1739
- reason: 'capability_disabled',
1740
- message: "Agent 'diagnostician' is disabled",
1741
- nextAction: "Enable agent 'diagnostician' in .pd/config.yaml internalAgents.agents.diagnostician.enabled",
1742
- });
1743
- });
1744
-
1745
- afterEach(async () => {
1746
- const runtimeV2 = await import('@principles/core/runtime-v2');
1747
- vi.mocked(runtimeV2.resolveDiagnosticianCapability).mockReset();
1748
- });
1749
-
1750
- it('DIAG-638-01: --json emits a structured capability_disabled result and exits 1', async () => {
1751
- const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
1752
- const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => undefined) as () => never);
1753
-
1754
- await handleDiagnoseRun({
1755
- taskId: 'diag_task-1',
1756
- workspace: '/tmp/fake-workspace',
1757
- runtime: 'test-double',
1758
- json: true,
1759
- } as DiagnoseRunOptions);
1760
-
1761
- const jsonLine = logSpy.mock.calls
1762
- .map((c) => String(c[0]))
1763
- .find((line) => line.trim().startsWith('{'));
1764
- expect(jsonLine).toBeDefined();
1765
- const parsed = JSON.parse(jsonLine as string);
1766
- expect(parsed.reason).toBe('capability_disabled');
1767
- expect(parsed.nextAction).toContain('internalAgents.agents.diagnostician.enabled');
1768
- expect(parsed.message).toContain('disabled');
1769
-
1770
- // Kill switch fires before any runtime machinery: no adapter, no runner.
1771
- const runtimeV2 = await import('@principles/core/runtime-v2');
1772
- expect(runtimeV2.TestDoubleRuntimeAdapter).not.toHaveBeenCalled();
1773
- expect(runtimeV2.SplitDiagnosticianRunner).not.toHaveBeenCalled();
1774
- expect(exitSpy).toHaveBeenCalledWith(1);
1775
-
1776
- logSpy.mockRestore();
1777
- exitSpy.mockRestore();
1778
- });
1779
-
1780
- it('DIAG-638-02: human-readable output names the reason and the recovery action', async () => {
1781
- const errSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
1782
- const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => undefined) as () => never);
1783
-
1784
- await handleDiagnoseRun({
1785
- taskId: 'diag_task-1',
1786
- workspace: '/tmp/fake-workspace',
1787
- runtime: 'test-double',
1788
- json: false,
1789
- } as DiagnoseRunOptions);
1790
-
1791
- const out = errSpy.mock.calls.map((c) => String(c[0])).join('\n');
1792
- expect(out).toContain('diagnostician');
1793
- expect(out).toContain('capability_disabled');
1794
- expect(out).toContain('internalAgents.agents.diagnostician.enabled');
1795
-
1796
- const runtimeV2b = await import('@principles/core/runtime-v2');
1797
- expect(runtimeV2b.TestDoubleRuntimeAdapter).not.toHaveBeenCalled();
1798
- expect(exitSpy).toHaveBeenCalledWith(1);
1799
-
1800
- errSpy.mockRestore();
1801
- exitSpy.mockRestore();
1802
- });
1803
- });