@principles/pd-cli 1.152.12 → 1.152.13

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