@principles/core 1.123.0 → 1.125.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (231) hide show
  1. package/dist/runtime-v2/__tests__/architecture-regression.test.js +42 -74
  2. package/dist/runtime-v2/__tests__/architecture-regression.test.js.map +1 -1
  3. package/dist/runtime-v2/__tests__/attack-e2e-pipeline-smoke.test.js +115 -21
  4. package/dist/runtime-v2/__tests__/attack-e2e-pipeline-smoke.test.js.map +1 -1
  5. package/dist/runtime-v2/__tests__/golden-path-diagnostician-e2e.test.js.map +1 -1
  6. package/dist/runtime-v2/__tests__/pain-signal-bridge-retried.test.js +7 -6
  7. package/dist/runtime-v2/__tests__/pain-signal-bridge-retried.test.js.map +1 -1
  8. package/dist/runtime-v2/__tests__/pain-signal-bridge-short-circuit.test.js.map +1 -1
  9. package/dist/runtime-v2/__tests__/pain-signal-bridge-workspace-dir.test.js.map +1 -1
  10. package/dist/runtime-v2/adapter/pi-ai-runtime-adapter.d.ts.map +1 -1
  11. package/dist/runtime-v2/adapter/pi-ai-runtime-adapter.js +4 -0
  12. package/dist/runtime-v2/adapter/pi-ai-runtime-adapter.js.map +1 -1
  13. package/dist/runtime-v2/cli/diagnose.d.ts +2 -2
  14. package/dist/runtime-v2/cli/diagnose.d.ts.map +1 -1
  15. package/dist/runtime-v2/config/__tests__/pd-config-contract.test.js +3 -3
  16. package/dist/runtime-v2/config/__tests__/pd-config-contract.test.js.map +1 -1
  17. package/dist/runtime-v2/config/pd-config-defaults.js +3 -3
  18. package/dist/runtime-v2/config/pd-config-defaults.js.map +1 -1
  19. package/dist/runtime-v2/config/pd-config-types.d.ts +2 -0
  20. package/dist/runtime-v2/config/pd-config-types.d.ts.map +1 -1
  21. package/dist/runtime-v2/config/pd-config-types.js.map +1 -1
  22. package/dist/runtime-v2/diagnostician/__tests__/diag-distiller-output.test.d.ts +2 -0
  23. package/dist/runtime-v2/diagnostician/__tests__/diag-distiller-output.test.d.ts.map +1 -0
  24. package/dist/runtime-v2/diagnostician/__tests__/diag-distiller-output.test.js +45 -0
  25. package/dist/runtime-v2/diagnostician/__tests__/diag-distiller-output.test.js.map +1 -0
  26. package/dist/runtime-v2/diagnostician/__tests__/diag-rootcause-output.test.d.ts +2 -0
  27. package/dist/runtime-v2/diagnostician/__tests__/diag-rootcause-output.test.d.ts.map +1 -0
  28. package/dist/runtime-v2/diagnostician/__tests__/diag-rootcause-output.test.js +57 -0
  29. package/dist/runtime-v2/diagnostician/__tests__/diag-rootcause-output.test.js.map +1 -0
  30. package/dist/runtime-v2/diagnostician/__tests__/distiller-prompt-builder.test.d.ts +2 -0
  31. package/dist/runtime-v2/diagnostician/__tests__/distiller-prompt-builder.test.d.ts.map +1 -0
  32. package/dist/runtime-v2/diagnostician/__tests__/distiller-prompt-builder.test.js +39 -0
  33. package/dist/runtime-v2/diagnostician/__tests__/distiller-prompt-builder.test.js.map +1 -0
  34. package/dist/runtime-v2/diagnostician/__tests__/rootcause-prompt-builder.test.d.ts +2 -0
  35. package/dist/runtime-v2/diagnostician/__tests__/rootcause-prompt-builder.test.d.ts.map +1 -0
  36. package/dist/runtime-v2/diagnostician/__tests__/rootcause-prompt-builder.test.js +34 -0
  37. package/dist/runtime-v2/diagnostician/__tests__/rootcause-prompt-builder.test.js.map +1 -0
  38. package/dist/runtime-v2/diagnostician/__tests__/router-prompt-builder.test.d.ts +2 -0
  39. package/dist/runtime-v2/diagnostician/__tests__/router-prompt-builder.test.d.ts.map +1 -0
  40. package/dist/runtime-v2/diagnostician/__tests__/router-prompt-builder.test.js +57 -0
  41. package/dist/runtime-v2/diagnostician/__tests__/router-prompt-builder.test.js.map +1 -0
  42. package/dist/runtime-v2/diagnostician/diag-distiller-output.d.ts +80 -0
  43. package/dist/runtime-v2/diagnostician/diag-distiller-output.d.ts.map +1 -0
  44. package/dist/runtime-v2/diagnostician/diag-distiller-output.js +103 -0
  45. package/dist/runtime-v2/diagnostician/diag-distiller-output.js.map +1 -0
  46. package/dist/runtime-v2/diagnostician/diag-rootcause-output.d.ts +115 -0
  47. package/dist/runtime-v2/diagnostician/diag-rootcause-output.d.ts.map +1 -0
  48. package/dist/runtime-v2/diagnostician/diag-rootcause-output.js +166 -0
  49. package/dist/runtime-v2/diagnostician/diag-rootcause-output.js.map +1 -0
  50. package/dist/runtime-v2/diagnostician/distiller-prompt-builder.d.ts +134 -0
  51. package/dist/runtime-v2/diagnostician/distiller-prompt-builder.d.ts.map +1 -0
  52. package/dist/runtime-v2/diagnostician/distiller-prompt-builder.js +156 -0
  53. package/dist/runtime-v2/diagnostician/distiller-prompt-builder.js.map +1 -0
  54. package/dist/runtime-v2/diagnostician/rootcause-prompt-builder.d.ts +96 -0
  55. package/dist/runtime-v2/diagnostician/rootcause-prompt-builder.d.ts.map +1 -0
  56. package/dist/runtime-v2/diagnostician/rootcause-prompt-builder.js +216 -0
  57. package/dist/runtime-v2/diagnostician/rootcause-prompt-builder.js.map +1 -0
  58. package/dist/runtime-v2/diagnostician/router-prompt-builder.d.ts +127 -0
  59. package/dist/runtime-v2/diagnostician/router-prompt-builder.d.ts.map +1 -0
  60. package/dist/runtime-v2/diagnostician/router-prompt-builder.js +131 -0
  61. package/dist/runtime-v2/diagnostician/router-prompt-builder.js.map +1 -0
  62. package/dist/runtime-v2/diagnostician-prompt-builder.d.ts +1 -64
  63. package/dist/runtime-v2/diagnostician-prompt-builder.d.ts.map +1 -1
  64. package/dist/runtime-v2/diagnostician-prompt-builder.js +0 -186
  65. package/dist/runtime-v2/diagnostician-prompt-builder.js.map +1 -1
  66. package/dist/runtime-v2/feature-flags/__tests__/feature-flag-contract.test.js +13 -7
  67. package/dist/runtime-v2/feature-flags/__tests__/feature-flag-contract.test.js.map +1 -1
  68. package/dist/runtime-v2/feature-flags/feature-flag-contract.js +3 -3
  69. package/dist/runtime-v2/feature-flags/feature-flag-contract.js.map +1 -1
  70. package/dist/runtime-v2/index.d.ts +14 -10
  71. package/dist/runtime-v2/index.d.ts.map +1 -1
  72. package/dist/runtime-v2/index.js +9 -6
  73. package/dist/runtime-v2/index.js.map +1 -1
  74. package/dist/runtime-v2/internalization/__tests__/__fixtures__/split-pipeline-mock-outputs.d.ts +25 -0
  75. package/dist/runtime-v2/internalization/__tests__/__fixtures__/split-pipeline-mock-outputs.d.ts.map +1 -0
  76. package/dist/runtime-v2/internalization/__tests__/__fixtures__/split-pipeline-mock-outputs.js +123 -0
  77. package/dist/runtime-v2/internalization/__tests__/__fixtures__/split-pipeline-mock-outputs.js.map +1 -0
  78. package/dist/runtime-v2/internalization/__tests__/diag-chain-e2e.test.d.ts +2 -0
  79. package/dist/runtime-v2/internalization/__tests__/diag-chain-e2e.test.d.ts.map +1 -0
  80. package/dist/runtime-v2/internalization/__tests__/diag-chain-e2e.test.js +734 -0
  81. package/dist/runtime-v2/internalization/__tests__/diag-chain-e2e.test.js.map +1 -0
  82. package/dist/runtime-v2/internalization/__tests__/diag-distiller-runner.test.d.ts +2 -0
  83. package/dist/runtime-v2/internalization/__tests__/diag-distiller-runner.test.d.ts.map +1 -0
  84. package/dist/runtime-v2/internalization/__tests__/diag-distiller-runner.test.js +437 -0
  85. package/dist/runtime-v2/internalization/__tests__/diag-distiller-runner.test.js.map +1 -0
  86. package/dist/runtime-v2/internalization/__tests__/diag-rootcause-runner.test.d.ts +2 -0
  87. package/dist/runtime-v2/internalization/__tests__/diag-rootcause-runner.test.d.ts.map +1 -0
  88. package/dist/runtime-v2/internalization/__tests__/diag-rootcause-runner.test.js +336 -0
  89. package/dist/runtime-v2/internalization/__tests__/diag-rootcause-runner.test.js.map +1 -0
  90. package/dist/runtime-v2/internalization/__tests__/diag-router-runner.test.d.ts +2 -0
  91. package/dist/runtime-v2/internalization/__tests__/diag-router-runner.test.d.ts.map +1 -0
  92. package/dist/runtime-v2/internalization/__tests__/diag-router-runner.test.js +437 -0
  93. package/dist/runtime-v2/internalization/__tests__/diag-router-runner.test.js.map +1 -0
  94. package/dist/runtime-v2/internalization/__tests__/runnerkind-seam.test.js +3 -3
  95. package/dist/runtime-v2/internalization/__tests__/runnerkind-seam.test.js.map +1 -1
  96. package/dist/runtime-v2/internalization/diag-distiller-runner.d.ts +83 -0
  97. package/dist/runtime-v2/internalization/diag-distiller-runner.d.ts.map +1 -0
  98. package/dist/runtime-v2/internalization/diag-distiller-runner.js +274 -0
  99. package/dist/runtime-v2/internalization/diag-distiller-runner.js.map +1 -0
  100. package/dist/runtime-v2/internalization/diag-rootcause-runner.d.ts +73 -0
  101. package/dist/runtime-v2/internalization/diag-rootcause-runner.d.ts.map +1 -0
  102. package/dist/runtime-v2/internalization/diag-rootcause-runner.js +229 -0
  103. package/dist/runtime-v2/internalization/diag-rootcause-runner.js.map +1 -0
  104. package/dist/runtime-v2/internalization/diag-router-runner.d.ts +85 -0
  105. package/dist/runtime-v2/internalization/diag-router-runner.d.ts.map +1 -0
  106. package/dist/runtime-v2/internalization/diag-router-runner.js +353 -0
  107. package/dist/runtime-v2/internalization/diag-router-runner.js.map +1 -0
  108. package/dist/runtime-v2/internalization/evaluator-runner.d.ts +1 -1
  109. package/dist/runtime-v2/internalization/evaluator-runner.d.ts.map +1 -1
  110. package/dist/runtime-v2/internalization/evaluator-runner.js +1 -1
  111. package/dist/runtime-v2/internalization/evaluator-runner.js.map +1 -1
  112. package/dist/runtime-v2/internalization/split-diagnostician-runner.d.ts +66 -0
  113. package/dist/runtime-v2/internalization/split-diagnostician-runner.d.ts.map +1 -0
  114. package/dist/runtime-v2/internalization/split-diagnostician-runner.js +241 -0
  115. package/dist/runtime-v2/internalization/split-diagnostician-runner.js.map +1 -0
  116. package/dist/runtime-v2/observer/__tests__/empathy-observer.real-e2e.test.js +28 -21
  117. package/dist/runtime-v2/observer/__tests__/empathy-observer.real-e2e.test.js.map +1 -1
  118. package/dist/runtime-v2/pain-signal-bridge.d.ts +22 -3
  119. package/dist/runtime-v2/pain-signal-bridge.d.ts.map +1 -1
  120. package/dist/runtime-v2/pain-signal-bridge.js +16 -7
  121. package/dist/runtime-v2/pain-signal-bridge.js.map +1 -1
  122. package/dist/runtime-v2/pain-signal-runtime-factory.d.ts +13 -1
  123. package/dist/runtime-v2/pain-signal-runtime-factory.d.ts.map +1 -1
  124. package/dist/runtime-v2/pain-signal-runtime-factory.js +64 -34
  125. package/dist/runtime-v2/pain-signal-runtime-factory.js.map +1 -1
  126. package/dist/runtime-v2/runner/__tests__/base-peer-runner-trust-boundary.test.js +2 -2
  127. package/dist/runtime-v2/runner/__tests__/base-peer-runner-trust-boundary.test.js.map +1 -1
  128. package/dist/runtime-v2/runner/__tests__/diagnose.test.js.map +1 -1
  129. package/dist/runtime-v2/runner/base-peer-runner.d.ts +3 -3
  130. package/dist/runtime-v2/runner/base-peer-runner.d.ts.map +1 -1
  131. package/dist/runtime-v2/runner/base-peer-runner.js +6 -6
  132. package/dist/runtime-v2/runner/base-peer-runner.js.map +1 -1
  133. package/dist/runtime-v2/runner/peer-runner-types.d.ts +3 -3
  134. package/dist/runtime-v2/runner/peer-runner-types.d.ts.map +1 -1
  135. package/dist/runtime-v2/store/candidate/sqlite-candidate-store.js +2 -2
  136. package/dist/runtime-v2/store/candidate/sqlite-candidate-store.js.map +1 -1
  137. package/dist/runtime-v2/types/principle-enums.d.ts +2 -2
  138. package/dist/runtime-v2/types/principle-enums.d.ts.map +1 -1
  139. package/dist/runtime-v2/types/principle-enums.js +1 -0
  140. package/dist/runtime-v2/types/principle-enums.js.map +1 -1
  141. package/dist/runtime-v2/types/principle-schema.d.ts +1 -1
  142. package/dist/runtime-v2/types/principle-tree-store.d.ts +1 -1
  143. package/dist/telemetry-event.d.ts +2 -2
  144. package/dist/telemetry-event.d.ts.map +1 -1
  145. package/dist/telemetry-event.js +1 -0
  146. package/dist/telemetry-event.js.map +1 -1
  147. package/package.json +1 -1
  148. package/dist/runtime-v2/__tests__/diagnostician-core-grounding.test.d.ts +0 -2
  149. package/dist/runtime-v2/__tests__/diagnostician-core-grounding.test.d.ts.map +0 -1
  150. package/dist/runtime-v2/__tests__/diagnostician-core-grounding.test.js +0 -122
  151. package/dist/runtime-v2/__tests__/diagnostician-core-grounding.test.js.map +0 -1
  152. package/dist/runtime-v2/diagnostician/__tests__/diagnostician-prompt-builder.integration.test.d.ts +0 -2
  153. package/dist/runtime-v2/diagnostician/__tests__/diagnostician-prompt-builder.integration.test.d.ts.map +0 -1
  154. package/dist/runtime-v2/diagnostician/__tests__/diagnostician-prompt-builder.integration.test.js +0 -169
  155. package/dist/runtime-v2/diagnostician/__tests__/diagnostician-prompt-builder.integration.test.js.map +0 -1
  156. package/dist/runtime-v2/diagnostician/__tests__/diagnostician-prompt-builder.test.d.ts +0 -2
  157. package/dist/runtime-v2/diagnostician/__tests__/diagnostician-prompt-builder.test.d.ts.map +0 -1
  158. package/dist/runtime-v2/diagnostician/__tests__/diagnostician-prompt-builder.test.js +0 -462
  159. package/dist/runtime-v2/diagnostician/__tests__/diagnostician-prompt-builder.test.js.map +0 -1
  160. package/dist/runtime-v2/diagnostician/__tests__/diagnostician-prompt-language.test.d.ts +0 -13
  161. package/dist/runtime-v2/diagnostician/__tests__/diagnostician-prompt-language.test.d.ts.map +0 -1
  162. package/dist/runtime-v2/diagnostician/__tests__/diagnostician-prompt-language.test.js +0 -97
  163. package/dist/runtime-v2/diagnostician/__tests__/diagnostician-prompt-language.test.js.map +0 -1
  164. package/dist/runtime-v2/runner/__tests__/diagnostician-runner.integration.test.d.ts +0 -2
  165. package/dist/runtime-v2/runner/__tests__/diagnostician-runner.integration.test.d.ts.map +0 -1
  166. package/dist/runtime-v2/runner/__tests__/diagnostician-runner.integration.test.js +0 -378
  167. package/dist/runtime-v2/runner/__tests__/diagnostician-runner.integration.test.js.map +0 -1
  168. package/dist/runtime-v2/runner/__tests__/diagnostician-runner.test.d.ts +0 -2
  169. package/dist/runtime-v2/runner/__tests__/diagnostician-runner.test.d.ts.map +0 -1
  170. package/dist/runtime-v2/runner/__tests__/diagnostician-runner.test.js +0 -682
  171. package/dist/runtime-v2/runner/__tests__/diagnostician-runner.test.js.map +0 -1
  172. package/dist/runtime-v2/runner/__tests__/diagnostician-telemetry.test.d.ts +0 -2
  173. package/dist/runtime-v2/runner/__tests__/diagnostician-telemetry.test.d.ts.map +0 -1
  174. package/dist/runtime-v2/runner/__tests__/diagnostician-telemetry.test.js +0 -286
  175. package/dist/runtime-v2/runner/__tests__/diagnostician-telemetry.test.js.map +0 -1
  176. package/dist/runtime-v2/runner/__tests__/dual-track-e2e.test.d.ts +0 -2
  177. package/dist/runtime-v2/runner/__tests__/dual-track-e2e.test.d.ts.map +0 -1
  178. package/dist/runtime-v2/runner/__tests__/dual-track-e2e.test.js +0 -320
  179. package/dist/runtime-v2/runner/__tests__/dual-track-e2e.test.js.map +0 -1
  180. package/dist/runtime-v2/runner/__tests__/lease-expiration-recovery.integration.test.d.ts +0 -2
  181. package/dist/runtime-v2/runner/__tests__/lease-expiration-recovery.integration.test.d.ts.map +0 -1
  182. package/dist/runtime-v2/runner/__tests__/lease-expiration-recovery.integration.test.js +0 -261
  183. package/dist/runtime-v2/runner/__tests__/lease-expiration-recovery.integration.test.js.map +0 -1
  184. package/dist/runtime-v2/runner/__tests__/m5-05-e2e.test.d.ts +0 -2
  185. package/dist/runtime-v2/runner/__tests__/m5-05-e2e.test.d.ts.map +0 -1
  186. package/dist/runtime-v2/runner/__tests__/m5-05-e2e.test.js +0 -405
  187. package/dist/runtime-v2/runner/__tests__/m5-05-e2e.test.js.map +0 -1
  188. package/dist/runtime-v2/runner/__tests__/m6-06-e2e.test.d.ts +0 -2
  189. package/dist/runtime-v2/runner/__tests__/m6-06-e2e.test.d.ts.map +0 -1
  190. package/dist/runtime-v2/runner/__tests__/m6-06-e2e.test.js +0 -347
  191. package/dist/runtime-v2/runner/__tests__/m6-06-e2e.test.js.map +0 -1
  192. package/dist/runtime-v2/runner/__tests__/m6-06-legacy.test.d.ts +0 -2
  193. package/dist/runtime-v2/runner/__tests__/m6-06-legacy.test.d.ts.map +0 -1
  194. package/dist/runtime-v2/runner/__tests__/m6-06-legacy.test.js +0 -186
  195. package/dist/runtime-v2/runner/__tests__/m6-06-legacy.test.js.map +0 -1
  196. package/dist/runtime-v2/runner/__tests__/m6-06-real-path.test.d.ts +0 -2
  197. package/dist/runtime-v2/runner/__tests__/m6-06-real-path.test.d.ts.map +0 -1
  198. package/dist/runtime-v2/runner/__tests__/m6-06-real-path.test.js +0 -355
  199. package/dist/runtime-v2/runner/__tests__/m6-06-real-path.test.js.map +0 -1
  200. package/dist/runtime-v2/runner/__tests__/m8-02-e2e.test.d.ts +0 -2
  201. package/dist/runtime-v2/runner/__tests__/m8-02-e2e.test.d.ts.map +0 -1
  202. package/dist/runtime-v2/runner/__tests__/m8-02-e2e.test.js +0 -486
  203. package/dist/runtime-v2/runner/__tests__/m8-02-e2e.test.js.map +0 -1
  204. package/dist/runtime-v2/runner/__tests__/m9-adapter-integration.test.d.ts +0 -2
  205. package/dist/runtime-v2/runner/__tests__/m9-adapter-integration.test.d.ts.map +0 -1
  206. package/dist/runtime-v2/runner/__tests__/m9-adapter-integration.test.js +0 -171
  207. package/dist/runtime-v2/runner/__tests__/m9-adapter-integration.test.js.map +0 -1
  208. package/dist/runtime-v2/runner/__tests__/m9-e2e.test.d.ts +0 -2
  209. package/dist/runtime-v2/runner/__tests__/m9-e2e.test.d.ts.map +0 -1
  210. package/dist/runtime-v2/runner/__tests__/m9-e2e.test.js +0 -175
  211. package/dist/runtime-v2/runner/__tests__/m9-e2e.test.js.map +0 -1
  212. package/dist/runtime-v2/runner/__tests__/max-attempts-exceeded.integration.test.d.ts +0 -2
  213. package/dist/runtime-v2/runner/__tests__/max-attempts-exceeded.integration.test.d.ts.map +0 -1
  214. package/dist/runtime-v2/runner/__tests__/max-attempts-exceeded.integration.test.js +0 -276
  215. package/dist/runtime-v2/runner/__tests__/max-attempts-exceeded.integration.test.js.map +0 -1
  216. package/dist/runtime-v2/runner/__tests__/retry-wait-recovery.integration.test.d.ts +0 -2
  217. package/dist/runtime-v2/runner/__tests__/retry-wait-recovery.integration.test.d.ts.map +0 -1
  218. package/dist/runtime-v2/runner/__tests__/retry-wait-recovery.integration.test.js +0 -272
  219. package/dist/runtime-v2/runner/__tests__/retry-wait-recovery.integration.test.js.map +0 -1
  220. package/dist/runtime-v2/runner/__tests__/start-run-input.test.d.ts +0 -2
  221. package/dist/runtime-v2/runner/__tests__/start-run-input.test.d.ts.map +0 -1
  222. package/dist/runtime-v2/runner/__tests__/start-run-input.test.js +0 -67
  223. package/dist/runtime-v2/runner/__tests__/start-run-input.test.js.map +0 -1
  224. package/dist/runtime-v2/runner/diagnostician-runner-options.d.ts +0 -57
  225. package/dist/runtime-v2/runner/diagnostician-runner-options.d.ts.map +0 -1
  226. package/dist/runtime-v2/runner/diagnostician-runner-options.js +0 -21
  227. package/dist/runtime-v2/runner/diagnostician-runner-options.js.map +0 -1
  228. package/dist/runtime-v2/runner/diagnostician-runner.d.ts +0 -89
  229. package/dist/runtime-v2/runner/diagnostician-runner.d.ts.map +0 -1
  230. package/dist/runtime-v2/runner/diagnostician-runner.js +0 -470
  231. package/dist/runtime-v2/runner/diagnostician-runner.js.map +0 -1
@@ -1,682 +0,0 @@
1
- /**
2
- * DiagnosticianRunner unit tests.
3
- *
4
- * Covers 11 scenarios:
5
- * 1. Happy path: full lifecycle succeeds
6
- * 2. Polling loop: pollRun returns 'running' then 'succeeded'
7
- * 3. Timeout: pollRun never reaches terminal
8
- * 4. Runtime failure: pollRun returns 'failed'
9
- * 5. Context build failure (transient)
10
- * 6. Context build failure (permanent)
11
- * 7. Validation failure
12
- * 8. StartRunInput construction
13
- * 9. Lease conflict
14
- * 10. Max attempts exceeded
15
- * 11. OpenClaw history compatibility
16
- */
17
- import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
18
- import { PDRuntimeError } from '../../error-categories.js';
19
- import { DiagnosticianRunner } from '../diagnostician-runner.js';
20
- import { RunnerPhase } from '../runner-phase.js';
21
- // ── Test fixtures ──────────────────────────────────────────────────────────────
22
- const TASK_ID = 'task-test-001';
23
- const RUN_ID = 'run-test-001';
24
- const OWNER = 'test-owner';
25
- const RUNTIME_KIND = 'test-double';
26
- function makeTaskRecord(overrides = {}) {
27
- return {
28
- taskId: TASK_ID,
29
- taskKind: 'diagnostician',
30
- status: 'leased',
31
- createdAt: '2026-04-23T00:00:00Z',
32
- updatedAt: '2026-04-23T00:00:00Z',
33
- leaseOwner: OWNER,
34
- leaseExpiresAt: '2026-04-23T01:00:00Z',
35
- attemptCount: 1,
36
- maxAttempts: 3,
37
- ...overrides,
38
- };
39
- }
40
- function makeRunHandle() {
41
- return {
42
- runId: RUN_ID,
43
- runtimeKind: RUNTIME_KIND,
44
- startedAt: '2026-04-23T00:00:00Z',
45
- };
46
- }
47
- function makeContextPayload(overrides = {}) {
48
- return {
49
- contextId: 'ctx-001',
50
- contextHash: 'abc123hash',
51
- taskId: TASK_ID,
52
- workspaceDir: '/test/workspace',
53
- sourceRefs: ['trajectory-001'],
54
- diagnosisTarget: {
55
- reasonSummary: 'test diagnosis',
56
- },
57
- conversationWindow: [
58
- { ts: '2026-04-23T00:00:00Z', role: 'user', text: 'hello' },
59
- ],
60
- ...overrides,
61
- };
62
- }
63
- function makeDiagnosticianOutput(overrides = {}) {
64
- return {
65
- valid: true,
66
- diagnosisId: 'diag-001',
67
- summary: 'Test diagnosis summary',
68
- rootCause: 'Test root cause',
69
- violatedPrinciples: [],
70
- evidence: [],
71
- recommendations: [],
72
- confidence: 0.9,
73
- ...overrides,
74
- };
75
- }
76
- function createMocks() {
77
- const taskRecord = makeTaskRecord();
78
- const runHandle = makeRunHandle();
79
- const contextPayload = makeContextPayload();
80
- const output = makeDiagnosticianOutput();
81
- const mockStateManager = {
82
- acquireLease: vi.fn().mockResolvedValue(taskRecord),
83
- markTaskSucceeded: vi.fn().mockResolvedValue(taskRecord),
84
- markTaskFailed: vi.fn().mockResolvedValue(taskRecord),
85
- markTaskRetryWait: vi.fn().mockResolvedValue(taskRecord),
86
- updateRunOutput: vi.fn().mockResolvedValue({}),
87
- getRetryPolicy: vi.fn().mockReturnValue({
88
- calculateBackoff: vi.fn().mockReturnValue(30_000),
89
- shouldRetry: vi.fn().mockReturnValue(true),
90
- }),
91
- getRunsByTask: vi.fn().mockResolvedValue([{ runId: RUN_ID, taskId: TASK_ID }]),
92
- };
93
- const mockContextAssembler = {
94
- assemble: vi.fn().mockResolvedValue(contextPayload),
95
- };
96
- const mockRuntimeAdapter = {
97
- kind: vi.fn().mockReturnValue(RUNTIME_KIND),
98
- getCapabilities: vi.fn(),
99
- healthCheck: vi.fn(),
100
- startRun: vi.fn().mockResolvedValue(runHandle),
101
- pollRun: vi.fn().mockResolvedValue({
102
- runId: RUN_ID,
103
- status: 'succeeded',
104
- }),
105
- cancelRun: vi.fn().mockResolvedValue(undefined),
106
- fetchOutput: vi.fn().mockResolvedValue({
107
- runId: RUN_ID,
108
- payload: output,
109
- }),
110
- fetchArtifacts: vi.fn(),
111
- };
112
- const mockValidator = {
113
- validate: vi.fn().mockResolvedValue({
114
- valid: true,
115
- errors: [],
116
- }),
117
- };
118
- const mockEventEmitter = {
119
- emitTelemetry: vi.fn().mockReturnValue(true),
120
- on: vi.fn(),
121
- emit: vi.fn(),
122
- };
123
- return {
124
- mockStateManager: mockStateManager,
125
- mockContextAssembler: mockContextAssembler,
126
- mockRuntimeAdapter: mockRuntimeAdapter,
127
- mockValidator: mockValidator,
128
- mockEventEmitter: mockEventEmitter,
129
- taskRecord,
130
- runHandle,
131
- contextPayload,
132
- output,
133
- // Expose typed mocks for assertions
134
- _stateManager: mockStateManager,
135
- _contextAssembler: mockContextAssembler,
136
- _runtimeAdapter: mockRuntimeAdapter,
137
- _validator: mockValidator,
138
- _committer: { commit: vi.fn().mockResolvedValue({ commitId: "mock-commit-id", artifactId: "mock-artifact-id", candidateCount: 0 }) },
139
- _eventEmitter: mockEventEmitter,
140
- };
141
- }
142
- function createRunner(mocks) {
143
- return new DiagnosticianRunner({
144
- stateManager: mocks.mockStateManager,
145
- contextAssembler: mocks.mockContextAssembler,
146
- runtimeAdapter: mocks.mockRuntimeAdapter,
147
- eventEmitter: mocks.mockEventEmitter,
148
- validator: mocks.mockValidator,
149
- committer: mocks._committer,
150
- }, {
151
- owner: OWNER,
152
- runtimeKind: RUNTIME_KIND,
153
- pollIntervalMs: 100,
154
- timeoutMs: 1000,
155
- });
156
- }
157
- /** Type-safe helper to extract the first call argument from a mock. */
158
- function firstCallArg(mockFn) {
159
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
160
- return mockFn.mock.calls[0][0];
161
- }
162
- // ── Tests ──────────────────────────────────────────────────────────────────────
163
- describe('DiagnosticianRunner', () => {
164
- beforeEach(() => {
165
- vi.useFakeTimers({ shouldAdvanceTime: true });
166
- });
167
- afterEach(() => {
168
- vi.useRealTimers();
169
- });
170
- // 1. Happy path
171
- it('succeeds end-to-end when all phases complete normally', async () => {
172
- const mocks = createMocks();
173
- const runner = createRunner(mocks);
174
- const result = await runner.run(TASK_ID);
175
- expect(result.status).toBe('succeeded');
176
- expect(result.taskId).toBe(TASK_ID);
177
- expect(result.output).toBeDefined();
178
- expect(result.output?.diagnosisId).toBe('diag-001');
179
- expect(result.contextHash).toBe('abc123hash');
180
- expect(result.attemptCount).toBe(1);
181
- expect(mocks._stateManager.acquireLease).toHaveBeenCalledWith({
182
- taskId: TASK_ID,
183
- owner: OWNER,
184
- runtimeKind: RUNTIME_KIND,
185
- });
186
- expect(mocks._stateManager.markTaskSucceeded).toHaveBeenCalledWith(TASK_ID, 'commit://mock-commit-id');
187
- expect(mocks._stateManager.updateRunOutput).toHaveBeenCalledWith(RUN_ID, JSON.stringify(mocks.output));
188
- });
189
- // 2. Polling loop
190
- it('polls until terminal status is reached', async () => {
191
- const mocks = createMocks();
192
- // First poll: running, second poll: succeeded
193
- mocks._runtimeAdapter.pollRun
194
- .mockResolvedValueOnce({ runId: RUN_ID, status: 'running' })
195
- .mockResolvedValueOnce({ runId: RUN_ID, status: 'succeeded' });
196
- const runner = createRunner(mocks);
197
- const resultPromise = runner.run(TASK_ID);
198
- // Advance through the first sleep
199
- await vi.advanceTimersByTimeAsync(200);
200
- const result = await resultPromise;
201
- expect(result.status).toBe('succeeded');
202
- expect(mocks._runtimeAdapter.pollRun).toHaveBeenCalledTimes(2);
203
- });
204
- // 3. Timeout
205
- it('cancels run and retries on timeout', async () => {
206
- const mocks = createMocks();
207
- // pollRun always returns 'running'
208
- mocks._runtimeAdapter.pollRun.mockResolvedValue({ runId: RUN_ID, status: 'running' });
209
- const runner = createRunner(mocks);
210
- const resultPromise = runner.run(TASK_ID);
211
- // Advance past timeout
212
- await vi.advanceTimersByTimeAsync(1500);
213
- const result = await resultPromise;
214
- expect(mocks._runtimeAdapter.cancelRun).toHaveBeenCalledWith(RUN_ID);
215
- // shouldRetry returns true by default, so it should be retried
216
- expect(result.status).toBe('retried');
217
- expect(result.errorCategory).toBe('timeout');
218
- });
219
- // 4. Runtime failure
220
- it('handles runtime failure and retries if policy allows', async () => {
221
- const mocks = createMocks();
222
- mocks._runtimeAdapter.pollRun.mockResolvedValue({
223
- runId: RUN_ID,
224
- status: 'failed',
225
- reason: 'agent error',
226
- });
227
- const runner = createRunner(mocks);
228
- const result = await runner.run(TASK_ID);
229
- expect(result.status).toBe('retried');
230
- expect(result.errorCategory).toBe('execution_failed');
231
- expect(result.failureReason).toContain('failed');
232
- });
233
- // 5. Context build failure (transient)
234
- it('retries on transient context assembly error', async () => {
235
- const mocks = createMocks();
236
- mocks._contextAssembler.assemble.mockRejectedValue(new PDRuntimeError('runtime_unavailable', 'DB connection lost'));
237
- const runner = createRunner(mocks);
238
- const result = await runner.run(TASK_ID);
239
- expect(mocks._stateManager.markTaskRetryWait).toHaveBeenCalledWith(TASK_ID, 'runtime_unavailable');
240
- expect(result.status).toBe('retried');
241
- expect(result.errorCategory).toBe('runtime_unavailable');
242
- });
243
- // 6. Context build failure (permanent)
244
- it('fails permanently on workspace_invalid error', async () => {
245
- const mocks = createMocks();
246
- mocks._contextAssembler.assemble.mockRejectedValue(new PDRuntimeError('workspace_invalid', 'Workspace not found'));
247
- const runner = createRunner(mocks);
248
- const result = await runner.run(TASK_ID);
249
- expect(mocks._stateManager.markTaskFailed).toHaveBeenCalledWith(TASK_ID, 'workspace_invalid');
250
- expect(result.status).toBe('failed');
251
- expect(result.errorCategory).toBe('workspace_invalid');
252
- });
253
- // 7. Validation failure
254
- it('retries or fails on validation failure', async () => {
255
- const mocks = createMocks();
256
- mocks._validator.validate.mockResolvedValue({
257
- valid: false,
258
- errors: ['Missing taskId', 'Invalid confidence'],
259
- errorCategory: 'output_invalid',
260
- });
261
- const runner = createRunner(mocks);
262
- const result = await runner.run(TASK_ID);
263
- // Default shouldRetry returns true
264
- expect(result.status).toBe('retried');
265
- expect(result.errorCategory).toBe('output_invalid');
266
- expect(result.failureReason).toContain('Validation failed');
267
- });
268
- // 8b. StartRunInput construction with custom agentId
269
- it('uses custom agentId from resolvedOptions when provided', async () => {
270
- const mocks = createMocks();
271
- const runner = new DiagnosticianRunner({
272
- stateManager: mocks.mockStateManager,
273
- contextAssembler: mocks.mockContextAssembler,
274
- runtimeAdapter: mocks.mockRuntimeAdapter,
275
- eventEmitter: mocks.mockEventEmitter,
276
- validator: mocks.mockValidator,
277
- committer: mocks._committer,
278
- }, {
279
- owner: OWNER,
280
- runtimeKind: RUNTIME_KIND,
281
- pollIntervalMs: 100,
282
- timeoutMs: 1000,
283
- agentId: 'custom-diagnostician',
284
- });
285
- await runner.run(TASK_ID);
286
- const startInput = firstCallArg(mocks._runtimeAdapter.startRun);
287
- expect(startInput.agentSpec.agentId).toBe('custom-diagnostician');
288
- });
289
- // 8c. StartRunInput uses default 'main' when agentId not provided
290
- it('defaults agentSpec.agentId to main when agentId not provided', async () => {
291
- const mocks = createMocks();
292
- const runner = createRunner(mocks);
293
- await runner.run(TASK_ID);
294
- const startInput = firstCallArg(mocks._runtimeAdapter.startRun);
295
- expect(startInput.agentSpec.agentId).toBe('main');
296
- });
297
- // 9. Lease conflict
298
- it('lease_conflict returns non-mutating result without markTaskRetryWait/markTaskFailed', async () => {
299
- const mocks = createMocks();
300
- mocks._stateManager.acquireLease.mockRejectedValue(new PDRuntimeError('lease_conflict', 'Task already leased'));
301
- const runner = createRunner(mocks);
302
- const result = await runner.run(TASK_ID);
303
- // lease_conflict is handled as non-mutating — no state changes
304
- expect(result.status).toBe('failed');
305
- expect(result.errorCategory).toBe('lease_conflict');
306
- // Mutation methods must NOT be called for lease_conflict
307
- expect(mocks._stateManager.markTaskRetryWait).not.toHaveBeenCalled();
308
- expect(mocks._stateManager.markTaskFailed).not.toHaveBeenCalled();
309
- });
310
- // 9b. Post-lease errors use real leasedTask (not synthetic)
311
- it('post-lease error uses real attemptCount/maxAttempts from leasedTask', async () => {
312
- const mocks = createMocks();
313
- // leasedTask has attemptCount=2, maxAttempts=3 (not the default 1/3)
314
- mocks._stateManager.acquireLease.mockResolvedValue(makeTaskRecord({ attemptCount: 2, maxAttempts: 3 }));
315
- // Make context assembly fail to trigger post-lease error path
316
- mocks._contextAssembler.assemble.mockRejectedValue(new PDRuntimeError('runtime_unavailable', 'DB connection lost'));
317
- const runner = createRunner(mocks);
318
- const result = await runner.run(TASK_ID);
319
- // Should retry (not fail permanently) because runtime_unavailable is not permanent
320
- // and real leasedTask (attemptCount=2, maxAttempts=3) means shouldRetry returns true
321
- expect(result.status).toBe('retried');
322
- // Verify markTaskRetryWait was called with the real task's attemptCount
323
- expect(mocks._stateManager.markTaskRetryWait).toHaveBeenCalledWith(TASK_ID, 'runtime_unavailable');
324
- });
325
- // 9c. Post-lease error with maxAttempts=1 triggers fail immediately
326
- it('post-lease error with attemptCount=maxAttempts fails permanently', async () => {
327
- const mocks = createMocks();
328
- // leasedTask at last attempt (attemptCount=3, maxAttempts=3)
329
- mocks._stateManager.acquireLease.mockResolvedValue(makeTaskRecord({ attemptCount: 3, maxAttempts: 3 }));
330
- // Override shouldRetry to return false when attemptCount >= maxAttempts
331
- mocks._stateManager.getRetryPolicy.mockReturnValue({
332
- calculateBackoff: vi.fn().mockReturnValue(30_000),
333
- shouldRetry: vi.fn().mockReturnValue(false),
334
- });
335
- // Make context assembly fail
336
- mocks._contextAssembler.assemble.mockRejectedValue(new PDRuntimeError('runtime_unavailable', 'DB connection lost'));
337
- const runner = createRunner(mocks);
338
- const result = await runner.run(TASK_ID);
339
- // Should fail with max_attempts_exceeded (not runtime_unavailable)
340
- // because shouldRetry=false means no more retries
341
- expect(result.status).toBe('failed');
342
- expect(result.errorCategory).toBe('max_attempts_exceeded');
343
- expect(mocks._stateManager.markTaskFailed).toHaveBeenCalledWith(TASK_ID, 'max_attempts_exceeded');
344
- });
345
- // 10. Max attempts exceeded
346
- it('marks task failed when max attempts exceeded', async () => {
347
- const mocks = createMocks();
348
- // Make shouldRetry return false
349
- mocks._stateManager.getRetryPolicy.mockReturnValue({
350
- calculateBackoff: vi.fn().mockReturnValue(30_000),
351
- shouldRetry: vi.fn().mockReturnValue(false),
352
- });
353
- // Trigger a failure path (runtime failure)
354
- mocks._runtimeAdapter.pollRun.mockResolvedValue({
355
- runId: RUN_ID,
356
- status: 'failed',
357
- reason: 'agent error',
358
- });
359
- const runner = createRunner(mocks);
360
- const result = await runner.run(TASK_ID);
361
- expect(mocks._stateManager.markTaskFailed).toHaveBeenCalledWith(TASK_ID, 'max_attempts_exceeded');
362
- expect(result.status).toBe('failed');
363
- expect(result.errorCategory).toBe('max_attempts_exceeded');
364
- });
365
- // 11. OpenClaw history compatibility
366
- it('passes through openclaw-history context without inspection', async () => {
367
- const mocks = createMocks();
368
- // Context with mixed runtime_kind entries (openclaw-imported format)
369
- const openClawContext = makeContextPayload({
370
- sourceRefs: ['trajectory-001', 'openclaw-history-import-002'],
371
- conversationWindow: [
372
- { ts: '2026-04-23T00:00:00Z', role: 'user', text: 'hello from openclaw' },
373
- { ts: '2026-04-23T00:01:00Z', role: 'assistant', text: 'response from openclaw' },
374
- { ts: '2026-04-23T00:02:00Z', role: 'tool', toolName: 'Write', toolResultSummary: 'wrote file' },
375
- ],
376
- });
377
- mocks._contextAssembler.assemble.mockResolvedValue(openClawContext);
378
- const runner = createRunner(mocks);
379
- const result = await runner.run(TASK_ID);
380
- // Runner should NOT reject the context
381
- expect(result.status).toBe('succeeded');
382
- // startRun should have been called with the context serialized in inputPayload
383
- const startInput = firstCallArg(mocks._runtimeAdapter.startRun);
384
- const inputPayloadStr = startInput.inputPayload;
385
- const inputPayload = JSON.parse(inputPayloadStr);
386
- expect(inputPayload.context.conversationWindow).toHaveLength(3);
387
- expect(inputPayload.context.sourceRefs).toContain('openclaw-history-import-002');
388
- // contextItems should be empty (instruction embedded in inputPayload)
389
- expect(startInput.contextItems).toHaveLength(0);
390
- });
391
- // ── Committer integration tests (m5-03 Task 5) ──────────────────────────────
392
- describe('Committer integration', () => {
393
- // 12. Commit before markTaskSucceeded (call order)
394
- it('calls committer.commit before markTaskSucceeded', async () => {
395
- const mocks = createMocks();
396
- const callOrder = [];
397
- const typedCommitter = mocks._committer;
398
- typedCommitter.commit.mockImplementation(async () => {
399
- callOrder.push('commit');
400
- return { commitId: 'call-order-commit-id', artifactId: 'art-1', candidateCount: 1 };
401
- });
402
- mocks._stateManager.markTaskSucceeded.mockImplementation(async () => {
403
- callOrder.push('markTaskSucceeded');
404
- return mocks.taskRecord;
405
- });
406
- const runner = createRunner(mocks);
407
- await runner.run(TASK_ID);
408
- expect(callOrder).toEqual(['commit', 'markTaskSucceeded']);
409
- });
410
- // 13. resultRef uses commit:// scheme
411
- it('resultRef uses commit:// scheme after commit', async () => {
412
- const mocks = createMocks();
413
- const typedCommitter = mocks._committer;
414
- typedCommitter.commit.mockResolvedValue({
415
- commitId: 'verify-commit-123',
416
- artifactId: 'art-verify',
417
- candidateCount: 3,
418
- });
419
- const runner = createRunner(mocks);
420
- await runner.run(TASK_ID);
421
- expect(mocks._stateManager.markTaskSucceeded).toHaveBeenCalledWith(TASK_ID, 'commit://verify-commit-123');
422
- });
423
- // 14. Commit failure triggers retry with artifact_commit_failed
424
- it('commit failure triggers retry with artifact_commit_failed', async () => {
425
- const mocks = createMocks();
426
- const typedCommitter = mocks._committer;
427
- typedCommitter.commit.mockRejectedValue(new PDRuntimeError('artifact_commit_failed', 'Commit failed', {}));
428
- const runner = createRunner(mocks);
429
- const result = await runner.run(TASK_ID);
430
- expect(result.status).toBe('retried');
431
- expect(result.errorCategory).toBe('artifact_commit_failed');
432
- expect(mocks._stateManager.markTaskSucceeded).not.toHaveBeenCalled();
433
- expect(mocks._stateManager.markTaskRetryWait).toHaveBeenCalledWith(TASK_ID, 'artifact_commit_failed');
434
- });
435
- // 15. Commit failure with max attempts marks task failed
436
- it('commit failure with max attempts marks task failed', async () => {
437
- const mocks = createMocks();
438
- const typedCommitter = mocks._committer;
439
- typedCommitter.commit.mockRejectedValue(new PDRuntimeError('artifact_commit_failed', 'Commit failed', {}));
440
- mocks._stateManager.getRetryPolicy.mockReturnValue({
441
- calculateBackoff: vi.fn().mockReturnValue(30_000),
442
- shouldRetry: vi.fn().mockReturnValue(false),
443
- });
444
- const runner = createRunner(mocks);
445
- const result = await runner.run(TASK_ID);
446
- expect(result.status).toBe('failed');
447
- expect(result.errorCategory).toBe('max_attempts_exceeded');
448
- expect(mocks._stateManager.markTaskFailed).toHaveBeenCalledWith(TASK_ID, 'max_attempts_exceeded');
449
- });
450
- // 16. RunnerPhase transitions through Committing
451
- it('RunnerPhase transitions through Committing during commit', async () => {
452
- const mocks = createMocks();
453
- const typedCommitter = mocks._committer;
454
- typedCommitter.commit.mockResolvedValue({
455
- commitId: 'phase-test-commit',
456
- artifactId: 'art-phase',
457
- candidateCount: 0,
458
- });
459
- const runner = createRunner(mocks);
460
- const result = await runner.run(TASK_ID);
461
- expect(result.status).toBe('succeeded');
462
- expect(typedCommitter.commit).toHaveBeenCalledTimes(1);
463
- expect(runner.currentPhase).toBe(RunnerPhase.Completed);
464
- });
465
- });
466
- // ── M5-04: Telemetry events ───────────────────────────────────────────────
467
- describe('M5-04 Telemetry events', () => {
468
- // TELE-01: diagnostician_artifact_committed emitted after successful commit
469
- it('emits diagnostician_artifact_committed after successful commit', async () => {
470
- const mocks = createMocks();
471
- const typedCommitter = mocks._committer;
472
- typedCommitter.commit.mockResolvedValue({
473
- commitId: 'commit-artifact-001',
474
- artifactId: 'artifact-001',
475
- candidateCount: 2,
476
- });
477
- const runner = createRunner(mocks);
478
- await runner.run(TASK_ID);
479
- expect(mocks._eventEmitter.emitTelemetry).toHaveBeenCalledWith(expect.objectContaining({
480
- eventType: 'diagnostician_artifact_committed',
481
- traceId: TASK_ID,
482
- payload: expect.objectContaining({
483
- commitId: 'commit-artifact-001',
484
- artifactId: 'artifact-001',
485
- candidateCount: 2,
486
- taskId: TASK_ID,
487
- runId: RUN_ID,
488
- }),
489
- }));
490
- });
491
- // TELE-02: diagnostician_artifact_commit_failed emitted when commit throws
492
- it('emits diagnostician_artifact_commit_failed when commit throws', async () => {
493
- const mocks = createMocks();
494
- const typedCommitter = mocks._committer;
495
- typedCommitter.commit.mockRejectedValue(new PDRuntimeError('artifact_commit_failed', 'Database constraint violation', {}));
496
- const runner = createRunner(mocks);
497
- await runner.run(TASK_ID);
498
- expect(mocks._eventEmitter.emitTelemetry).toHaveBeenCalledWith(expect.objectContaining({
499
- eventType: 'diagnostician_artifact_commit_failed',
500
- traceId: TASK_ID,
501
- payload: expect.objectContaining({
502
- taskId: TASK_ID,
503
- runId: RUN_ID,
504
- errorCategory: 'artifact_commit_failed',
505
- }),
506
- }));
507
- });
508
- // TELE-03: principle_candidate_registered emitted per principle candidate
509
- it('emits principle_candidate_registered for each principle recommendation', async () => {
510
- const mocks = createMocks();
511
- const typedCommitter = mocks._committer;
512
- typedCommitter.commit.mockResolvedValue({
513
- commitId: 'commit-candidate-001',
514
- artifactId: 'artifact-001',
515
- candidateCount: 2,
516
- });
517
- // Override output with 2 principle recommendations
518
- const output = makeDiagnosticianOutput({
519
- recommendations: [
520
- { kind: 'principle', description: 'Use immutable data structures' },
521
- { kind: 'principle', description: 'Prefer pure functions' },
522
- { kind: 'rule', description: 'Follow existing naming conventions' },
523
- ],
524
- });
525
- mocks._runtimeAdapter.fetchOutput = vi.fn().mockResolvedValue({
526
- runId: RUN_ID,
527
- payload: output,
528
- });
529
- const runner = createRunner(mocks);
530
- await runner.run(TASK_ID);
531
- // Should have 2 calls to principle_candidate_registered
532
- const candidateEvents = mocks._eventEmitter.emitTelemetry.mock.calls.filter((call) => call[0]?.eventType === 'principle_candidate_registered');
533
- expect(candidateEvents).toHaveLength(2);
534
- const [firstEvent, secondEvent] = candidateEvents;
535
- if (!firstEvent || !secondEvent) {
536
- throw new Error('Expected 2 candidate events but got fewer');
537
- }
538
- expect(firstEvent[0]?.payload).toMatchObject({
539
- commitId: 'commit-candidate-001',
540
- kind: 'principle',
541
- candidateIndex: 0,
542
- sourceRunId: RUN_ID,
543
- });
544
- expect(secondEvent[0]?.payload).toMatchObject({
545
- commitId: 'commit-candidate-001',
546
- kind: 'principle',
547
- candidateIndex: 1,
548
- sourceRunId: RUN_ID,
549
- });
550
- });
551
- // TELE-04: all events use emitTelemetry (StoreEventEmitter)
552
- it('all new telemetry events use emitTelemetry, not console.log or side effects', async () => {
553
- const mocks = createMocks();
554
- const typedCommitter = mocks._committer;
555
- typedCommitter.commit.mockResolvedValue({
556
- commitId: 'commit-tele-004',
557
- artifactId: 'artifact-004',
558
- candidateCount: 1,
559
- });
560
- // Override output with 1 principle recommendation so the event fires
561
- const output = makeDiagnosticianOutput({
562
- recommendations: [
563
- { kind: 'principle', description: 'Test principle' },
564
- ],
565
- });
566
- mocks._runtimeAdapter.fetchOutput = vi.fn().mockResolvedValue({
567
- runId: RUN_ID,
568
- payload: output,
569
- });
570
- const runner = createRunner(mocks);
571
- await runner.run(TASK_ID);
572
- // All 3 new event types should go through emitTelemetry
573
- const allEventTypes = mocks._eventEmitter.emitTelemetry.mock.calls.map((call) => call[0]?.eventType);
574
- expect(allEventTypes).toContain('diagnostician_artifact_committed');
575
- expect(allEventTypes).toContain('principle_candidate_registered');
576
- });
577
- });
578
- // ── PRI-137: workspace_dirty as permanent error ──────────────────────────────────
579
- describe('PRI-137: workspace_dirty permanent error handling', () => {
580
- it('fails permanently on workspace_dirty error (never retries)', async () => {
581
- const mocks = createMocks();
582
- mocks._contextAssembler.assemble.mockRejectedValue(new PDRuntimeError('workspace_dirty', 'Workspace contains uncommitted changes', {
583
- dirtyFiles: ['file1.ts', 'file2.ts'],
584
- }));
585
- const runner = createRunner(mocks);
586
- const result = await runner.run(TASK_ID);
587
- expect(result.status).toBe('failed');
588
- expect(result.errorCategory).toBe('workspace_dirty');
589
- expect(result.failureReason).toContain('workspace_dirty');
590
- expect(mocks._stateManager.markTaskFailed).toHaveBeenCalledWith(TASK_ID, 'workspace_dirty');
591
- expect(mocks._stateManager.markTaskRetryWait).not.toHaveBeenCalled();
592
- });
593
- it('workspace_dirty fails immediately even when shouldRetry returns true', async () => {
594
- const mocks = createMocks();
595
- mocks._contextAssembler.assemble.mockRejectedValue(new PDRuntimeError('workspace_dirty', 'Dirty workspace detected'));
596
- mocks._stateManager.getRetryPolicy.mockReturnValue({
597
- calculateBackoff: vi.fn().mockReturnValue(30_000),
598
- shouldRetry: vi.fn().mockReturnValue(true),
599
- });
600
- const runner = createRunner(mocks);
601
- const result = await runner.run(TASK_ID);
602
- expect(result.status).toBe('failed');
603
- expect(result.errorCategory).toBe('workspace_dirty');
604
- expect(mocks._stateManager.markTaskRetryWait).not.toHaveBeenCalled();
605
- expect(mocks._stateManager.markTaskFailed).toHaveBeenCalledWith(TASK_ID, 'workspace_dirty');
606
- });
607
- it('workspace_dirty is in PERMANENT_ERROR_CATEGORIES set', async () => {
608
- const mocks = createMocks();
609
- mocks._contextAssembler.assemble.mockRejectedValue(new PDRuntimeError('workspace_dirty', 'Workspace dirty'));
610
- mocks._stateManager.getRetryPolicy.mockReturnValue({
611
- calculateBackoff: vi.fn().mockReturnValue(30_000),
612
- shouldRetry: vi.fn().mockReturnValue(false),
613
- });
614
- const runner = createRunner(mocks);
615
- const result = await runner.run(TASK_ID);
616
- expect(result.status).toBe('failed');
617
- expect(result.errorCategory).toBe('workspace_dirty');
618
- });
619
- it('emits task_failed telemetry with workspace_dirty category', async () => {
620
- const mocks = createMocks();
621
- mocks._contextAssembler.assemble.mockRejectedValue(new PDRuntimeError('workspace_dirty', 'Uncommitted changes found'));
622
- const runner = createRunner(mocks);
623
- await runner.run(TASK_ID);
624
- const allEventTypes = mocks._eventEmitter.emitTelemetry.mock.calls.map((call) => call[0]?.eventType);
625
- expect(allEventTypes).toContain('diagnostician_task_failed');
626
- const taskFailedEvents = mocks._eventEmitter.emitTelemetry.mock.calls
627
- .filter((call) => call[0]?.eventType === 'diagnostician_task_failed');
628
- expect(taskFailedEvents[0]?.[0]?.payload?.errorCategory).toBe('workspace_dirty');
629
- });
630
- it('capability_missing also fails permanently (part of permanent error set)', async () => {
631
- const mocks = createMocks();
632
- mocks._contextAssembler.assemble.mockRejectedValue(new PDRuntimeError('capability_missing', 'Required capability not available'));
633
- const runner = createRunner(mocks);
634
- const result = await runner.run(TASK_ID);
635
- expect(result.status).toBe('failed');
636
- expect(result.errorCategory).toBe('capability_missing');
637
- expect(mocks._stateManager.markTaskFailed).toHaveBeenCalledWith(TASK_ID, 'capability_missing');
638
- expect(mocks._stateManager.markTaskRetryWait).not.toHaveBeenCalled();
639
- });
640
- });
641
- // ── ERR-001/005: Runner boundary schema validation ──────────────────────────
642
- describe('Runner boundary schema validation (ERR-001/005)', () => {
643
- it('rejects adapter payload that does not match DiagnosticianOutputV1Schema', async () => {
644
- const mocks = createMocks();
645
- // Adapter returns payload that bypasses its own validation (defense-in-depth test)
646
- mocks._runtimeAdapter.fetchOutput = vi.fn().mockResolvedValue({
647
- runId: RUN_ID,
648
- payload: { wrong: 'shape', missing: 'all required fields' },
649
- });
650
- const runner = createRunner(mocks);
651
- const result = await runner.run(TASK_ID);
652
- // Should end up in retry_wait/output_invalid, not silently accepted
653
- expect(result.status).toBe('retried');
654
- expect(result.errorCategory).toBe('output_invalid');
655
- });
656
- it('includes evidence in output_invalid error when runner boundary rejects payload', async () => {
657
- const mocks = createMocks();
658
- mocks._runtimeAdapter.fetchOutput = vi.fn().mockResolvedValue({
659
- runId: RUN_ID,
660
- payload: { invalid: true },
661
- });
662
- const runner = createRunner(mocks);
663
- const result = await runner.run(TASK_ID);
664
- expect(result.status).toBe('retried');
665
- expect(result.errorCategory).toBe('output_invalid');
666
- // The retry_wait call should have been made with output_invalid
667
- expect(mocks._stateManager.markTaskRetryWait).toHaveBeenCalledWith(TASK_ID, 'output_invalid');
668
- });
669
- it('accepts valid DiagnosticianOutputV1 payload through runner boundary', async () => {
670
- const mocks = createMocks();
671
- const validOutput = makeDiagnosticianOutput();
672
- mocks._runtimeAdapter.fetchOutput = vi.fn().mockResolvedValue({
673
- runId: RUN_ID,
674
- payload: validOutput,
675
- });
676
- const runner = createRunner(mocks);
677
- const result = await runner.run(TASK_ID);
678
- expect(result.status).toBe('succeeded');
679
- });
680
- });
681
- });
682
- //# sourceMappingURL=diagnostician-runner.test.js.map