@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,1121 +0,0 @@
1
- /**
2
- * Tests for pd pain retry command.
3
- *
4
- * Covers:
5
- * - Command parser/registration: --pain-id, --json, --force
6
- * - Success path: retry_wait + last_error → succeeded + last_error cleared
7
- * - Not found path: JSON single object + reason + nextAction
8
- * - Already succeeded without --force: refused
9
- * - Force path: allow retry of succeeded task
10
- * - Strict JSON: --json stdout exactly one parseable JSON object
11
- * - No mutation on failed validation: no run/candidate/ledger created when task not found
12
- * - painId with diagnosis_ prefix: rejected with reason + nextAction
13
- * - Wrong taskKind: rejected with reason + nextAction
14
- * - Missing pi-ai config: rejected with reason + nextAction
15
- */
16
- import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
17
- import { Command } from 'commander';
18
-
19
- // ── Mocks ──────────────────────────────────────────────────────────────────────
20
-
21
- const { MockRuntimeStateManager, mockGetTask, mockGetCandidatesByTaskId, mockUpdateCandidateStatus, mockGetRunsByTask } = vi.hoisted(() => {
22
- const mockGetTask = vi.fn().mockResolvedValue(null);
23
- const mockGetCandidatesByTaskId = vi.fn().mockResolvedValue([]);
24
- const mockUpdateCandidateStatus = vi.fn().mockResolvedValue(undefined);
25
- const mockGetRunsByTask = vi.fn().mockResolvedValue([]);
26
-
27
- class MockRuntimeStateManager {
28
- initialize = vi.fn().mockResolvedValue(undefined);
29
- close = vi.fn().mockResolvedValue(undefined);
30
- getTask = mockGetTask;
31
- getCandidatesByTaskId = mockGetCandidatesByTaskId;
32
- updateCandidateStatus = mockUpdateCandidateStatus;
33
- getRunsByTask = mockGetRunsByTask;
34
- connection = {} as Record<string, unknown>;
35
- taskStore = {};
36
- runStore = {};
37
- }
38
- return { MockRuntimeStateManager, mockGetTask, mockGetCandidatesByTaskId, mockUpdateCandidateStatus, mockGetRunsByTask };
39
- }, { validateType: true });
40
-
41
- const { mockIntake, MockCandidateIntakeService } = vi.hoisted(() => {
42
- const mockIntake = vi.fn();
43
- function MockCandidateIntakeService(this: any) {
44
- return { intake: mockIntake };
45
- }
46
- MockCandidateIntakeService.prototype = {};
47
- return { mockIntake, MockCandidateIntakeService };
48
- });
49
-
50
- const { MockPrincipleTreeLedgerAdapter } = vi.hoisted(() => {
51
- function MockPrincipleTreeLedgerAdapter(this: any) {
52
- return {};
53
- }
54
- MockPrincipleTreeLedgerAdapter.prototype = {};
55
- return { MockPrincipleTreeLedgerAdapter };
56
- });
57
-
58
- const { mockRun, mockResolveRuntimeConfig, mockResolveRuntimeFromPdConfig } = vi.hoisted(() => {
59
- const mockRun = vi.fn().mockResolvedValue({
60
- status: 'succeeded',
61
- taskId: 'diagnosis_test-pain-1',
62
- runId: 'run-retry-1',
63
- contextHash: 'abc123',
64
- });
65
- const mockResolveRuntimeConfig = vi.fn().mockReturnValue({
66
- runtimeKind: 'pi-ai',
67
- provider: 'test-provider',
68
- model: 'test-model',
69
- apiKeyEnv: 'TEST_KEY',
70
- timeoutMs: 300000,
71
- agentId: 'main',
72
- });
73
- const mockResolveRuntimeFromPdConfig = vi.fn().mockReturnValue({
74
- result: {
75
- runtimeKind: 'pi-ai',
76
- provider: 'test-provider',
77
- model: 'test-model',
78
- apiKeyEnv: 'TEST_KEY',
79
- timeoutMs: 300000,
80
- agentId: 'main',
81
- },
82
- legacyWarnings: [],
83
- configSource: '.pd/config.yaml',
84
- configLoadResult: { ok: true, effective: {}, defaults: {}, legacyFilesDetected: [] },
85
- });
86
- return { mockRun, mockResolveRuntimeConfig, mockResolveRuntimeFromPdConfig };
87
- });
88
-
89
- vi.mock('../../src/resolve-workspace.js', () => ({
90
- resolveWorkspaceDir: vi.fn().mockReturnValue('/tmp/fake-workspace'),
91
- }));
92
-
93
- // BUG-1 (PRI-442): capture runner constructor args to verify effectiveConfig wiring
94
- const { diagRootCauseRunnerCtor, diagDistillerRunnerCtor, diagRouterRunnerCtor } = vi.hoisted(() => {
95
- const diagRootCauseRunnerCtor = vi.fn().mockImplementation(function () { return {}; });
96
- const diagDistillerRunnerCtor = vi.fn().mockImplementation(function () { return {}; });
97
- const diagRouterRunnerCtor = vi.fn().mockImplementation(function () { return {}; });
98
- return { diagRootCauseRunnerCtor, diagDistillerRunnerCtor, diagRouterRunnerCtor };
99
- });
100
-
101
- vi.mock('../../src/services/pd-config-loader.js', () => ({
102
- loadPdConfig: vi.fn().mockReturnValue({
103
- ok: true,
104
- effective: { config: { featureFlags: { diagnostician_llm_degradation: true } }, source: 'file', warnings: [] },
105
- defaults: { config: {}, source: 'defaults', warnings: [] },
106
- }),
107
- computeFlagsFromLoadResult: vi.fn().mockReturnValue({}),
108
- }));
109
-
110
- // Dead-letter store mock: getByPainId returns null by default so the
111
- // implementation produces status='not_found' with reason='task_not_found'.
112
- // Tests that need to exercise the dead-letter replay path override this.
113
- const { MockSqliteDeadLetterStore, mockDeadLetterGetByPainId } = vi.hoisted(() => {
114
- const mockDeadLetterGetByPainId = vi.fn().mockReturnValue(null);
115
- class MockSqliteDeadLetterStore {
116
- getByPainId = mockDeadLetterGetByPainId;
117
- constructor(_connection: unknown) {}
118
- }
119
- return { MockSqliteDeadLetterStore, mockDeadLetterGetByPainId };
120
- });
121
-
122
- vi.mock('@principles/core/runtime-v2', () => {
123
- return {
124
- RuntimeStateManager: vi.fn().mockImplementation(function () {
125
- return new MockRuntimeStateManager();
126
- }),
127
- SqliteHistoryQuery: vi.fn().mockImplementation(function () { return {}; }),
128
- SqliteContextAssembler: vi.fn().mockImplementation(function () { return {}; }),
129
- SqliteDiagnosticianCommitter: vi.fn().mockImplementation(function () { return {}; }),
130
- SqliteTrajectoryLocator: vi.fn().mockImplementation(function () { return {}; }),
131
- SqliteSourceTraceLocator: vi.fn().mockImplementation(function () { return {}; }),
132
- SqliteDeadLetterStore: MockSqliteDeadLetterStore,
133
- PainSignalBridge: vi.fn().mockImplementation(function () { return {}; }),
134
- StoreEventEmitter: vi.fn().mockImplementation(function () { return {}; }),
135
- storeEmitter: { emitTelemetry: vi.fn() },
136
- SplitDiagnosticianRunner: vi.fn().mockImplementation(function () { return {}; }),
137
- DiagRootCauseRunner: diagRootCauseRunnerCtor,
138
- DiagDistillerRunner: diagDistillerRunnerCtor,
139
- DiagRouterRunner: diagRouterRunnerCtor,
140
- DefaultDiagRootCauseValidator: vi.fn().mockImplementation(function () { return {}; }),
141
- DefaultDiagDistillerValidator: vi.fn().mockImplementation(function () { return {}; }),
142
- DisabledDiagnosticianRunner: vi.fn().mockImplementation(function () { return {}; }),
143
- TestDoubleRuntimeAdapter: vi.fn().mockImplementation(function () { return {}; }),
144
- OpenClawCliRuntimeAdapter: vi.fn().mockImplementation(function () { return {}; }),
145
- PiAiRuntimeAdapter: vi.fn().mockImplementation(function () { return {}; }),
146
- SPLIT_PIPELINE_TOTAL_TIMEOUT_MS: 300000,
147
- // PRI-638: capability gate — available by default; disabled cases override this.
148
- resolveDiagnosticianCapability: vi.fn((): { available: boolean; reason?: string; message?: string; nextAction?: string } => ({ available: true })),
149
- PDRuntimeError: class PDRuntimeError extends Error {
150
- constructor(public category: string, message: string) {
151
- super(message);
152
- this.name = 'PDRuntimeError';
153
- }
154
- },
155
- CandidateIntakeService: MockCandidateIntakeService,
156
- // PRI-503: admission gate mock — admit by default so existing retry/intake
157
- // tests keep their original flow. Tests that need to assert refusal behavior
158
- // can override this mock per-test.
159
- evaluateCandidateAdmissionFromRecord: vi.fn().mockReturnValue({
160
- decision: 'admitted',
161
- reason: 'mock_admitted',
162
- nextAction: 'none',
163
- evidenceStatus: 'unknown',
164
- }),
165
- resolveRuntimeConfig: mockResolveRuntimeConfig,
166
- isRuntimeConfigError: vi.fn().mockReturnValue(false),
167
- isFeatureEnabled: vi.fn().mockReturnValue(true),
168
- resolveOutputLanguage: vi.fn().mockReturnValue({ outputLanguage: 'zh-CN' }),
169
- validatePdConfig: vi.fn().mockReturnValue({ valid: true, errors: [] }),
170
- computeEffectivePdConfig: vi.fn().mockReturnValue({ config: {}, source: 'defaults', warnings: [] }),
171
- computeFeatureFlagsFromConfig: vi.fn().mockReturnValue({}),
172
- redactPdConfig: vi.fn().mockImplementation((c) => c),
173
- run: mockRun,
174
- status: vi.fn(),
175
- PrincipleTreeLedgerAdapter: MockPrincipleTreeLedgerAdapter,
176
- };
177
- });
178
-
179
- vi.mock('../../src/config-reader.js', () => ({
180
- readOutputLanguageFromWorkspace: vi.fn().mockReturnValue({ outputLanguage: 'zh-CN' }),
181
- }));
182
-
183
- vi.mock('../../src/services/resolve-runtime-from-pd-config.js', () => ({
184
- resolveRuntimeFromPdConfig: mockResolveRuntimeFromPdConfig,
185
- }));
186
-
187
- import { handlePainRetry } from '../../src/commands/pain-retry.js';
188
-
189
- // ── Test Data ──────────────────────────────────────────────────────────────────
190
-
191
- const RETRY_WAIT_TASK = {
192
- taskId: 'diagnosis_test-pain-1',
193
- taskKind: 'diagnostician',
194
- status: 'retry_wait' as const,
195
- attemptCount: 1,
196
- maxAttempts: 3,
197
- lastError: 'output_invalid',
198
- leaseOwner: null,
199
- leaseExpiresAt: null,
200
- resultRef: null,
201
- createdAt: new Date().toISOString(),
202
- updatedAt: new Date().toISOString(),
203
- };
204
-
205
- const FAILED_TASK = {
206
- taskId: 'diagnosis_test-pain-failed',
207
- taskKind: 'diagnostician',
208
- status: 'failed' as const,
209
- attemptCount: 3,
210
- maxAttempts: 3,
211
- lastError: 'timeout',
212
- leaseOwner: null,
213
- leaseExpiresAt: null,
214
- resultRef: null,
215
- createdAt: new Date().toISOString(),
216
- updatedAt: new Date().toISOString(),
217
- };
218
-
219
- const SUCCEEDED_TASK = {
220
- taskId: 'diagnosis_test-pain-succeeded',
221
- taskKind: 'diagnostician',
222
- status: 'succeeded' as const,
223
- attemptCount: 2,
224
- maxAttempts: 3,
225
- lastError: null,
226
- leaseOwner: null,
227
- leaseExpiresAt: null,
228
- resultRef: 'commit://abc',
229
- createdAt: new Date().toISOString(),
230
- updatedAt: new Date().toISOString(),
231
- };
232
-
233
- const NON_DIAGNOSTICIAN_TASK = {
234
- taskId: 'diagnosis_test-pain-wrong',
235
- taskKind: 'dreamer',
236
- status: 'failed' as const,
237
- attemptCount: 1,
238
- maxAttempts: 3,
239
- lastError: 'timeout',
240
- leaseOwner: null,
241
- leaseExpiresAt: null,
242
- resultRef: null,
243
- createdAt: new Date().toISOString(),
244
- updatedAt: new Date().toISOString(),
245
- };
246
-
247
- // ── Tests ──────────────────────────────────────────────────────────────────────
248
-
249
- describe('pd pain retry — validation and error paths', () => {
250
- beforeEach(() => {
251
- vi.clearAllMocks();
252
- mockResolveRuntimeConfig.mockReturnValue({
253
- runtimeKind: 'pi-ai',
254
- provider: 'test-provider',
255
- model: 'test-model',
256
- apiKeyEnv: 'TEST_KEY',
257
- timeoutMs: 300000,
258
- agentId: 'main',
259
- });
260
- mockResolveRuntimeFromPdConfig.mockReturnValue({
261
- result: {
262
- runtimeKind: 'pi-ai',
263
- provider: 'test-provider',
264
- model: 'test-model',
265
- apiKeyEnv: 'TEST_KEY',
266
- timeoutMs: 300000,
267
- agentId: 'main',
268
- },
269
- legacyWarnings: [],
270
- configSource: '.pd/config.yaml',
271
- configLoadResult: { ok: true, effective: {}, defaults: {}, legacyFilesDetected: [] },
272
- });
273
- mockGetTask.mockResolvedValue(null);
274
- mockGetCandidatesByTaskId.mockResolvedValue([]);
275
- mockUpdateCandidateStatus.mockResolvedValue(undefined);
276
- mockGetRunsByTask.mockResolvedValue([]);
277
- mockIntake.mockReset();
278
- mockDeadLetterGetByPainId.mockReturnValue(null);
279
- mockRun.mockResolvedValue({
280
- status: 'succeeded',
281
- taskId: 'diagnosis_test-pain-1',
282
- runId: 'run-retry-1',
283
- contextHash: 'abc123',
284
- });
285
- });
286
-
287
- it('RETRY-01: painId not found — JSON output with reason + nextAction', async () => {
288
- mockGetTask.mockResolvedValue(null);
289
-
290
- const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
291
- const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => undefined) as () => never);
292
-
293
- await handlePainRetry({
294
- painId: 'nonexistent-pain',
295
- workspace: '/tmp/fake-workspace',
296
- runtime: 'test-double',
297
- json: true,
298
- });
299
-
300
- const jsonCall = logSpy.mock.calls.find((call) => {
301
- try { JSON.parse(call[0] as string); return true; } catch { return false; }
302
- });
303
- expect(jsonCall).toBeDefined();
304
- const output = JSON.parse(jsonCall![0] as string);
305
- expect(output.status).toBe('not_found');
306
- expect(output.painId).toBe('nonexistent-pain');
307
- expect(output.reason).toContain('task_not_found');
308
- expect(output.nextAction).toBeDefined();
309
- expect(exitSpy).toHaveBeenCalledWith(1);
310
-
311
- logSpy.mockRestore();
312
- exitSpy.mockRestore();
313
- });
314
-
315
- it('RETRY-02: painId with diagnosis_ prefix — rejected with reason + nextAction', async () => {
316
- const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
317
- const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => undefined) as () => never);
318
-
319
- await handlePainRetry({
320
- painId: 'diagnosis_test-pain-1',
321
- workspace: '/tmp/fake-workspace',
322
- json: true,
323
- });
324
-
325
- // Find the JSON output (may be mixed with other logs)
326
- const jsonCall = logSpy.mock.calls.find((call) => {
327
- try { JSON.parse(call[0] as string); return true; } catch { return false; }
328
- });
329
- expect(jsonCall).toBeDefined();
330
- const output = JSON.parse(jsonCall![0] as string);
331
- expect(output.status).toBe('refused');
332
- expect(output.reason).toContain('diagnosis_');
333
- expect(output.nextAction).toContain('pd diagnose run');
334
- expect(exitSpy).toHaveBeenCalledWith(1);
335
-
336
- logSpy.mockRestore();
337
- exitSpy.mockRestore();
338
- });
339
-
340
- it('RETRY-03: task is not diagnostician — refused with reason + nextAction', async () => {
341
- mockGetTask.mockResolvedValue(NON_DIAGNOSTICIAN_TASK);
342
-
343
- const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
344
- const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => undefined) as () => never);
345
-
346
- await handlePainRetry({
347
- painId: 'test-pain-wrong',
348
- workspace: '/tmp/fake-workspace',
349
- runtime: 'test-double',
350
- json: true,
351
- });
352
-
353
- const jsonCall = logSpy.mock.calls.find((call) => {
354
- try { JSON.parse(call[0] as string); return true; } catch { return false; }
355
- });
356
- expect(jsonCall).toBeDefined();
357
- const output = JSON.parse(jsonCall![0] as string);
358
- expect(output.status).toBe('refused');
359
- expect(output.reason).toContain('wrong_task_kind');
360
- expect(output.nextAction).toBeDefined();
361
- expect(exitSpy).toHaveBeenCalledWith(1);
362
-
363
- logSpy.mockRestore();
364
- exitSpy.mockRestore();
365
- });
366
-
367
- it('RETRY-04: already succeeded without --force — refused', async () => {
368
- mockGetTask.mockResolvedValue(SUCCEEDED_TASK);
369
-
370
- const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
371
- const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => undefined) as () => never);
372
-
373
- await handlePainRetry({
374
- painId: 'test-pain-succeeded',
375
- workspace: '/tmp/fake-workspace',
376
- runtime: 'test-double',
377
- json: true,
378
- });
379
-
380
- const jsonCall = logSpy.mock.calls.find((call) => {
381
- try { JSON.parse(call[0] as string); return true; } catch { return false; }
382
- });
383
- expect(jsonCall).toBeDefined();
384
- const output = JSON.parse(jsonCall![0] as string);
385
- expect(output.status).toBe('refused');
386
- expect(output.reason).toContain('already_succeeded');
387
- expect(output.nextAction).toContain('--force');
388
- expect(exitSpy).toHaveBeenCalledWith(1);
389
-
390
- logSpy.mockRestore();
391
- exitSpy.mockRestore();
392
- });
393
-
394
- it('RETRY-05: no mutation on failed validation — run not called when task not found', async () => {
395
- mockGetTask.mockResolvedValue(null);
396
-
397
- const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => undefined) as () => never);
398
-
399
- await handlePainRetry({
400
- painId: 'nonexistent',
401
- workspace: '/tmp/fake-workspace',
402
- runtime: 'test-double',
403
- json: true,
404
- });
405
-
406
- expect(mockRun).not.toHaveBeenCalled();
407
- expect(mockGetCandidatesByTaskId).not.toHaveBeenCalled();
408
- expect(mockIntake).not.toHaveBeenCalled();
409
-
410
- exitSpy.mockRestore();
411
- });
412
-
413
- it('RETRY-05a: missing --runtime and no config — refused with reason + nextAction (JSON)', async () => {
414
- mockGetTask.mockResolvedValue(RETRY_WAIT_TASK);
415
- // PRI-393: resolveRuntimeFromPdConfig returns error → no runtime resolved
416
- mockResolveRuntimeFromPdConfig.mockReturnValueOnce({
417
- result: {
418
- reason: 'config_not_found',
419
- message: 'No .pd/config.yaml found',
420
- nextAction: 'Create .pd/config.yaml or pass --runtime',
421
- },
422
- legacyWarnings: [],
423
- configSource: '.pd/config.yaml',
424
- configLoadResult: { ok: false, effective: {}, defaults: {}, legacyFilesDetected: [] },
425
- });
426
-
427
- const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
428
- const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => undefined) as () => never);
429
-
430
- await handlePainRetry({
431
- painId: 'test-pain-1',
432
- workspace: '/tmp/fake-workspace',
433
- // No runtime specified
434
- json: true,
435
- });
436
-
437
- const jsonCall = logSpy.mock.calls.find((call) => {
438
- try { JSON.parse(call[0] as string); return true; } catch { return false; }
439
- });
440
- expect(jsonCall).toBeDefined();
441
- const output = JSON.parse(jsonCall![0] as string);
442
- expect(output.status).toBe('refused');
443
- expect(output.reason).toContain('missing_runtime');
444
- expect(output.nextAction).toContain('--runtime');
445
- expect(exitSpy).toHaveBeenCalledWith(1);
446
-
447
- logSpy.mockRestore();
448
- exitSpy.mockRestore();
449
- });
450
-
451
- it('RETRY-05b: conflicting flags --openclaw-local + --openclaw-gateway — JSON output', async () => {
452
- mockGetTask.mockResolvedValue(RETRY_WAIT_TASK);
453
-
454
- const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
455
- const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => undefined) as () => never);
456
-
457
- await handlePainRetry({
458
- painId: 'test-pain-1',
459
- workspace: '/tmp/fake-workspace',
460
- runtime: 'openclaw-cli',
461
- openclawLocal: true,
462
- openclawGateway: true,
463
- json: true,
464
- });
465
-
466
- const jsonCall = logSpy.mock.calls.find((call) => {
467
- try { JSON.parse(call[0] as string); return true; } catch { return false; }
468
- });
469
- expect(jsonCall).toBeDefined();
470
- const output = JSON.parse(jsonCall![0] as string);
471
- expect(output.status).toBe('refused');
472
- expect(output.reason).toContain('conflicting_flags');
473
- expect(output.nextAction).toBeDefined();
474
- expect(exitSpy).toHaveBeenCalledWith(1);
475
-
476
- logSpy.mockRestore();
477
- exitSpy.mockRestore();
478
- });
479
-
480
- it('RETRY-05c: blank provider/model/apiKeyEnv — refused with missing_required_config', async () => {
481
- mockGetTask.mockResolvedValue(RETRY_WAIT_TASK);
482
- // PRI-393: resolveRuntimeFromPdConfig returns config with blank strings
483
- mockResolveRuntimeFromPdConfig.mockReturnValueOnce({
484
- result: {
485
- runtimeKind: 'pi-ai',
486
- provider: '',
487
- model: ' ',
488
- apiKeyEnv: '',
489
- timeoutMs: 300000,
490
- agentId: 'main',
491
- },
492
- legacyWarnings: [],
493
- configSource: '.pd/config.yaml',
494
- configLoadResult: { ok: true, effective: {}, defaults: {}, legacyFilesDetected: [] },
495
- });
496
-
497
- const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
498
- const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => undefined) as () => never);
499
-
500
- await handlePainRetry({
501
- painId: 'test-pain-1',
502
- workspace: '/tmp/fake-workspace',
503
- runtime: 'pi-ai',
504
- json: true,
505
- });
506
-
507
- const jsonCall = logSpy.mock.calls.find((call) => {
508
- try { JSON.parse(call[0] as string); return true; } catch { return false; }
509
- });
510
- expect(jsonCall).toBeDefined();
511
- const output = JSON.parse(jsonCall![0] as string);
512
- expect(output.status).toBe('refused');
513
- expect(output.reason).toContain('missing_required_config');
514
- expect(output.reason).toContain('provider');
515
- expect(output.reason).toContain('model');
516
- expect(output.reason).toContain('apiKeyEnv');
517
- expect(output.nextAction).toBeDefined();
518
- expect(exitSpy).toHaveBeenCalledWith(1);
519
-
520
- logSpy.mockRestore();
521
- exitSpy.mockRestore();
522
- });
523
-
524
- it('DPB-09: openclaw-cli flag overrides file config mode (config=gateway, flag=local → runtimeMode=local)', async () => {
525
- mockGetTask.mockResolvedValue(RETRY_WAIT_TASK);
526
- mockResolveRuntimeFromPdConfig.mockReturnValueOnce({
527
- result: {
528
- runtimeKind: 'openclaw-cli',
529
- openclawMode: 'gateway',
530
- timeoutMs: 300000,
531
- agentId: 'main',
532
- },
533
- legacyWarnings: [],
534
- configSource: '.pd/config.yaml',
535
- configLoadResult: { ok: true, effective: {}, defaults: {}, legacyFilesDetected: [] },
536
- });
537
-
538
- const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
539
- const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => undefined) as () => never);
540
-
541
- await handlePainRetry({
542
- painId: 'test-pain-1',
543
- workspace: '/tmp/fake-workspace',
544
- runtime: 'openclaw-cli',
545
- openclawLocal: true,
546
- json: true,
547
- });
548
-
549
- // Flag override: config says gateway, flag says local → adapter gets local
550
- const OpenClawCliMock = vi.mocked(
551
- await import('@principles/core/runtime-v2').then(m => m.OpenClawCliRuntimeAdapter),
552
- );
553
- expect(OpenClawCliMock).toHaveBeenCalledWith(
554
- expect.objectContaining({ runtimeMode: 'local' }),
555
- );
556
- expect(exitSpy).not.toHaveBeenCalledWith(1);
557
-
558
- logSpy.mockRestore();
559
- exitSpy.mockRestore();
560
- });
561
-
562
- it('DPB-09: openclaw-cli flag overrides file config mode (config=local, flag=gateway → runtimeMode=gateway)', async () => {
563
- mockGetTask.mockResolvedValue(RETRY_WAIT_TASK);
564
- mockResolveRuntimeFromPdConfig.mockReturnValueOnce({
565
- result: {
566
- runtimeKind: 'openclaw-cli',
567
- openclawMode: 'local',
568
- timeoutMs: 300000,
569
- agentId: 'main',
570
- },
571
- legacyWarnings: [],
572
- configSource: '.pd/config.yaml',
573
- configLoadResult: { ok: true, effective: {}, defaults: {}, legacyFilesDetected: [] },
574
- });
575
-
576
- const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
577
- const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => undefined) as () => never);
578
-
579
- await handlePainRetry({
580
- painId: 'test-pain-1',
581
- workspace: '/tmp/fake-workspace',
582
- runtime: 'openclaw-cli',
583
- openclawGateway: true,
584
- json: true,
585
- });
586
-
587
- const OpenClawCliMock = vi.mocked(
588
- await import('@principles/core/runtime-v2').then(m => m.OpenClawCliRuntimeAdapter),
589
- );
590
- expect(OpenClawCliMock).toHaveBeenCalledWith(
591
- expect.objectContaining({ runtimeMode: 'gateway' }),
592
- );
593
- expect(exitSpy).not.toHaveBeenCalledWith(1);
594
-
595
- logSpy.mockRestore();
596
- exitSpy.mockRestore();
597
- });
598
- });
599
-
600
- describe('pd pain retry — success paths', () => {
601
- beforeEach(() => {
602
- vi.clearAllMocks();
603
- mockGetCandidatesByTaskId.mockResolvedValue([]);
604
- mockUpdateCandidateStatus.mockResolvedValue(undefined);
605
- mockGetRunsByTask.mockResolvedValue([]);
606
- mockIntake.mockReset();
607
- mockRun.mockResolvedValue({
608
- status: 'succeeded',
609
- taskId: 'diagnosis_test-pain-1',
610
- runId: 'run-retry-1',
611
- contextHash: 'abc123',
612
- });
613
- });
614
-
615
- it('RETRY-06: retry_wait + last_error → succeeded — JSON output with previousTaskStatus and previousLastError', async () => {
616
- mockGetTask.mockResolvedValue(RETRY_WAIT_TASK);
617
- mockGetCandidatesByTaskId.mockResolvedValue([
618
- { candidateId: 'cand-1', artifactId: 'art-1', taskId: 'diagnosis_test-pain-1', status: 'pending' },
619
- ]);
620
- mockIntake.mockResolvedValue({ id: 'ledger-1', title: 'Principle 1', status: 'probation' });
621
-
622
- const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
623
- const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => undefined) as () => never);
624
-
625
- await handlePainRetry({
626
- painId: 'test-pain-1',
627
- workspace: '/tmp/fake-workspace',
628
- runtime: 'test-double',
629
- json: true,
630
- });
631
-
632
- expect(logSpy).toHaveBeenCalledTimes(1);
633
- const output = JSON.parse(logSpy.mock.calls[0][0]);
634
- expect(output.status).toBe('succeeded');
635
- expect(output.painId).toBe('test-pain-1');
636
- expect(output.taskId).toBe('diagnosis_test-pain-1');
637
- expect(output.previousTaskStatus).toBe('retry_wait');
638
- expect(output.previousLastError).toBe('output_invalid');
639
- expect(output.newTaskStatus).toBe('succeeded');
640
- expect(output.candidateIds).toContain('cand-1');
641
- expect(output.nextAction).toContain('pd candidate internalize');
642
- expect(exitSpy).not.toHaveBeenCalledWith(1);
643
-
644
- logSpy.mockRestore();
645
- exitSpy.mockRestore();
646
- });
647
-
648
- it('RETRY-07: failed task → succeeded — JSON output', async () => {
649
- mockGetTask.mockResolvedValue(FAILED_TASK);
650
- mockGetCandidatesByTaskId.mockResolvedValue([]);
651
-
652
- const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
653
- const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => undefined) as () => never);
654
-
655
- await handlePainRetry({
656
- painId: 'test-pain-failed',
657
- workspace: '/tmp/fake-workspace',
658
- runtime: 'test-double',
659
- json: true,
660
- });
661
-
662
- expect(logSpy).toHaveBeenCalledTimes(1);
663
- const output = JSON.parse(logSpy.mock.calls[0][0]);
664
- expect(output.status).toBe('succeeded');
665
- expect(output.previousTaskStatus).toBe('failed');
666
- expect(output.previousLastError).toBe('timeout');
667
- expect(output.nextAction).toContain('No candidates');
668
- expect(exitSpy).not.toHaveBeenCalledWith(1);
669
-
670
- logSpy.mockRestore();
671
- exitSpy.mockRestore();
672
- });
673
-
674
- it('RETRY-08: --force allows retry of succeeded task', async () => {
675
- mockGetTask.mockResolvedValue(SUCCEEDED_TASK);
676
- mockGetCandidatesByTaskId.mockResolvedValue([]);
677
-
678
- const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
679
- const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => undefined) as () => never);
680
-
681
- await handlePainRetry({
682
- painId: 'test-pain-succeeded',
683
- workspace: '/tmp/fake-workspace',
684
- runtime: 'test-double',
685
- json: true,
686
- force: true,
687
- });
688
-
689
- expect(logSpy).toHaveBeenCalledTimes(1);
690
- const output = JSON.parse(logSpy.mock.calls[0][0]);
691
- expect(output.status).toBe('succeeded');
692
- expect(output.previousTaskStatus).toBe('succeeded');
693
- expect(exitSpy).not.toHaveBeenCalledWith(1);
694
-
695
- logSpy.mockRestore();
696
- exitSpy.mockRestore();
697
- });
698
-
699
- it('RETRY-09: --json outputs exactly one parseable JSON object', async () => {
700
- mockGetTask.mockResolvedValue(RETRY_WAIT_TASK);
701
- mockGetCandidatesByTaskId.mockResolvedValue([]);
702
-
703
- const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
704
- const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => undefined) as () => never);
705
-
706
- await handlePainRetry({
707
- painId: 'test-pain-1',
708
- workspace: '/tmp/fake-workspace',
709
- runtime: 'test-double',
710
- json: true,
711
- });
712
-
713
- expect(logSpy).toHaveBeenCalledTimes(1);
714
- const rawOutput = logSpy.mock.calls[0][0] as string;
715
- const parsed = JSON.parse(rawOutput);
716
- expect(parsed.status).toBe('succeeded');
717
- expect(parsed.painId).toBe('test-pain-1');
718
- expect(parsed.nextAction).toBeDefined();
719
- expect(exitSpy).not.toHaveBeenCalledWith(1);
720
-
721
- logSpy.mockRestore();
722
- exitSpy.mockRestore();
723
- });
724
-
725
- it('RETRY-10: nextAction mentions internalization is NOT automatic', async () => {
726
- mockGetTask.mockResolvedValue(RETRY_WAIT_TASK);
727
- mockGetCandidatesByTaskId.mockResolvedValue([
728
- { candidateId: 'cand-1', artifactId: 'art-1', taskId: 'diagnosis_test-pain-1', status: 'pending' },
729
- ]);
730
- mockIntake.mockResolvedValue({ id: 'ledger-1', title: 'Principle 1', status: 'probation' });
731
-
732
- const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
733
- const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => undefined) as () => never);
734
-
735
- await handlePainRetry({
736
- painId: 'test-pain-1',
737
- workspace: '/tmp/fake-workspace',
738
- runtime: 'test-double',
739
- json: true,
740
- });
741
-
742
- const output = JSON.parse(logSpy.mock.calls[0][0]);
743
- expect(output.nextAction).toContain('NOT started automatically');
744
- expect(output.nextAction).toContain('pd candidate internalize --candidate-id cand-1');
745
- expect(exitSpy).not.toHaveBeenCalledWith(1);
746
-
747
- logSpy.mockRestore();
748
- exitSpy.mockRestore();
749
- });
750
-
751
- it('RETRY-11: failed retry — JSON output with errorCategory + nextAction', async () => {
752
- mockGetTask.mockResolvedValue(RETRY_WAIT_TASK);
753
- mockRun.mockResolvedValueOnce({
754
- status: 'failed',
755
- taskId: 'diagnosis_test-pain-1',
756
- errorCategory: 'output_invalid',
757
- failureReason: 'LLM output failed validation',
758
- attemptCount: 2,
759
- });
760
-
761
- const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
762
- const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => undefined) as () => never);
763
-
764
- await handlePainRetry({
765
- painId: 'test-pain-1',
766
- workspace: '/tmp/fake-workspace',
767
- runtime: 'test-double',
768
- json: true,
769
- });
770
-
771
- expect(logSpy).toHaveBeenCalledTimes(1);
772
- const output = JSON.parse(logSpy.mock.calls[0][0]);
773
- expect(output.status).toBe('failed');
774
- expect(output.errorCategory).toBe('output_invalid');
775
- expect(output.nextAction).toBeDefined();
776
- expect(exitSpy).toHaveBeenCalledWith(1);
777
-
778
- logSpy.mockRestore();
779
- exitSpy.mockRestore();
780
- });
781
-
782
- it('RETRY-12: intake failure — JSON output with intake_failed + nextAction', async () => {
783
- mockGetTask.mockResolvedValue(RETRY_WAIT_TASK);
784
- mockGetCandidatesByTaskId.mockResolvedValue([
785
- { candidateId: 'cand-fail', artifactId: 'art-1', taskId: 'diagnosis_test-pain-1', status: 'pending' },
786
- ]);
787
- mockIntake.mockImplementation(() => { throw new Error('Ledger write failed'); });
788
-
789
- const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
790
- const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => undefined) as () => never);
791
-
792
- await handlePainRetry({
793
- painId: 'test-pain-1',
794
- workspace: '/tmp/fake-workspace',
795
- runtime: 'test-double',
796
- json: true,
797
- });
798
-
799
- const output = JSON.parse(logSpy.mock.calls[0][0]);
800
- expect(output.intake.candidates[0].status).toBe('intake_failed');
801
- expect(output.intake.candidates[0].nextAction).toContain('pd candidate intake');
802
- expect(exitSpy).toHaveBeenCalledWith(1);
803
-
804
- logSpy.mockRestore();
805
- exitSpy.mockRestore();
806
- });
807
- });
808
-
809
- describe('pd pain retry — human-readable output', () => {
810
- beforeEach(() => {
811
- vi.clearAllMocks();
812
- mockGetCandidatesByTaskId.mockResolvedValue([]);
813
- mockUpdateCandidateStatus.mockResolvedValue(undefined);
814
- mockGetRunsByTask.mockResolvedValue([]);
815
- mockIntake.mockReset();
816
- mockDeadLetterGetByPainId.mockReturnValue(null);
817
- mockRun.mockResolvedValue({
818
- status: 'succeeded',
819
- taskId: 'diagnosis_test-pain-1',
820
- runId: 'run-retry-1',
821
- contextHash: 'abc123',
822
- });
823
- });
824
-
825
- it('RETRY-13: not found — human-readable error with nextAction', async () => {
826
- mockGetTask.mockResolvedValue(null);
827
-
828
- const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
829
- const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => undefined) as () => never);
830
-
831
- await handlePainRetry({
832
- painId: 'nonexistent',
833
- workspace: '/tmp/fake-workspace',
834
- runtime: 'test-double',
835
- json: false,
836
- });
837
-
838
- const allErrors = errorSpy.mock.calls.map(call => call[0]).join('\n');
839
- expect(allErrors).toContain('nonexistent');
840
- expect(allErrors).toContain('nextAction');
841
- expect(exitSpy).toHaveBeenCalledWith(1);
842
-
843
- errorSpy.mockRestore();
844
- exitSpy.mockRestore();
845
- });
846
-
847
- it('RETRY-14: success — human-readable output shows previous status and nextAction', async () => {
848
- mockGetTask.mockResolvedValue(RETRY_WAIT_TASK);
849
- mockGetCandidatesByTaskId.mockResolvedValue([
850
- { candidateId: 'cand-1', artifactId: 'art-1', taskId: 'diagnosis_test-pain-1', status: 'pending' },
851
- ]);
852
- mockIntake.mockResolvedValue({ id: 'ledger-1', title: 'Principle 1', status: 'probation' });
853
-
854
- const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
855
- const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => undefined) as () => never);
856
-
857
- await handlePainRetry({
858
- painId: 'test-pain-1',
859
- workspace: '/tmp/fake-workspace',
860
- runtime: 'test-double',
861
- json: false,
862
- });
863
-
864
- const allOutput = logSpy.mock.calls.map(call => call[0]).join('\n');
865
- expect(allOutput).toContain('retry_wait');
866
- expect(allOutput).toContain('output_invalid');
867
- expect(allOutput).toContain('succeeded');
868
- expect(allOutput).toContain('pd candidate internalize');
869
- expect(exitSpy).not.toHaveBeenCalledWith(1);
870
-
871
- logSpy.mockRestore();
872
- exitSpy.mockRestore();
873
- });
874
- });
875
-
876
- describe('Commander wiring for pd pain retry', () => {
877
- function createPainRetryProgram(): { program: Command; capturedOpts: Record<string, unknown> } {
878
- const program = new Command();
879
- program.exitOverride();
880
- const capturedOpts: Record<string, unknown> = {};
881
-
882
- program
883
- .command('pain')
884
- .command('retry')
885
- .requiredOption('-p, --pain-id <painId>', 'Pain ID')
886
- .option('-w, --workspace <path>', 'Workspace directory')
887
- .option('-r, --runtime <kind>', 'Runtime kind')
888
- .option('--openclaw-local', 'Use local OpenClaw')
889
- .option('--openclaw-gateway', 'Use gateway OpenClaw')
890
- .option('-a, --agent <agentId>', 'Agent ID')
891
- .option('--provider <name>', 'LLM provider')
892
- .option('--model <id>', 'Model ID')
893
- .option('--apiKeyEnv <name>', 'API key env var')
894
- .option('--baseUrl <url>', 'Custom base URL')
895
- .option('--maxRetries <n>', 'Max retries', parseInt)
896
- .option('--timeoutMs <ms>', 'Timeout ms', parseInt)
897
- .option('--force', 'Force retry of succeeded task')
898
- .option('--json', 'Output raw JSON')
899
- .action(async (opts) => {
900
- Object.assign(capturedOpts, opts);
901
- });
902
-
903
- return { program, capturedOpts };
904
- }
905
-
906
- it('CMD-01: --pain-id is required, missing → error', async () => {
907
- const { program } = createPainRetryProgram();
908
- await expect(
909
- program.parseAsync(['node', 'pd', 'pain', 'retry', '--json'])
910
- ).rejects.toThrow();
911
- });
912
-
913
- it('CMD-02: --pain-id sets opts.painId', async () => {
914
- const { program, capturedOpts } = createPainRetryProgram();
915
- await program.parseAsync(['node', 'pd', 'pain', 'retry', '--pain-id', 'abc123']);
916
- expect(capturedOpts.painId).toBe('abc123');
917
- });
918
-
919
- it('CMD-03: -p short form sets opts.painId', async () => {
920
- const { program, capturedOpts } = createPainRetryProgram();
921
- await program.parseAsync(['node', 'pd', 'pain', 'retry', '-p', 'abc123']);
922
- expect(capturedOpts.painId).toBe('abc123');
923
- });
924
-
925
- it('CMD-04: --force sets opts.force === true', async () => {
926
- const { program, capturedOpts } = createPainRetryProgram();
927
- await program.parseAsync(['node', 'pd', 'pain', 'retry', '--pain-id', 'abc', '--force']);
928
- expect(capturedOpts.force).toBe(true);
929
- });
930
-
931
- it('CMD-05: default (no --force) → opts.force === undefined', async () => {
932
- const { program, capturedOpts } = createPainRetryProgram();
933
- await program.parseAsync(['node', 'pd', 'pain', 'retry', '--pain-id', 'abc']);
934
- expect(capturedOpts.force).toBeUndefined();
935
- });
936
-
937
- it('CMD-06: --json sets opts.json === true', async () => {
938
- const { program, capturedOpts } = createPainRetryProgram();
939
- await program.parseAsync(['node', 'pd', 'pain', 'retry', '--pain-id', 'abc', '--json']);
940
- expect(capturedOpts.json).toBe(true);
941
- });
942
-
943
- it('CMD-07: --runtime sets opts.runtime', async () => {
944
- const { program, capturedOpts } = createPainRetryProgram();
945
- await program.parseAsync(['node', 'pd', 'pain', 'retry', '--pain-id', 'abc', '--runtime', 'pi-ai']);
946
- expect(capturedOpts.runtime).toBe('pi-ai');
947
- });
948
-
949
- // REGRESSION: PRI-337 — all options must route through pain retry
950
- it('CMD-08: --baseUrl sets opts.baseUrl', async () => {
951
- const { program, capturedOpts } = createPainRetryProgram();
952
- await program.parseAsync(['node', 'pd', 'pain', 'retry', '--pain-id', 'abc', '--baseUrl', 'https://custom.api.com']);
953
- expect(capturedOpts.baseUrl).toBe('https://custom.api.com');
954
- });
955
-
956
- it('CMD-09: --maxRetries sets opts.maxRetries as number', async () => {
957
- const { program, capturedOpts } = createPainRetryProgram();
958
- await program.parseAsync(['node', 'pd', 'pain', 'retry', '--pain-id', 'abc', '--maxRetries', '5']);
959
- expect(capturedOpts.maxRetries).toBe(5);
960
- });
961
-
962
- it('CMD-10: --timeoutMs sets opts.timeoutMs as number', async () => {
963
- const { program, capturedOpts } = createPainRetryProgram();
964
- await program.parseAsync(['node', 'pd', 'pain', 'retry', '--pain-id', 'abc', '--timeoutMs', '60000']);
965
- expect(capturedOpts.timeoutMs).toBe(60000);
966
- });
967
-
968
- it('CMD-11: --provider sets opts.provider', async () => {
969
- const { program, capturedOpts } = createPainRetryProgram();
970
- await program.parseAsync(['node', 'pd', 'pain', 'retry', '--pain-id', 'abc', '--provider', 'openrouter']);
971
- expect(capturedOpts.provider).toBe('openrouter');
972
- });
973
-
974
- it('CMD-12: --model sets opts.model', async () => {
975
- const { program, capturedOpts } = createPainRetryProgram();
976
- await program.parseAsync(['node', 'pd', 'pain', 'retry', '--pain-id', 'abc', '--model', 'anthropic/claude-sonnet-4']);
977
- expect(capturedOpts.model).toBe('anthropic/claude-sonnet-4');
978
- });
979
-
980
- it('CMD-13: --apiKeyEnv sets opts.apiKeyEnv', async () => {
981
- const { program, capturedOpts } = createPainRetryProgram();
982
- await program.parseAsync(['node', 'pd', 'pain', 'retry', '--pain-id', 'abc', '--apiKeyEnv', 'OPENROUTER_KEY']);
983
- expect(capturedOpts.apiKeyEnv).toBe('OPENROUTER_KEY');
984
- });
985
-
986
- it('CMD-14: -a (--agent) sets opts.agent', async () => {
987
- const { program, capturedOpts } = createPainRetryProgram();
988
- await program.parseAsync(['node', 'pd', 'pain', 'retry', '--pain-id', 'abc', '-a', 'main']);
989
- expect(capturedOpts.agent).toBe('main');
990
- });
991
-
992
- it('CMD-15: --openclaw-local sets opts.openclawLocal === true', async () => {
993
- const { program, capturedOpts } = createPainRetryProgram();
994
- await program.parseAsync(['node', 'pd', 'pain', 'retry', '--pain-id', 'abc', '--openclaw-local']);
995
- expect(capturedOpts.openclawLocal).toBe(true);
996
- });
997
-
998
- it('CMD-16: --openclaw-gateway sets opts.openclawGateway === true', async () => {
999
- const { program, capturedOpts } = createPainRetryProgram();
1000
- await program.parseAsync(['node', 'pd', 'pain', 'retry', '--pain-id', 'abc', '--openclaw-gateway']);
1001
- expect(capturedOpts.openclawGateway).toBe(true);
1002
- });
1003
- });
1004
-
1005
- // BUG-1 (PRI-442): effectiveConfig must be passed to split-pipeline runners
1006
- // so that ADR-0019 LLM rate-limit degradation (isDegradationEnabled) can fire.
1007
- // EP-02: production path wiring.
1008
- describe('BUG-1 (PRI-442): pain retry — effectiveConfig wiring to split-pipeline runners', () => {
1009
- beforeEach(() => {
1010
- vi.clearAllMocks();
1011
- mockGetCandidatesByTaskId.mockResolvedValue([]);
1012
- mockUpdateCandidateStatus.mockResolvedValue(undefined);
1013
- mockGetRunsByTask.mockResolvedValue([]);
1014
- mockIntake.mockReset();
1015
- mockRun.mockResolvedValue({
1016
- status: 'succeeded',
1017
- taskId: 'diagnosis_test-pain-1',
1018
- runId: 'run-retry-1',
1019
- contextHash: 'abc123',
1020
- });
1021
- });
1022
-
1023
- it('passes effectiveConfig to all three runners when split pipeline is enabled', async () => {
1024
- mockGetTask.mockResolvedValue(RETRY_WAIT_TASK);
1025
-
1026
- const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
1027
- const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => undefined) as () => never);
1028
-
1029
- await handlePainRetry({
1030
- painId: 'test-pain-1',
1031
- workspace: '/tmp/fake-workspace',
1032
- runtime: 'test-double',
1033
- json: true,
1034
- });
1035
-
1036
- expect(diagRootCauseRunnerCtor).toHaveBeenCalled();
1037
- const rootOptions = diagRootCauseRunnerCtor.mock.calls[0]?.[1];
1038
- expect(rootOptions?.effectiveConfig).toBeDefined();
1039
- expect(rootOptions?.effectiveConfig).toEqual(
1040
- expect.objectContaining({ config: expect.anything(), source: 'file' }),
1041
- );
1042
-
1043
- const distillerOptions = diagDistillerRunnerCtor.mock.calls[0]?.[1];
1044
- expect(distillerOptions?.effectiveConfig).toBeDefined();
1045
-
1046
- const routerOptions = diagRouterRunnerCtor.mock.calls[0]?.[1];
1047
- expect(routerOptions?.effectiveConfig).toBeDefined();
1048
-
1049
- logSpy.mockRestore();
1050
- exitSpy.mockRestore();
1051
- });
1052
- });
1053
-
1054
- // ── PRI-638: unified capability-disabled semantics ───────────────────────────
1055
- //
1056
- // On main, an Owner-disabled Diagnostician surfaced from `pd pain retry` as
1057
- // `missing_runtime` ("no .pd/config.yaml runtime binding found") — telling the
1058
- // Owner their config was broken when they had deliberately switched the agent
1059
- // off. The capability gate now runs BEFORE runtime resolution and reads the
1060
- // same canonical authority the runtime factory uses.
1061
-
1062
- describe('PRI-638: pd pain retry when Diagnostician capability is disabled', () => {
1063
- beforeEach(async () => {
1064
- vi.clearAllMocks();
1065
- const runtimeV2 = await import('@principles/core/runtime-v2');
1066
- vi.mocked(runtimeV2.resolveDiagnosticianCapability).mockReturnValue({
1067
- available: false,
1068
- reason: 'capability_disabled',
1069
- message: "Agent 'diagnostician' is disabled",
1070
- nextAction: "Enable agent 'diagnostician' in .pd/config.yaml internalAgents.agents.diagnostician.enabled",
1071
- });
1072
- });
1073
-
1074
- afterEach(async () => {
1075
- const runtimeV2 = await import('@principles/core/runtime-v2');
1076
- vi.mocked(runtimeV2.resolveDiagnosticianCapability).mockReset();
1077
- });
1078
-
1079
- it('RETRY-638-01: --json refuses with capability_disabled, not missing_runtime', async () => {
1080
- const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
1081
- const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => undefined) as () => never);
1082
-
1083
- await handlePainRetry({
1084
- painId: 'pain-638',
1085
- workspace: '/tmp/fake-workspace',
1086
- json: true,
1087
- });
1088
-
1089
- const jsonCall = logSpy.mock.calls.find((call) => String(call[0]).trim().startsWith('{'));
1090
- expect(jsonCall).toBeDefined();
1091
- const parsed = JSON.parse(String(jsonCall?.[0]));
1092
- expect(parsed.reason).toBe('capability_disabled');
1093
- expect(parsed.reason).not.toBe('missing_runtime');
1094
- expect(parsed.nextAction).toContain('internalAgents.agents.diagnostician.enabled');
1095
- expect(exitSpy).toHaveBeenCalledWith(1);
1096
-
1097
- logSpy.mockRestore();
1098
- exitSpy.mockRestore();
1099
- });
1100
-
1101
- it('RETRY-638-02: capability gate fires before the runtime adapter is built', async () => {
1102
- const errSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
1103
- const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => undefined) as () => never);
1104
-
1105
- await handlePainRetry({
1106
- painId: 'pain-638',
1107
- workspace: '/tmp/fake-workspace',
1108
- runtime: 'test-double',
1109
- json: false,
1110
- });
1111
-
1112
- const out = errSpy.mock.calls.map((c) => String(c[0])).join('\n');
1113
- expect(out).toContain('capability_disabled');
1114
- const runtimeV2 = await import('@principles/core/runtime-v2');
1115
- expect(runtimeV2.TestDoubleRuntimeAdapter).not.toHaveBeenCalled();
1116
- expect(runtimeV2.SplitDiagnosticianRunner).not.toHaveBeenCalled();
1117
-
1118
- errSpy.mockRestore();
1119
- exitSpy.mockRestore();
1120
- });
1121
- });