@principles/pd-cli 1.152.12 → 1.152.14

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 (305) hide show
  1. package/dist/commands/candidate.d.ts.map +1 -1
  2. package/dist/commands/candidate.js +34 -7
  3. package/dist/commands/candidate.js.map +1 -1
  4. package/dist/commands/diagnose.d.ts.map +1 -1
  5. package/dist/commands/diagnose.js +23 -7
  6. package/dist/commands/diagnose.js.map +1 -1
  7. package/dist/commands/pain-retry.d.ts.map +1 -1
  8. package/dist/commands/pain-retry.js +21 -7
  9. package/dist/commands/pain-retry.js.map +1 -1
  10. package/package.json +13 -7
  11. package/CHANGELOG.md +0 -23
  12. package/dist/commands/__tests__/intent-flag-wiring.test.d.ts +0 -9
  13. package/dist/commands/__tests__/intent-flag-wiring.test.d.ts.map +0 -1
  14. package/dist/commands/__tests__/intent-flag-wiring.test.js +0 -237
  15. package/dist/commands/__tests__/intent-flag-wiring.test.js.map +0 -1
  16. package/dist/commands/__tests__/legacy-cleanup.test.d.ts +0 -18
  17. package/dist/commands/__tests__/legacy-cleanup.test.d.ts.map +0 -1
  18. package/dist/commands/__tests__/legacy-cleanup.test.js +0 -467
  19. package/dist/commands/__tests__/legacy-cleanup.test.js.map +0 -1
  20. package/dist/commands/__tests__/mvp-smoke.test.d.ts +0 -15
  21. package/dist/commands/__tests__/mvp-smoke.test.d.ts.map +0 -1
  22. package/dist/commands/__tests__/mvp-smoke.test.js +0 -262
  23. package/dist/commands/__tests__/mvp-smoke.test.js.map +0 -1
  24. package/dist/commands/__tests__/pain-retry-maxTokens-flag.test.d.ts +0 -2
  25. package/dist/commands/__tests__/pain-retry-maxTokens-flag.test.d.ts.map +0 -1
  26. package/dist/commands/__tests__/pain-retry-maxTokens-flag.test.js +0 -60
  27. package/dist/commands/__tests__/pain-retry-maxTokens-flag.test.js.map +0 -1
  28. package/dist/commands/__tests__/rulecode-flag-wiring.test.d.ts +0 -21
  29. package/dist/commands/__tests__/rulecode-flag-wiring.test.d.ts.map +0 -1
  30. package/dist/commands/__tests__/rulecode-flag-wiring.test.js +0 -186
  31. package/dist/commands/__tests__/rulecode-flag-wiring.test.js.map +0 -1
  32. package/dist/commands/__tests__/rulecode-handler.test.d.ts +0 -16
  33. package/dist/commands/__tests__/rulecode-handler.test.d.ts.map +0 -1
  34. package/dist/commands/__tests__/rulecode-handler.test.js +0 -285
  35. package/dist/commands/__tests__/rulecode-handler.test.js.map +0 -1
  36. package/dist/commands/__tests__/run-rulehost-flag-wiring.test.d.ts +0 -24
  37. package/dist/commands/__tests__/run-rulehost-flag-wiring.test.d.ts.map +0 -1
  38. package/dist/commands/__tests__/run-rulehost-flag-wiring.test.js +0 -232
  39. package/dist/commands/__tests__/run-rulehost-flag-wiring.test.js.map +0 -1
  40. package/dist/commands/__tests__/runtime-activation-nextaction.test.d.ts +0 -15
  41. package/dist/commands/__tests__/runtime-activation-nextaction.test.d.ts.map +0 -1
  42. package/dist/commands/__tests__/runtime-activation-nextaction.test.js +0 -73
  43. package/dist/commands/__tests__/runtime-activation-nextaction.test.js.map +0 -1
  44. package/dist/commands/__tests__/runtime-probe-config.test.d.ts +0 -20
  45. package/dist/commands/__tests__/runtime-probe-config.test.d.ts.map +0 -1
  46. package/dist/commands/__tests__/runtime-probe-config.test.js +0 -389
  47. package/dist/commands/__tests__/runtime-probe-config.test.js.map +0 -1
  48. package/dist/commands/__tests__/telemetry-flag-wiring.test.d.ts +0 -9
  49. package/dist/commands/__tests__/telemetry-flag-wiring.test.d.ts.map +0 -1
  50. package/dist/commands/__tests__/telemetry-flag-wiring.test.js +0 -76
  51. package/dist/commands/__tests__/telemetry-flag-wiring.test.js.map +0 -1
  52. package/dist/commands/__tests__/version.test.d.ts +0 -2
  53. package/dist/commands/__tests__/version.test.d.ts.map +0 -1
  54. package/dist/commands/__tests__/version.test.js +0 -238
  55. package/dist/commands/__tests__/version.test.js.map +0 -1
  56. package/dist/commands/runtime-uat.guard.test.d.ts +0 -2
  57. package/dist/commands/runtime-uat.guard.test.d.ts.map +0 -1
  58. package/dist/commands/runtime-uat.guard.test.js +0 -153
  59. package/dist/commands/runtime-uat.guard.test.js.map +0 -1
  60. package/dist/services/__tests__/cli-output.test.d.ts +0 -18
  61. package/dist/services/__tests__/cli-output.test.d.ts.map +0 -1
  62. package/dist/services/__tests__/cli-output.test.js +0 -103
  63. package/dist/services/__tests__/cli-output.test.js.map +0 -1
  64. package/dist/services/__tests__/evaluator-runner-deps.test.d.ts +0 -2
  65. package/dist/services/__tests__/evaluator-runner-deps.test.d.ts.map +0 -1
  66. package/dist/services/__tests__/evaluator-runner-deps.test.js +0 -343
  67. package/dist/services/__tests__/evaluator-runner-deps.test.js.map +0 -1
  68. package/dist/services/__tests__/rulehost-readiness.test.d.ts +0 -2
  69. package/dist/services/__tests__/rulehost-readiness.test.d.ts.map +0 -1
  70. package/dist/services/__tests__/rulehost-readiness.test.js +0 -311
  71. package/dist/services/__tests__/rulehost-readiness.test.js.map +0 -1
  72. package/dist/services/__tests__/runtime-adapter-resolver.test.d.ts +0 -18
  73. package/dist/services/__tests__/runtime-adapter-resolver.test.d.ts.map +0 -1
  74. package/dist/services/__tests__/runtime-adapter-resolver.test.js +0 -673
  75. package/dist/services/__tests__/runtime-adapter-resolver.test.js.map +0 -1
  76. package/dist/utils/production-workspace-guard.test.d.ts +0 -2
  77. package/dist/utils/production-workspace-guard.test.d.ts.map +0 -1
  78. package/dist/utils/production-workspace-guard.test.js +0 -137
  79. package/dist/utils/production-workspace-guard.test.js.map +0 -1
  80. package/scripts/llm-dogfood.ts +0 -456
  81. package/scripts/migrate-illegal-expected-decision.ts +0 -252
  82. package/src/commands/__tests__/intent-flag-wiring.test.ts +0 -282
  83. package/src/commands/__tests__/legacy-cleanup.test.ts +0 -602
  84. package/src/commands/__tests__/mvp-smoke.test.ts +0 -300
  85. package/src/commands/__tests__/pain-retry-maxTokens-flag.test.ts +0 -73
  86. package/src/commands/__tests__/rulecode-flag-wiring.test.ts +0 -237
  87. package/src/commands/__tests__/rulecode-handler.test.ts +0 -369
  88. package/src/commands/__tests__/run-rulehost-flag-wiring.test.ts +0 -292
  89. package/src/commands/__tests__/runtime-activation-nextaction.test.ts +0 -81
  90. package/src/commands/__tests__/runtime-probe-config.test.ts +0 -432
  91. package/src/commands/__tests__/telemetry-flag-wiring.test.ts +0 -84
  92. package/src/commands/__tests__/version.test.ts +0 -251
  93. package/src/commands/admission-gate.ts +0 -30
  94. package/src/commands/artifact.ts +0 -82
  95. package/src/commands/build-trajectory-evidence.ts +0 -298
  96. package/src/commands/candidate.ts +0 -1250
  97. package/src/commands/codex-ingest-catchup.ts +0 -114
  98. package/src/commands/codex-ingest-quarantine.ts +0 -127
  99. package/src/commands/codex-reconcile.ts +0 -84
  100. package/src/commands/codex-setup.ts +0 -476
  101. package/src/commands/codex-worker.ts +0 -185
  102. package/src/commands/command-helpers.ts +0 -24
  103. package/src/commands/config-doctor.ts +0 -193
  104. package/src/commands/console.ts +0 -797
  105. package/src/commands/context.ts +0 -72
  106. package/src/commands/demo-story-a.ts +0 -241
  107. package/src/commands/diagnose.ts +0 -756
  108. package/src/commands/errors-list.ts +0 -441
  109. package/src/commands/evolution-tasks-list.ts +0 -48
  110. package/src/commands/evolution-tasks-show.ts +0 -62
  111. package/src/commands/flow.ts +0 -60
  112. package/src/commands/health-codex.ts +0 -576
  113. package/src/commands/health.ts +0 -315
  114. package/src/commands/history.ts +0 -63
  115. package/src/commands/intent.ts +0 -457
  116. package/src/commands/legacy-cleanup.ts +0 -531
  117. package/src/commands/legacy-import.ts +0 -104
  118. package/src/commands/mvp-smoke.ts +0 -160
  119. package/src/commands/pain-evidence.ts +0 -226
  120. package/src/commands/pain-list.ts +0 -239
  121. package/src/commands/pain-record.ts +0 -612
  122. package/src/commands/pain-retry.ts +0 -916
  123. package/src/commands/principles-stats.ts +0 -700
  124. package/src/commands/proven-channel-baseline.ts +0 -115
  125. package/src/commands/quality-scorecard.ts +0 -272
  126. package/src/commands/remediation-output.ts +0 -66
  127. package/src/commands/rulecode.ts +0 -465
  128. package/src/commands/run.ts +0 -89
  129. package/src/commands/runtime-activation.ts +0 -1629
  130. package/src/commands/runtime-artifact-repair.ts +0 -524
  131. package/src/commands/runtime-canary.ts +0 -395
  132. package/src/commands/runtime-compatibility-scan.ts +0 -105
  133. package/src/commands/runtime-diagnostics-export.ts +0 -229
  134. package/src/commands/runtime-features.ts +0 -153
  135. package/src/commands/runtime-gfi-snapshot.ts +0 -135
  136. package/src/commands/runtime-health-snapshot.ts +0 -108
  137. package/src/commands/runtime-init.ts +0 -429
  138. package/src/commands/runtime-internalization-enqueue-successors.ts +0 -530
  139. package/src/commands/runtime-internalization-integrity-repair.ts +0 -79
  140. package/src/commands/runtime-internalization-integrity.ts +0 -107
  141. package/src/commands/runtime-internalization-queue.ts +0 -135
  142. package/src/commands/runtime-internalization-retry.ts +0 -191
  143. package/src/commands/runtime-internalization-run-once.ts +0 -791
  144. package/src/commands/runtime-internalization-run-rulehost.ts +0 -749
  145. package/src/commands/runtime-internalization-wake-once.ts +0 -87
  146. package/src/commands/runtime-pain-flood-simulation.ts +0 -121
  147. package/src/commands/runtime-pruning.ts +0 -438
  148. package/src/commands/runtime-recovery-failed-tasks.ts +0 -258
  149. package/src/commands/runtime-recovery.ts +0 -109
  150. package/src/commands/runtime-synthetic-baseline.ts +0 -70
  151. package/src/commands/runtime-uat.guard.test.ts +0 -175
  152. package/src/commands/runtime-uat.ts +0 -380
  153. package/src/commands/runtime.ts +0 -473
  154. package/src/commands/samples-list.ts +0 -39
  155. package/src/commands/samples-review.ts +0 -36
  156. package/src/commands/task.ts +0 -271
  157. package/src/commands/telemetry.ts +0 -349
  158. package/src/commands/trace.ts +0 -174
  159. package/src/commands/trajectory-db-unavailable.ts +0 -19
  160. package/src/commands/trajectory.ts +0 -64
  161. package/src/commands/version.ts +0 -49
  162. package/src/config-reader.ts +0 -122
  163. package/src/index.ts +0 -1276
  164. package/src/legacy/legacy-import.ts +0 -179
  165. package/src/legacy/session-history-import.ts +0 -203
  166. package/src/resolve-workspace.ts +0 -106
  167. package/src/services/__tests__/cli-output.test.ts +0 -130
  168. package/src/services/__tests__/evaluator-runner-deps.test.ts +0 -382
  169. package/src/services/__tests__/rulehost-readiness.test.ts +0 -363
  170. package/src/services/__tests__/runtime-adapter-resolver.test.ts +0 -793
  171. package/src/services/cli-output.ts +0 -95
  172. package/src/services/config-doctor.ts +0 -697
  173. package/src/services/console-launcher.ts +0 -537
  174. package/src/services/demo-rule-compiler.ts +0 -40
  175. package/src/services/demo-story-a-runner.ts +0 -471
  176. package/src/services/mainline-snapshot-assembler.ts +0 -579
  177. package/src/services/pain-flood-simulation-runner.ts +0 -462
  178. package/src/services/pd-config-loader.ts +0 -433
  179. package/src/services/proven-channel-baseline-runner.ts +0 -157
  180. package/src/services/quality-scorecard/data-extractor.ts +0 -150
  181. package/src/services/quality-scorecard/local-evaluator.ts +0 -142
  182. package/src/services/quality-scorecard/strong-model-gate.ts +0 -202
  183. package/src/services/resolve-runtime-from-pd-config.ts +0 -230
  184. package/src/services/rulehost-pipeline-runner.ts +0 -1214
  185. package/src/services/rulehost-readiness.ts +0 -326
  186. package/src/services/runtime-adapter-resolver.ts +0 -364
  187. package/src/services/synthetic-baseline-runner.ts +0 -400
  188. package/src/services/version-report.ts +0 -253
  189. package/src/services/workspace-tool-semantics.ts +0 -26
  190. package/src/utils/path-security.ts +0 -96
  191. package/src/utils/production-workspace-guard.test.ts +0 -157
  192. package/src/utils/production-workspace-guard.ts +0 -210
  193. package/tests/bdd/cli-contract.steps.ts +0 -346
  194. package/tests/bdd/principles-stats.steps.ts +0 -211
  195. package/tests/bdd/support/gherkin-loader.ts +0 -147
  196. package/tests/bdd/support/repo-root.ts +0 -62
  197. package/tests/bdd/support/vitest-bdd.ts +0 -179
  198. package/tests/commands/admission-gate-coverage.test.ts +0 -79
  199. package/tests/commands/build-trajectory-evidence.test.ts +0 -508
  200. package/tests/commands/candidate-audit-repair.test.ts +0 -421
  201. package/tests/commands/candidate-intake.test.ts +0 -732
  202. package/tests/commands/candidate-internalization-backfill.test.ts +0 -548
  203. package/tests/commands/candidate-internalize-lineage.test.ts +0 -628
  204. package/tests/commands/candidate-internalize.test.ts +0 -299
  205. package/tests/commands/candidate-route.test.ts +0 -329
  206. package/tests/commands/candidate-show.test.ts +0 -95
  207. package/tests/commands/cli-command-tree.test.ts +0 -144
  208. package/tests/commands/cli-help-snapshot.test.ts +0 -155
  209. package/tests/commands/cli-skill-contract.test.ts +0 -121
  210. package/tests/commands/codex-consent-reversibility.steps.test.ts +0 -299
  211. package/tests/commands/codex-ingest-quarantine.test.ts +0 -136
  212. package/tests/commands/codex-setup.test.ts +0 -297
  213. package/tests/commands/codex-worker-daemon.test.ts +0 -95
  214. package/tests/commands/codex-worker-registration.test.ts +0 -207
  215. package/tests/commands/config-doctor.test.ts +0 -336
  216. package/tests/commands/console-launcher-edge-cases.test.ts +0 -388
  217. package/tests/commands/console-open.test.ts +0 -1381
  218. package/tests/commands/context.test.ts +0 -114
  219. package/tests/commands/demo-story-a.test.ts +0 -318
  220. package/tests/commands/diagnose.test.ts +0 -1803
  221. package/tests/commands/errors-list.test.ts +0 -679
  222. package/tests/commands/evolution-tasks-list.test.ts +0 -41
  223. package/tests/commands/evolution-tasks-show.test.ts +0 -46
  224. package/tests/commands/health-codex.test.ts +0 -349
  225. package/tests/commands/health-host-wiring.test.ts +0 -76
  226. package/tests/commands/health-reliability.test.ts +0 -99
  227. package/tests/commands/health.test.ts +0 -413
  228. package/tests/commands/intent.test.ts +0 -321
  229. package/tests/commands/legacy-cleanup.test.ts +0 -148
  230. package/tests/commands/pain-evidence.test.ts +0 -518
  231. package/tests/commands/pain-list.test.ts +0 -219
  232. package/tests/commands/pain-record-async.test.ts +0 -335
  233. package/tests/commands/pain-record-session-parser.test.ts +0 -228
  234. package/tests/commands/pain-record.test.ts +0 -851
  235. package/tests/commands/pain-retry.test.ts +0 -1121
  236. package/tests/commands/plugin-config-resolution-cutover.test.ts +0 -229
  237. package/tests/commands/pri-393-runtime-config-unification.test.ts +0 -443
  238. package/tests/commands/principles-stats-wiring.test.ts +0 -135
  239. package/tests/commands/principles-stats.test.ts +0 -274
  240. package/tests/commands/product-path-regression.test.ts +0 -86
  241. package/tests/commands/proven-channel-baseline.test.ts +0 -441
  242. package/tests/commands/rulecode.test.ts +0 -177
  243. package/tests/commands/run-rulehost-handler.test.ts +0 -908
  244. package/tests/commands/runtime-activation-approve-flag-wiring.test.ts +0 -227
  245. package/tests/commands/runtime-activation-deactivate-flag-wiring.test.ts +0 -167
  246. package/tests/commands/runtime-activation-dispatch-flag-wiring.test.ts +0 -262
  247. package/tests/commands/runtime-activation-governance-audit.test.ts +0 -95
  248. package/tests/commands/runtime-activation-list-flag-wiring.test.ts +0 -193
  249. package/tests/commands/runtime-activation-promote-flag-wiring.test.ts +0 -248
  250. package/tests/commands/runtime-activation-shadow-telemetry.test.ts +0 -130
  251. package/tests/commands/runtime-activation.test.ts +0 -2052
  252. package/tests/commands/runtime-artifact-repair-registration.test.ts +0 -74
  253. package/tests/commands/runtime-artifact-repair.test.ts +0 -404
  254. package/tests/commands/runtime-canary.test.ts +0 -430
  255. package/tests/commands/runtime-compatibility-scan.test.ts +0 -146
  256. package/tests/commands/runtime-diagnostics-export.test.ts +0 -175
  257. package/tests/commands/runtime-features.test.ts +0 -249
  258. package/tests/commands/runtime-health-snapshot.test.ts +0 -358
  259. package/tests/commands/runtime-init-empty-workspace.test.ts +0 -252
  260. package/tests/commands/runtime-init.test.ts +0 -420
  261. package/tests/commands/runtime-internalization-enqueue-successors.test.ts +0 -782
  262. package/tests/commands/runtime-internalization-integrity-repair.test.ts +0 -239
  263. package/tests/commands/runtime-internalization-integrity.test.ts +0 -140
  264. package/tests/commands/runtime-internalization-queue.test.ts +0 -380
  265. package/tests/commands/runtime-internalization-retry-owner-authority.test.ts +0 -431
  266. package/tests/commands/runtime-internalization-run-once-evaluator-parity.test.ts +0 -269
  267. package/tests/commands/runtime-internalization-run-once-language.test.ts +0 -303
  268. package/tests/commands/runtime-internalization-run-once-real-config.test.ts +0 -237
  269. package/tests/commands/runtime-internalization-run-once-rollout-parity.test.ts +0 -706
  270. package/tests/commands/runtime-internalization-run-once.test.ts +0 -1791
  271. package/tests/commands/runtime-internalization-wake-once.test.ts +0 -171
  272. package/tests/commands/runtime-internalization.test.ts +0 -53
  273. package/tests/commands/runtime-pain-flood-simulation.test.ts +0 -418
  274. package/tests/commands/runtime-pruning.test.ts +0 -694
  275. package/tests/commands/runtime-recovery-failed-tasks.test.ts +0 -328
  276. package/tests/commands/runtime-recovery.test.ts +0 -120
  277. package/tests/commands/runtime-synthetic-baseline.test.ts +0 -249
  278. package/tests/commands/runtime-uat.test.ts +0 -397
  279. package/tests/commands/runtime.test.ts +0 -392
  280. package/tests/commands/samples-list.test.ts +0 -43
  281. package/tests/commands/samples-review.test.ts +0 -41
  282. package/tests/commands/task.test.ts +0 -153
  283. package/tests/commands/telemetry.test.ts +0 -190
  284. package/tests/commands/trace.test.ts +0 -316
  285. package/tests/config-reader.test.ts +0 -143
  286. package/tests/e2e/candidate-intake-e2e.test.ts +0 -338
  287. package/tests/e2e/cli-full-flow.test.ts +0 -233
  288. package/tests/e2e/cross-package-acceptance.test.ts +0 -635
  289. package/tests/helpers/pd-cli-path.ts +0 -5
  290. package/tests/pd-cli-smoke.test.ts +0 -13
  291. package/tests/resolve-workspace.test.ts +0 -196
  292. package/tests/services/config-doctor-signal-health.test.ts +0 -109
  293. package/tests/services/console-launcher.test.ts +0 -114
  294. package/tests/services/demo-rule-compiler.test.ts +0 -242
  295. package/tests/services/mainline-snapshot-assembler.test.ts +0 -572
  296. package/tests/services/pd-config-loader.test.ts +0 -505
  297. package/tests/services/proven-channel-baseline-runner.test.ts +0 -30
  298. package/tests/services/quality-scorecard/strong-model-gate.test.ts +0 -133
  299. package/tests/services/resolve-runtime-from-pd-config.test.ts +0 -485
  300. package/tests/services/rulehost-pipeline-e2e.test.ts +0 -538
  301. package/tests/services/rulehost-pipeline-runner.test.ts +0 -1242
  302. package/tests/services/workspace-tool-semantics.test.ts +0 -76
  303. package/tests/utils/path-security.test.ts +0 -180
  304. package/tsconfig.json +0 -26
  305. package/vitest.config.ts +0 -10
@@ -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
- });