@nathapp/nax 0.18.1

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 (459) hide show
  1. package/.gitlab-ci.yml +96 -0
  2. package/BRIEF.md +140 -0
  3. package/CHANGELOG.md +60 -0
  4. package/CLAUDE.md +159 -0
  5. package/README.md +373 -0
  6. package/US-007-IMPLEMENTATION.md +139 -0
  7. package/bin/nax.ts +930 -0
  8. package/biome.json +14 -0
  9. package/bun.lock +168 -0
  10. package/bunfig.toml +11 -0
  11. package/docs/20260216-fix-plan-context-review.md +56 -0
  12. package/docs/20260216-relentless-vs-ngent-comparison.md +208 -0
  13. package/docs/20260216-v02-plan.md +136 -0
  14. package/docs/20260216-v02-review.md +685 -0
  15. package/docs/20260217-dogfood-findings.md +56 -0
  16. package/docs/20260217-p2-plus-plan.md +117 -0
  17. package/docs/20260217-partial-fixes-plan.md +62 -0
  18. package/docs/20260217-plan-analyze-spec.md +117 -0
  19. package/docs/20260217-post-impl-review.md +1137 -0
  20. package/docs/20260217-quick-wins-plan.md +66 -0
  21. package/docs/20260217-split-runner-plan.md +75 -0
  22. package/docs/20260217-v03-impl-plan.md +80 -0
  23. package/docs/20260217-v03-post-impl-review.md +589 -0
  24. package/docs/20260217-v04-impl-plan.md +86 -0
  25. package/docs/20260217-v05-post-impl-review.md +850 -0
  26. package/docs/20260217-v06-post-impl-review.md +817 -0
  27. package/docs/20260218-adr003-port-plan.md +151 -0
  28. package/docs/20260218-review-adr003-verification.md +175 -0
  29. package/docs/20260219-fix-plan-bug16-19.md +79 -0
  30. package/docs/20260219-fix-plan-bug20-22.md +114 -0
  31. package/docs/20260219-plan-llm-routing.md +116 -0
  32. package/docs/20260219-review-bug20-22-fixes.md +135 -0
  33. package/docs/20260219-routing-baseline-keyword.md +63 -0
  34. package/docs/20260220-plan-structured-logging-p1.md +80 -0
  35. package/docs/20260220-plan-structured-logging-p2.md +37 -0
  36. package/docs/20260220-review-llm-routing.md +180 -0
  37. package/docs/20260220-review-post-fix-llm-routing.md +70 -0
  38. package/docs/20260221-fix-plan-relevantfiles-split.md +101 -0
  39. package/docs/20260221-fix-plan-routing-mode.md +125 -0
  40. package/docs/20260221-review-v0.9-implementation.md +379 -0
  41. package/docs/20260222-fix-plan-v091-routing-isolation.md +197 -0
  42. package/docs/20260223-fix-plan-prompt-audit.md +62 -0
  43. package/docs/20260224-nax-roadmap-phases.md +189 -0
  44. package/docs/20260225-phase2-llm-service-layer.md +401 -0
  45. package/docs/20260225-review-v0.10.1.md +187 -0
  46. package/docs/20260303-v010-implementation-plan.md +165 -0
  47. package/docs/CLAUDE.md.bak +191 -0
  48. package/docs/ROADMAP.md +165 -0
  49. package/docs/SPEC-rectification.md +0 -0
  50. package/docs/SPEC.md +324 -0
  51. package/docs/US-001-plugin-loading-verification.md +152 -0
  52. package/docs/architecture-analysis.md +1076 -0
  53. package/docs/bugs/BUG-21-escalation-null-attempts.md +48 -0
  54. package/docs/bugs-from-dogfood-run-c.md +243 -0
  55. package/docs/code-review-20260228.md +612 -0
  56. package/docs/code-review-v0.15.0.md +629 -0
  57. package/docs/hook-lifecycle-test-plan.md +149 -0
  58. package/docs/releases/v0.11.0-and-earlier.md +20 -0
  59. package/docs/releases/v0.12.0.md +15 -0
  60. package/docs/releases/v0.13.0.md +14 -0
  61. package/docs/releases/v0.14.0.md +20 -0
  62. package/docs/releases/v0.14.1.md +36 -0
  63. package/docs/releases/v0.14.2.md +51 -0
  64. package/docs/releases/v0.14.3.md +174 -0
  65. package/docs/releases/v0.14.4.md +94 -0
  66. package/docs/releases/v0.15.0.md +502 -0
  67. package/docs/releases/v0.15.1.md +170 -0
  68. package/docs/releases/v0.15.3.md +193 -0
  69. package/docs/specs/status-file-v0.10.1.md +812 -0
  70. package/docs/v0.10-global-config.md +206 -0
  71. package/docs/v0.10-plugin-system.md +415 -0
  72. package/docs/v0.10-prompt-optimizer.md +234 -0
  73. package/docs/v0.3-spec.md +244 -0
  74. package/docs/v0.4-spec.md +140 -0
  75. package/docs/v0.5-spec.md +237 -0
  76. package/docs/v0.6-spec.md +371 -0
  77. package/docs/v0.7-spec.md +177 -0
  78. package/docs/v0.8-llm-routing.md +206 -0
  79. package/docs/v0.8-structured-logging.md +132 -0
  80. package/docs/v0.9.3-prompt-audit.md +112 -0
  81. package/examples/plugins/console-reporter/index.test.ts +207 -0
  82. package/examples/plugins/console-reporter/index.ts +110 -0
  83. package/nax/config.json +147 -0
  84. package/nax/features/bugfix-v0171/prd.json +52 -0
  85. package/nax/features/config-management/prd.json +108 -0
  86. package/nax/features/config-management/progress.txt +5 -0
  87. package/nax/features/diagnose/acceptance.test.ts +412 -0
  88. package/nax/features/diagnose/prd.json +41 -0
  89. package/nax/features/orchestration-fixes/prd.json +89 -0
  90. package/nax/features/orchestration-fixes/progress.txt +1 -0
  91. package/nax/features/plugin-integration/US-007-VERIFICATION.md +259 -0
  92. package/nax/features/plugin-integration/prd.json +208 -0
  93. package/nax/features/plugin-integration/progress.txt +5 -0
  94. package/nax/features/precheck/prd.json +205 -0
  95. package/nax/features/precheck/progress.txt +15 -0
  96. package/nax/features/structured-logging/prd.json +199 -0
  97. package/nax/features/unlock/prd.json +36 -0
  98. package/package.json +47 -0
  99. package/src/acceptance/fix-generator.ts +348 -0
  100. package/src/acceptance/generator.ts +282 -0
  101. package/src/acceptance/index.ts +30 -0
  102. package/src/acceptance/types.ts +79 -0
  103. package/src/agents/claude-decompose.ts +169 -0
  104. package/src/agents/claude-plan.ts +139 -0
  105. package/src/agents/claude.ts +324 -0
  106. package/src/agents/cost.ts +268 -0
  107. package/src/agents/index.ts +13 -0
  108. package/src/agents/registry.ts +48 -0
  109. package/src/agents/types-extended.ts +133 -0
  110. package/src/agents/types.ts +113 -0
  111. package/src/agents/validation.ts +69 -0
  112. package/src/analyze/classifier.ts +305 -0
  113. package/src/analyze/index.ts +16 -0
  114. package/src/analyze/scanner.ts +175 -0
  115. package/src/analyze/types.ts +51 -0
  116. package/src/cli/accept.ts +108 -0
  117. package/src/cli/analyze-parser.ts +284 -0
  118. package/src/cli/analyze.ts +207 -0
  119. package/src/cli/config.ts +561 -0
  120. package/src/cli/constitution.ts +109 -0
  121. package/src/cli/diagnose-analysis.ts +159 -0
  122. package/src/cli/diagnose-formatter.ts +87 -0
  123. package/src/cli/diagnose.ts +203 -0
  124. package/src/cli/generate.ts +127 -0
  125. package/src/cli/index.ts +37 -0
  126. package/src/cli/init.ts +188 -0
  127. package/src/cli/interact.ts +295 -0
  128. package/src/cli/plan.ts +198 -0
  129. package/src/cli/plugins.ts +111 -0
  130. package/src/cli/prompts.ts +295 -0
  131. package/src/cli/runs.ts +174 -0
  132. package/src/cli/status-cost.ts +151 -0
  133. package/src/cli/status-features.ts +338 -0
  134. package/src/cli/status.ts +13 -0
  135. package/src/commands/common.ts +171 -0
  136. package/src/commands/diagnose.ts +17 -0
  137. package/src/commands/index.ts +8 -0
  138. package/src/commands/logs.ts +384 -0
  139. package/src/commands/precheck.ts +86 -0
  140. package/src/commands/unlock.ts +96 -0
  141. package/src/config/defaults.ts +160 -0
  142. package/src/config/index.ts +22 -0
  143. package/src/config/loader.ts +121 -0
  144. package/src/config/merger.ts +147 -0
  145. package/src/config/path-security.ts +121 -0
  146. package/src/config/paths.ts +27 -0
  147. package/src/config/schema.ts +56 -0
  148. package/src/config/schemas.ts +286 -0
  149. package/src/config/types.ts +423 -0
  150. package/src/config/validate.ts +103 -0
  151. package/src/constitution/generator.ts +191 -0
  152. package/src/constitution/generators/aider.ts +41 -0
  153. package/src/constitution/generators/claude.ts +35 -0
  154. package/src/constitution/generators/cursor.ts +36 -0
  155. package/src/constitution/generators/opencode.ts +38 -0
  156. package/src/constitution/generators/types.ts +33 -0
  157. package/src/constitution/generators/windsurf.ts +36 -0
  158. package/src/constitution/index.ts +10 -0
  159. package/src/constitution/loader.ts +133 -0
  160. package/src/constitution/types.ts +31 -0
  161. package/src/context/auto-detect.ts +227 -0
  162. package/src/context/builder.ts +246 -0
  163. package/src/context/elements.ts +83 -0
  164. package/src/context/formatter.ts +107 -0
  165. package/src/context/generator.ts +129 -0
  166. package/src/context/generators/aider.ts +34 -0
  167. package/src/context/generators/claude.ts +28 -0
  168. package/src/context/generators/cursor.ts +28 -0
  169. package/src/context/generators/opencode.ts +30 -0
  170. package/src/context/generators/windsurf.ts +28 -0
  171. package/src/context/greenfield.ts +114 -0
  172. package/src/context/index.ts +33 -0
  173. package/src/context/injector.ts +279 -0
  174. package/src/context/test-scanner.ts +370 -0
  175. package/src/context/types.ts +98 -0
  176. package/src/errors.ts +67 -0
  177. package/src/execution/batching.ts +157 -0
  178. package/src/execution/crash-recovery.ts +373 -0
  179. package/src/execution/escalation/escalation.ts +44 -0
  180. package/src/execution/escalation/index.ts +13 -0
  181. package/src/execution/escalation/tier-escalation.ts +295 -0
  182. package/src/execution/escalation/tier-outcome.ts +158 -0
  183. package/src/execution/helpers.ts +38 -0
  184. package/src/execution/index.ts +45 -0
  185. package/src/execution/lifecycle/acceptance-loop.ts +272 -0
  186. package/src/execution/lifecycle/headless-formatter.ts +85 -0
  187. package/src/execution/lifecycle/index.ts +12 -0
  188. package/src/execution/lifecycle/parallel-lifecycle.ts +101 -0
  189. package/src/execution/lifecycle/precheck-runner.ts +140 -0
  190. package/src/execution/lifecycle/run-cleanup.ts +81 -0
  191. package/src/execution/lifecycle/run-completion.ts +129 -0
  192. package/src/execution/lifecycle/run-initialization.ts +141 -0
  193. package/src/execution/lifecycle/run-lifecycle.ts +312 -0
  194. package/src/execution/lifecycle/run-setup.ts +204 -0
  195. package/src/execution/lifecycle/story-hooks.ts +38 -0
  196. package/src/execution/lifecycle/story-size-prompts.ts +123 -0
  197. package/src/execution/lock.ts +115 -0
  198. package/src/execution/parallel-executor.ts +216 -0
  199. package/src/execution/parallel.ts +400 -0
  200. package/src/execution/pid-registry.ts +280 -0
  201. package/src/execution/pipeline-result-handler.ts +388 -0
  202. package/src/execution/post-verify-rectification.ts +188 -0
  203. package/src/execution/post-verify.ts +274 -0
  204. package/src/execution/progress.ts +25 -0
  205. package/src/execution/prompts.ts +127 -0
  206. package/src/execution/queue-handler.ts +109 -0
  207. package/src/execution/rectification.ts +13 -0
  208. package/src/execution/runner.ts +377 -0
  209. package/src/execution/sequential-executor.ts +388 -0
  210. package/src/execution/status-file.ts +264 -0
  211. package/src/execution/status-writer.ts +139 -0
  212. package/src/execution/story-context.ts +229 -0
  213. package/src/execution/test-output-parser.ts +14 -0
  214. package/src/execution/verification.ts +72 -0
  215. package/src/hooks/index.ts +2 -0
  216. package/src/hooks/runner.ts +286 -0
  217. package/src/hooks/types.ts +67 -0
  218. package/src/interaction/chain.ts +154 -0
  219. package/src/interaction/index.ts +60 -0
  220. package/src/interaction/init.ts +83 -0
  221. package/src/interaction/plugins/auto.ts +217 -0
  222. package/src/interaction/plugins/cli.ts +300 -0
  223. package/src/interaction/plugins/telegram.ts +384 -0
  224. package/src/interaction/plugins/webhook.ts +258 -0
  225. package/src/interaction/state.ts +171 -0
  226. package/src/interaction/triggers.ts +229 -0
  227. package/src/interaction/types.ts +163 -0
  228. package/src/logger/formatters.ts +84 -0
  229. package/src/logger/index.ts +16 -0
  230. package/src/logger/logger.ts +298 -0
  231. package/src/logger/types.ts +48 -0
  232. package/src/logging/formatter.ts +355 -0
  233. package/src/logging/index.ts +22 -0
  234. package/src/logging/types.ts +93 -0
  235. package/src/metrics/aggregator.ts +190 -0
  236. package/src/metrics/index.ts +14 -0
  237. package/src/metrics/tracker.ts +200 -0
  238. package/src/metrics/types.ts +109 -0
  239. package/src/optimizer/index.ts +62 -0
  240. package/src/optimizer/noop.optimizer.ts +24 -0
  241. package/src/optimizer/rule-based.optimizer.ts +248 -0
  242. package/src/optimizer/types.ts +53 -0
  243. package/src/pipeline/events.ts +130 -0
  244. package/src/pipeline/index.ts +19 -0
  245. package/src/pipeline/runner.ts +161 -0
  246. package/src/pipeline/stages/acceptance.ts +197 -0
  247. package/src/pipeline/stages/completion.ts +99 -0
  248. package/src/pipeline/stages/constitution.ts +63 -0
  249. package/src/pipeline/stages/context.ts +117 -0
  250. package/src/pipeline/stages/execution.ts +194 -0
  251. package/src/pipeline/stages/index.ts +62 -0
  252. package/src/pipeline/stages/optimizer.ts +74 -0
  253. package/src/pipeline/stages/prompt.ts +57 -0
  254. package/src/pipeline/stages/queue-check.ts +103 -0
  255. package/src/pipeline/stages/review.ts +181 -0
  256. package/src/pipeline/stages/routing.ts +81 -0
  257. package/src/pipeline/stages/verify.ts +100 -0
  258. package/src/pipeline/types.ts +167 -0
  259. package/src/plugins/index.ts +31 -0
  260. package/src/plugins/loader.ts +287 -0
  261. package/src/plugins/registry.ts +168 -0
  262. package/src/plugins/types.ts +327 -0
  263. package/src/plugins/validator.ts +352 -0
  264. package/src/prd/index.ts +172 -0
  265. package/src/prd/types.ts +202 -0
  266. package/src/precheck/checks-blockers.ts +391 -0
  267. package/src/precheck/checks-warnings.ts +142 -0
  268. package/src/precheck/checks.ts +30 -0
  269. package/src/precheck/index.ts +247 -0
  270. package/src/precheck/story-size-gate.ts +144 -0
  271. package/src/precheck/types.ts +31 -0
  272. package/src/queue/index.ts +2 -0
  273. package/src/queue/manager.ts +254 -0
  274. package/src/queue/types.ts +54 -0
  275. package/src/review/index.ts +8 -0
  276. package/src/review/runner.ts +172 -0
  277. package/src/review/types.ts +66 -0
  278. package/src/routing/builder.ts +81 -0
  279. package/src/routing/chain.ts +74 -0
  280. package/src/routing/index.ts +16 -0
  281. package/src/routing/loader.ts +58 -0
  282. package/src/routing/router.ts +303 -0
  283. package/src/routing/strategies/adaptive.ts +215 -0
  284. package/src/routing/strategies/index.ts +8 -0
  285. package/src/routing/strategies/keyword.ts +163 -0
  286. package/src/routing/strategies/llm-prompts.ts +209 -0
  287. package/src/routing/strategies/llm.ts +235 -0
  288. package/src/routing/strategies/manual.ts +50 -0
  289. package/src/routing/strategy.ts +99 -0
  290. package/src/tdd/cleanup.ts +111 -0
  291. package/src/tdd/index.ts +23 -0
  292. package/src/tdd/isolation.ts +123 -0
  293. package/src/tdd/orchestrator.ts +383 -0
  294. package/src/tdd/prompts.ts +270 -0
  295. package/src/tdd/rectification-gate.ts +183 -0
  296. package/src/tdd/session-runner.ts +179 -0
  297. package/src/tdd/types.ts +81 -0
  298. package/src/tdd/verdict.ts +271 -0
  299. package/src/tui/App.tsx +265 -0
  300. package/src/tui/components/AgentPanel.tsx +75 -0
  301. package/src/tui/components/CostOverlay.tsx +118 -0
  302. package/src/tui/components/HelpOverlay.tsx +107 -0
  303. package/src/tui/components/StatusBar.tsx +63 -0
  304. package/src/tui/components/StoriesPanel.tsx +177 -0
  305. package/src/tui/hooks/useKeyboard.ts +142 -0
  306. package/src/tui/hooks/useLayout.ts +137 -0
  307. package/src/tui/hooks/usePipelineEvents.ts +183 -0
  308. package/src/tui/hooks/usePty.ts +194 -0
  309. package/src/tui/index.tsx +38 -0
  310. package/src/tui/types.ts +76 -0
  311. package/src/utils/git.ts +83 -0
  312. package/src/utils/queue-writer.ts +54 -0
  313. package/src/verification/executor.ts +235 -0
  314. package/src/verification/gate.ts +207 -0
  315. package/src/verification/index.ts +12 -0
  316. package/src/verification/parser.ts +230 -0
  317. package/src/verification/rectification.ts +108 -0
  318. package/src/verification/types.ts +113 -0
  319. package/src/worktree/dispatcher.ts +65 -0
  320. package/src/worktree/index.ts +2 -0
  321. package/src/worktree/manager.ts +187 -0
  322. package/src/worktree/merge.ts +301 -0
  323. package/src/worktree/types.ts +4 -0
  324. package/test/TEST_COVERAGE_US001.md +217 -0
  325. package/test/TEST_COVERAGE_US003.md +84 -0
  326. package/test/TEST_COVERAGE_US005.md +86 -0
  327. package/test/US-002-orchestrator.test.ts +246 -0
  328. package/test/acceptance/cm-003-default-view.test.ts +194 -0
  329. package/test/execution/pid-registry.test.ts +240 -0
  330. package/test/execution/post-verify.test.ts +224 -0
  331. package/test/helpers/timeout.ts +42 -0
  332. package/test/integration/US-002-TEST-SUMMARY.md +107 -0
  333. package/test/integration/US-003-TEST-SUMMARY.md +149 -0
  334. package/test/integration/US-004-TEST-SUMMARY.md +106 -0
  335. package/test/integration/US-005-TEST-SUMMARY.md +138 -0
  336. package/test/integration/US-007-TEST-SUMMARY.md +100 -0
  337. package/test/integration/agent-validation.test.ts +439 -0
  338. package/test/integration/analyze-integration.test.ts +261 -0
  339. package/test/integration/analyze-scanner.test.ts +131 -0
  340. package/test/integration/cli-config-default-edge-cases.test.ts +222 -0
  341. package/test/integration/cli-config-default-view.test.ts +229 -0
  342. package/test/integration/cli-config-diff.test.ts +460 -0
  343. package/test/integration/cli-config.test.ts +736 -0
  344. package/test/integration/cli-diagnose.test.ts +592 -0
  345. package/test/integration/cli-logs.test.ts +314 -0
  346. package/test/integration/cli-plugins.test.ts +678 -0
  347. package/test/integration/cli-precheck.test.ts +371 -0
  348. package/test/integration/cli-run-headless.test.ts +173 -0
  349. package/test/integration/cli.test.ts +75 -0
  350. package/test/integration/config/merger.test.ts +465 -0
  351. package/test/integration/config/paths.test.ts +51 -0
  352. package/test/integration/config-loader.test.ts +265 -0
  353. package/test/integration/config.test.ts +444 -0
  354. package/test/integration/context-integration.test.ts +702 -0
  355. package/test/integration/context-provider-injection.test.ts +506 -0
  356. package/test/integration/context-verification-integration.test.ts +295 -0
  357. package/test/integration/e2e.test.ts +896 -0
  358. package/test/integration/execution.test.ts +625 -0
  359. package/test/integration/helpers.test.ts +295 -0
  360. package/test/integration/hooks.test.ts +361 -0
  361. package/test/integration/interaction-chain-pipeline.test.ts +464 -0
  362. package/test/integration/isolation.test.ts +143 -0
  363. package/test/integration/logger.test.ts +461 -0
  364. package/test/integration/parallel.test.ts +250 -0
  365. package/test/integration/path-security.test.ts +173 -0
  366. package/test/integration/pipeline-acceptance.test.ts +302 -0
  367. package/test/integration/pipeline-events.test.ts +475 -0
  368. package/test/integration/pipeline.test.ts +658 -0
  369. package/test/integration/plan.test.ts +157 -0
  370. package/test/integration/plugin-routing.test.ts +921 -0
  371. package/test/integration/plugins/config-integration.test.ts +172 -0
  372. package/test/integration/plugins/config-resolution.test.ts +522 -0
  373. package/test/integration/plugins/loader.test.ts +641 -0
  374. package/test/integration/plugins/registry.test.ts +746 -0
  375. package/test/integration/plugins/validator.test.ts +563 -0
  376. package/test/integration/prd-pause.test.ts +205 -0
  377. package/test/integration/prd-resolvers.test.ts +185 -0
  378. package/test/integration/precheck-integration.test.ts +468 -0
  379. package/test/integration/precheck.test.ts +805 -0
  380. package/test/integration/progress.test.ts +34 -0
  381. package/test/integration/rectification-flow.test.ts +512 -0
  382. package/test/integration/reporter-lifecycle.test.ts +860 -0
  383. package/test/integration/review-config-commands.test.ts +319 -0
  384. package/test/integration/review-config-schema.test.ts +116 -0
  385. package/test/integration/review-plugin-integration.test.ts +722 -0
  386. package/test/integration/review.test.ts +149 -0
  387. package/test/integration/routing-stage-bug-021.test.ts +274 -0
  388. package/test/integration/routing-stage-greenfield.test.ts +286 -0
  389. package/test/integration/runner-config-plugins.test.ts +461 -0
  390. package/test/integration/runner-fixes.test.ts +399 -0
  391. package/test/integration/runner-plugin-integration.test.ts +543 -0
  392. package/test/integration/runner.test.ts +1679 -0
  393. package/test/integration/s5-greenfield-fallback.test.ts +297 -0
  394. package/test/integration/status-file-integration.test.ts +325 -0
  395. package/test/integration/status-file.test.ts +379 -0
  396. package/test/integration/status-writer.test.ts +345 -0
  397. package/test/integration/story-id-in-events.test.ts +273 -0
  398. package/test/integration/tdd-cleanup.test.ts +246 -0
  399. package/test/integration/tdd-orchestrator.test.ts +1762 -0
  400. package/test/integration/test-scanner.test.ts +403 -0
  401. package/test/integration/verification-asset-check.test.ts +142 -0
  402. package/test/integration/verify-stage.test.ts +275 -0
  403. package/test/integration/worktree/manager.test.ts +218 -0
  404. package/test/integration/worktree/merge.test.ts +341 -0
  405. package/test/manual/logging-formatter-demo.ts +158 -0
  406. package/test/ui/tui-agent-panel.test.tsx +99 -0
  407. package/test/ui/tui-controls.test.ts +334 -0
  408. package/test/ui/tui-cost-and-pty.test.ts +189 -0
  409. package/test/ui/tui-layout.test.ts +378 -0
  410. package/test/ui/tui-pty-integration.test.tsx +159 -0
  411. package/test/ui/tui-stories.test.ts +332 -0
  412. package/test/unit/acceptance.test.ts +186 -0
  413. package/test/unit/agent-stderr-capture.test.ts +146 -0
  414. package/test/unit/analyze-classifier.test.ts +215 -0
  415. package/test/unit/analyze.test.ts +224 -0
  416. package/test/unit/auto-detect.test.ts +249 -0
  417. package/test/unit/cli-status.test.ts +417 -0
  418. package/test/unit/commands/common.test.ts +320 -0
  419. package/test/unit/commands/logs.test.ts +416 -0
  420. package/test/unit/commands/unlock.test.ts +319 -0
  421. package/test/unit/constitution-generators.test.ts +160 -0
  422. package/test/unit/constitution.test.ts +209 -0
  423. package/test/unit/context.test.ts +1722 -0
  424. package/test/unit/cost.test.ts +231 -0
  425. package/test/unit/crash-recovery.test.ts +308 -0
  426. package/test/unit/escalation.test.ts +126 -0
  427. package/test/unit/execution-logging-stderr.test.ts +156 -0
  428. package/test/unit/execution-stage.test.ts +122 -0
  429. package/test/unit/fix-generator.test.ts +275 -0
  430. package/test/unit/formatters.test.ts +469 -0
  431. package/test/unit/greenfield.test.ts +179 -0
  432. package/test/unit/helpers.test.ts +317 -0
  433. package/test/unit/interaction/human-review-trigger.test.ts +164 -0
  434. package/test/unit/interaction-network-failures.test.ts +389 -0
  435. package/test/unit/interaction-plugins.test.ts +164 -0
  436. package/test/unit/isolation.test.ts +134 -0
  437. package/test/unit/logging/formatter.test.ts +455 -0
  438. package/test/unit/merge.test.ts +268 -0
  439. package/test/unit/metrics.test.ts +276 -0
  440. package/test/unit/optimizer/noop.optimizer.test.ts +125 -0
  441. package/test/unit/optimizer/rule-based.optimizer.test.ts +358 -0
  442. package/test/unit/prd-auto-default.test.ts +290 -0
  443. package/test/unit/prd-failure-category.test.ts +176 -0
  444. package/test/unit/prd-get-next-story.test.ts +186 -0
  445. package/test/unit/precheck-checks.test.ts +840 -0
  446. package/test/unit/precheck-story-size-gate.test.ts +287 -0
  447. package/test/unit/precheck-types.test.ts +142 -0
  448. package/test/unit/prompts.test.ts +475 -0
  449. package/test/unit/queue.test.ts +237 -0
  450. package/test/unit/rectification.test.ts +284 -0
  451. package/test/unit/registry.test.ts +287 -0
  452. package/test/unit/routing.test.ts +937 -0
  453. package/test/unit/run-lifecycle.test.ts +140 -0
  454. package/test/unit/storyid-events.test.ts +224 -0
  455. package/test/unit/tdd-verdict.test.ts +492 -0
  456. package/test/unit/test-output-parser.test.ts +377 -0
  457. package/test/unit/verdict.test.ts +324 -0
  458. package/test/unit/worktree-manager.test.ts +158 -0
  459. package/tsconfig.json +27 -0
@@ -0,0 +1,231 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import {
3
+ COST_RATES,
4
+ estimateCost,
5
+ estimateCostByDuration,
6
+ estimateCostFromOutput,
7
+ formatCostWithConfidence,
8
+ parseTokenUsage,
9
+ } from "../../src/agents/cost";
10
+
11
+ describe("parseTokenUsage", () => {
12
+ test("parses Claude Code token output with estimated confidence", () => {
13
+ const output = `
14
+ Agent completed successfully.
15
+ Input tokens: 12345
16
+ Output tokens: 6789
17
+ Total tokens: 19134
18
+ `;
19
+ const usage = parseTokenUsage(output);
20
+ expect(usage).not.toBeNull();
21
+ expect(usage?.inputTokens).toBe(12345);
22
+ expect(usage?.outputTokens).toBe(6789);
23
+ expect(usage?.confidence).toBe("estimated");
24
+ });
25
+
26
+ test("handles case-insensitive matches with estimated confidence", () => {
27
+ const output = "INPUT TOKENS: 1000\nOUTPUT TOKENS: 2000";
28
+ const usage = parseTokenUsage(output);
29
+ expect(usage).not.toBeNull();
30
+ expect(usage?.inputTokens).toBe(1000);
31
+ expect(usage?.outputTokens).toBe(2000);
32
+ expect(usage?.confidence).toBe("estimated");
33
+ });
34
+
35
+ test("returns null when tokens not found", () => {
36
+ const output = "Agent completed successfully.";
37
+ expect(parseTokenUsage(output)).toBeNull();
38
+ });
39
+
40
+ test("returns null when only partial token info", () => {
41
+ const output = "Input tokens: 1000";
42
+ expect(parseTokenUsage(output)).toBeNull();
43
+ });
44
+
45
+ test("parses JSON-structured token report with exact confidence (BUG-3)", () => {
46
+ const output = `{"usage": {"input_tokens": 15000, "output_tokens": 8500}}`;
47
+ const usage = parseTokenUsage(output);
48
+ expect(usage).not.toBeNull();
49
+ expect(usage?.inputTokens).toBe(15000);
50
+ expect(usage?.outputTokens).toBe(8500);
51
+ expect(usage?.confidence).toBe("exact");
52
+ });
53
+
54
+ test("parses JSON with surrounding text with exact confidence (BUG-3)", () => {
55
+ const output = `
56
+ Agent completed.
57
+ {"usage": {"input_tokens": 12000, "output_tokens": 4000}}
58
+ Done.
59
+ `;
60
+ const usage = parseTokenUsage(output);
61
+ expect(usage).not.toBeNull();
62
+ expect(usage?.inputTokens).toBe(12000);
63
+ expect(usage?.outputTokens).toBe(4000);
64
+ expect(usage?.confidence).toBe("exact");
65
+ });
66
+
67
+ test("parses underscore format with estimated confidence (BUG-3)", () => {
68
+ const output = "input_tokens: 9000\noutput_tokens: 3000";
69
+ const usage = parseTokenUsage(output);
70
+ expect(usage).not.toBeNull();
71
+ expect(usage?.inputTokens).toBe(9000);
72
+ expect(usage?.outputTokens).toBe(3000);
73
+ expect(usage?.confidence).toBe("estimated");
74
+ });
75
+
76
+ test("rejects unreasonably large token counts (BUG-3 sanity check)", () => {
77
+ const output = "Input tokens: 5000000\nOutput tokens: 2000000";
78
+ // Should reject tokens > 1M as likely false positive
79
+ expect(parseTokenUsage(output)).toBeNull();
80
+ });
81
+
82
+ test("requires at least 2 digits to avoid false positives (BUG-3)", () => {
83
+ const output = "version: 1\ninput: 5\noutput: 8";
84
+ // Should not match single-digit numbers
85
+ expect(parseTokenUsage(output)).toBeNull();
86
+ });
87
+
88
+ test("handles mixed format with word boundaries and estimated confidence (BUG-3)", () => {
89
+ const output = `
90
+ Model: claude-sonnet-4-5
91
+ input tokens: 15432
92
+ output tokens: 7891
93
+ Status: success
94
+ `;
95
+ const usage = parseTokenUsage(output);
96
+ expect(usage).not.toBeNull();
97
+ expect(usage?.inputTokens).toBe(15432);
98
+ expect(usage?.outputTokens).toBe(7891);
99
+ expect(usage?.confidence).toBe("estimated");
100
+ });
101
+ });
102
+
103
+ describe("estimateCost", () => {
104
+ test("calculates cost for fast tier (Haiku)", () => {
105
+ const cost = estimateCost("fast", 1_000_000, 1_000_000);
106
+ // $0.80 input + $4.00 output = $4.80
107
+ expect(cost).toBeCloseTo(4.8, 2);
108
+ });
109
+
110
+ test("calculates cost for balanced tier (Sonnet)", () => {
111
+ const cost = estimateCost("balanced", 1_000_000, 1_000_000);
112
+ // $3.00 input + $15.00 output = $18.00
113
+ expect(cost).toBeCloseTo(18.0, 2);
114
+ });
115
+
116
+ test("calculates cost for powerful tier (Opus)", () => {
117
+ const cost = estimateCost("powerful", 1_000_000, 1_000_000);
118
+ // $15.00 input + $75.00 output = $90.00
119
+ expect(cost).toBeCloseTo(90.0, 2);
120
+ });
121
+
122
+ test("handles small token counts", () => {
123
+ const cost = estimateCost("fast", 10_000, 5_000);
124
+ // (10k/1M * 0.80) + (5k/1M * 4.00) = 0.008 + 0.020 = 0.028
125
+ expect(cost).toBeCloseTo(0.028, 3);
126
+ });
127
+
128
+ test("handles zero tokens", () => {
129
+ const cost = estimateCost("balanced", 0, 0);
130
+ expect(cost).toBe(0);
131
+ });
132
+ });
133
+
134
+ describe("estimateCostFromOutput", () => {
135
+ test("estimates cost from parsed output with confidence", () => {
136
+ const output = "Input tokens: 100000\nOutput tokens: 50000";
137
+ const estimate = estimateCostFromOutput("fast", output);
138
+ // (100k/1M * 0.80) + (50k/1M * 4.00) = 0.08 + 0.20 = 0.28
139
+ expect(estimate).not.toBeNull();
140
+ expect(estimate?.cost).toBeCloseTo(0.28, 2);
141
+ expect(estimate?.confidence).toBe("estimated");
142
+ });
143
+
144
+ test("returns exact confidence for JSON output", () => {
145
+ const output = '{"usage": {"input_tokens": 100000, "output_tokens": 50000}}';
146
+ const estimate = estimateCostFromOutput("fast", output);
147
+ expect(estimate).not.toBeNull();
148
+ expect(estimate?.cost).toBeCloseTo(0.28, 2);
149
+ expect(estimate?.confidence).toBe("exact");
150
+ });
151
+
152
+ test("returns null when tokens cannot be parsed", () => {
153
+ const output = "Agent completed successfully.";
154
+ const estimate = estimateCostFromOutput("balanced", output);
155
+ expect(estimate).toBeNull();
156
+ });
157
+ });
158
+
159
+ describe("estimateCostByDuration", () => {
160
+ test("estimates cost for 1 minute fast tier with fallback confidence", () => {
161
+ const estimate = estimateCostByDuration("fast", 60000);
162
+ expect(estimate.cost).toBeCloseTo(0.01, 2);
163
+ expect(estimate.confidence).toBe("fallback");
164
+ });
165
+
166
+ test("estimates cost for 2 minutes balanced tier with fallback confidence", () => {
167
+ const estimate = estimateCostByDuration("balanced", 120000);
168
+ expect(estimate.cost).toBeCloseTo(0.1, 2);
169
+ expect(estimate.confidence).toBe("fallback");
170
+ });
171
+
172
+ test("estimates cost for 30 seconds powerful tier with fallback confidence", () => {
173
+ const estimate = estimateCostByDuration("powerful", 30000);
174
+ expect(estimate.cost).toBeCloseTo(0.075, 3);
175
+ expect(estimate.confidence).toBe("fallback");
176
+ });
177
+
178
+ test("handles zero duration with fallback confidence", () => {
179
+ const estimate = estimateCostByDuration("balanced", 0);
180
+ expect(estimate.cost).toBe(0);
181
+ expect(estimate.confidence).toBe("fallback");
182
+ });
183
+ });
184
+
185
+ describe("formatCostWithConfidence", () => {
186
+ test("formats exact confidence without prefix", () => {
187
+ const estimate = { cost: 0.12, confidence: "exact" as const };
188
+ expect(formatCostWithConfidence(estimate)).toBe("$0.12");
189
+ });
190
+
191
+ test("formats estimated confidence with tilde prefix", () => {
192
+ const estimate = { cost: 0.15, confidence: "estimated" as const };
193
+ expect(formatCostWithConfidence(estimate)).toBe("~$0.15");
194
+ });
195
+
196
+ test("formats fallback confidence with tilde and label", () => {
197
+ const estimate = { cost: 0.05, confidence: "fallback" as const };
198
+ expect(formatCostWithConfidence(estimate)).toBe("~$0.05 (duration-based)");
199
+ });
200
+
201
+ test("formats very small costs correctly", () => {
202
+ const estimate = { cost: 0.001, confidence: "exact" as const };
203
+ expect(formatCostWithConfidence(estimate)).toBe("$0.00");
204
+ });
205
+
206
+ test("formats large costs correctly", () => {
207
+ const estimate = { cost: 12.345, confidence: "estimated" as const };
208
+ expect(formatCostWithConfidence(estimate)).toBe("~$12.35");
209
+ });
210
+ });
211
+
212
+ describe("COST_RATES", () => {
213
+ test("has rates for all model tiers", () => {
214
+ expect(COST_RATES.fast).toBeDefined();
215
+ expect(COST_RATES.balanced).toBeDefined();
216
+ expect(COST_RATES.powerful).toBeDefined();
217
+ });
218
+
219
+ test("rates are positive numbers", () => {
220
+ for (const tier of ["fast", "balanced", "powerful"] as const) {
221
+ expect(COST_RATES[tier].inputPer1M).toBeGreaterThan(0);
222
+ expect(COST_RATES[tier].outputPer1M).toBeGreaterThan(0);
223
+ }
224
+ });
225
+
226
+ test("output costs are higher than input costs", () => {
227
+ for (const tier of ["fast", "balanced", "powerful"] as const) {
228
+ expect(COST_RATES[tier].outputPer1M).toBeGreaterThan(COST_RATES[tier].inputPer1M);
229
+ }
230
+ });
231
+ });
@@ -0,0 +1,308 @@
1
+ /**
2
+ * Unit tests for crash recovery module (US-007)
3
+ */
4
+
5
+ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
6
+ import { existsSync, mkdirSync, rmSync } from "node:fs";
7
+ import { join } from "node:path";
8
+ import { DEFAULT_CONFIG } from "../../src/config";
9
+ import {
10
+ type CrashRecoveryContext,
11
+ installCrashHandlers,
12
+ resetCrashHandlers,
13
+ startHeartbeat,
14
+ stopHeartbeat,
15
+ writeExitSummary,
16
+ } from "../../src/execution/crash-recovery";
17
+ import { StatusWriter } from "../../src/execution/status-writer";
18
+
19
+ const TEST_DIR = join(import.meta.dir, "..", ".tmp-crash-recovery");
20
+ const TEST_JSONL = join(TEST_DIR, "test.jsonl");
21
+ const TEST_STATUS_FILE = join(TEST_DIR, "status.json");
22
+
23
+ beforeEach(() => {
24
+ // Create test directory
25
+ if (existsSync(TEST_DIR)) {
26
+ rmSync(TEST_DIR, { recursive: true, force: true });
27
+ }
28
+ mkdirSync(TEST_DIR, { recursive: true });
29
+
30
+ // Reset crash handlers before each test
31
+ resetCrashHandlers();
32
+ });
33
+
34
+ afterEach(() => {
35
+ // Reset crash handlers after each test
36
+ resetCrashHandlers();
37
+
38
+ // Clean up test directory
39
+ if (existsSync(TEST_DIR)) {
40
+ rmSync(TEST_DIR, { recursive: true, force: true });
41
+ }
42
+ });
43
+
44
+ describe("crash-recovery", () => {
45
+ describe("installCrashHandlers", () => {
46
+ test("should install handlers without throwing", () => {
47
+ const statusWriter = new StatusWriter(TEST_STATUS_FILE, DEFAULT_CONFIG, {
48
+ runId: "test-run",
49
+ feature: "test-feature",
50
+ startedAt: new Date().toISOString(),
51
+ dryRun: false,
52
+ startTimeMs: Date.now(),
53
+ pid: process.pid,
54
+ });
55
+
56
+ const ctx: CrashRecoveryContext = {
57
+ statusWriter,
58
+ getTotalCost: () => 0,
59
+ getIterations: () => 0,
60
+ jsonlFilePath: TEST_JSONL,
61
+ };
62
+
63
+ expect(() => installCrashHandlers(ctx)).not.toThrow();
64
+ });
65
+
66
+ test("should not throw when called multiple times", () => {
67
+ const statusWriter = new StatusWriter(TEST_STATUS_FILE, DEFAULT_CONFIG, {
68
+ runId: "test-run",
69
+ feature: "test-feature",
70
+ startedAt: new Date().toISOString(),
71
+ dryRun: false,
72
+ startTimeMs: Date.now(),
73
+ pid: process.pid,
74
+ });
75
+
76
+ const ctx: CrashRecoveryContext = {
77
+ statusWriter,
78
+ getTotalCost: () => 0,
79
+ getIterations: () => 0,
80
+ jsonlFilePath: TEST_JSONL,
81
+ };
82
+
83
+ installCrashHandlers(ctx);
84
+ expect(() => installCrashHandlers(ctx)).not.toThrow();
85
+ });
86
+ });
87
+
88
+ describe("heartbeat", () => {
89
+ test("should start and stop heartbeat without throwing", () => {
90
+ const statusWriter = new StatusWriter(TEST_STATUS_FILE, DEFAULT_CONFIG, {
91
+ runId: "test-run",
92
+ feature: "test-feature",
93
+ startedAt: new Date().toISOString(),
94
+ dryRun: false,
95
+ startTimeMs: Date.now(),
96
+ pid: process.pid,
97
+ });
98
+
99
+ const totalCost = 0;
100
+ const iterations = 0;
101
+
102
+ expect(() =>
103
+ startHeartbeat(
104
+ statusWriter,
105
+ () => totalCost,
106
+ () => iterations,
107
+ TEST_JSONL,
108
+ ),
109
+ ).not.toThrow();
110
+
111
+ expect(() => stopHeartbeat()).not.toThrow();
112
+ });
113
+
114
+ test("should write heartbeat entry after interval", async () => {
115
+ const statusWriter = new StatusWriter(TEST_STATUS_FILE, DEFAULT_CONFIG, {
116
+ runId: "test-run",
117
+ feature: "test-feature",
118
+ startedAt: new Date().toISOString(),
119
+ dryRun: false,
120
+ startTimeMs: Date.now(),
121
+ pid: process.pid,
122
+ });
123
+
124
+ statusWriter.setPrd({
125
+ version: 1,
126
+ feature: "test-feature",
127
+ userStories: [],
128
+ });
129
+
130
+ const totalCost = 0;
131
+ const iterations = 0;
132
+
133
+ startHeartbeat(
134
+ statusWriter,
135
+ () => totalCost,
136
+ () => iterations,
137
+ TEST_JSONL,
138
+ );
139
+
140
+ // Wait for one heartbeat cycle (60s in production, but we can't wait that long in tests)
141
+ // This test just verifies no crash during startup
142
+ await Bun.sleep(100);
143
+
144
+ stopHeartbeat();
145
+
146
+ // Verify no crash (test passes if we reach here)
147
+ expect(true).toBe(true);
148
+ });
149
+
150
+ test("should stop heartbeat idempotently", () => {
151
+ expect(() => stopHeartbeat()).not.toThrow();
152
+ expect(() => stopHeartbeat()).not.toThrow();
153
+ });
154
+ });
155
+
156
+ describe("writeExitSummary", () => {
157
+ test("should write exit summary to JSONL file", async () => {
158
+ await writeExitSummary(TEST_JSONL, 1.23, 5, 3, 60000);
159
+
160
+ const file = Bun.file(TEST_JSONL);
161
+ expect(await file.exists()).toBe(true);
162
+
163
+ const content = await file.text();
164
+ const lines = content.trim().split("\n");
165
+ expect(lines.length).toBe(1);
166
+
167
+ const entry = JSON.parse(lines[0]);
168
+ expect(entry.level).toBe("info");
169
+ expect(entry.stage).toBe("exit-summary");
170
+ expect(entry.message).toBe("Run completed");
171
+ expect(entry.data.totalCost).toBe(1.23);
172
+ expect(entry.data.iterations).toBe(5);
173
+ expect(entry.data.storiesCompleted).toBe(3);
174
+ expect(entry.data.durationMs).toBe(60000);
175
+ expect(entry.data.exitedCleanly).toBe(true);
176
+ });
177
+
178
+ test("should not throw when JSONL path is undefined", async () => {
179
+ await expect(writeExitSummary(undefined, 1.23, 5, 3, 60000)).resolves.toBeUndefined();
180
+ });
181
+
182
+ test("should include timestamp in exit summary", async () => {
183
+ const beforeTime = new Date().toISOString();
184
+ await writeExitSummary(TEST_JSONL, 1.23, 5, 3, 60000);
185
+ const afterTime = new Date().toISOString();
186
+
187
+ const file = Bun.file(TEST_JSONL);
188
+ const content = await file.text();
189
+ const entry = JSON.parse(content.trim());
190
+
191
+ expect(entry.timestamp).toBeDefined();
192
+ expect(entry.timestamp >= beforeTime).toBe(true);
193
+ expect(entry.timestamp <= afterTime).toBe(true);
194
+ });
195
+ });
196
+
197
+ describe("resetCrashHandlers", () => {
198
+ test("should reset handlers and stop heartbeat", () => {
199
+ const statusWriter = new StatusWriter(TEST_STATUS_FILE, DEFAULT_CONFIG, {
200
+ runId: "test-run",
201
+ feature: "test-feature",
202
+ startedAt: new Date().toISOString(),
203
+ dryRun: false,
204
+ startTimeMs: Date.now(),
205
+ pid: process.pid,
206
+ });
207
+
208
+ const ctx: CrashRecoveryContext = {
209
+ statusWriter,
210
+ getTotalCost: () => 0,
211
+ getIterations: () => 0,
212
+ jsonlFilePath: TEST_JSONL,
213
+ };
214
+
215
+ installCrashHandlers(ctx);
216
+ startHeartbeat(
217
+ statusWriter,
218
+ () => 0,
219
+ () => 0,
220
+ TEST_JSONL,
221
+ );
222
+
223
+ expect(() => resetCrashHandlers()).not.toThrow();
224
+
225
+ // After reset, should be able to install handlers again
226
+ expect(() => installCrashHandlers(ctx)).not.toThrow();
227
+ });
228
+ });
229
+
230
+ describe("SIGTERM run.complete event (BUG-017)", () => {
231
+ test("should emit run.complete event when SIGTERM handler is invoked directly", async () => {
232
+ // Mock process.exit to prevent the test process from actually exiting
233
+ const originalExit = process.exit;
234
+ let exitCalled = false;
235
+ let exitCode: number | undefined;
236
+ process.exit = ((code?: number) => {
237
+ exitCalled = true;
238
+ exitCode = code;
239
+ }) as typeof process.exit;
240
+
241
+ const statusWriter = new StatusWriter(TEST_STATUS_FILE, DEFAULT_CONFIG, {
242
+ runId: "run-sigterm-mock",
243
+ feature: "mock-feature",
244
+ startedAt: new Date().toISOString(),
245
+ dryRun: false,
246
+ startTimeMs: Date.now(),
247
+ pid: process.pid,
248
+ });
249
+
250
+ statusWriter.setPrd({
251
+ version: 1,
252
+ feature: "mock-feature",
253
+ userStories: [],
254
+ });
255
+
256
+ const startTime = Date.now() - 5000;
257
+ const ctx: CrashRecoveryContext = {
258
+ statusWriter,
259
+ getTotalCost: () => 2.5,
260
+ getIterations: () => 4,
261
+ jsonlFilePath: TEST_JSONL,
262
+ runId: "run-sigterm-mock",
263
+ feature: "mock-feature",
264
+ getStartTime: () => startTime,
265
+ getTotalStories: () => 10,
266
+ getStoriesCompleted: () => 3,
267
+ };
268
+
269
+ const cleanup = installCrashHandlers(ctx);
270
+
271
+ // Capture the SIGTERM listener registered by installCrashHandlers
272
+ const sigtermListeners = process.listeners("SIGTERM");
273
+ const sigtermHandler = sigtermListeners[sigtermListeners.length - 1] as () => Promise<void>;
274
+
275
+ // Invoke the handler directly — no real signal, no subprocess
276
+ await sigtermHandler();
277
+
278
+ // Restore process.exit before assertions
279
+ process.exit = originalExit;
280
+ cleanup();
281
+
282
+ // Assert process.exit was called with the correct SIGTERM exit code
283
+ expect(exitCalled).toBe(true);
284
+ expect(exitCode).toBe(128 + 15); // SIGTERM = signal 15
285
+
286
+ // Assert run.complete event was written to JSONL
287
+ const file = Bun.file(TEST_JSONL);
288
+ expect(await file.exists()).toBe(true);
289
+ const content = await file.text();
290
+ const lines = content.trim().split("\n").filter(Boolean);
291
+ const entries = lines.map((line: string) => JSON.parse(line));
292
+ const runCompleteEvent = entries.find((e: { stage: string }) => e.stage === "run.complete");
293
+
294
+ expect(runCompleteEvent).toBeDefined();
295
+ expect(runCompleteEvent.level).toBe("info");
296
+ expect(runCompleteEvent.message).toBe("Feature execution terminated");
297
+ expect(runCompleteEvent.data.runId).toBe("run-sigterm-mock");
298
+ expect(runCompleteEvent.data.feature).toBe("mock-feature");
299
+ expect(runCompleteEvent.data.success).toBe(false);
300
+ expect(runCompleteEvent.data.exitReason).toBe("sigterm");
301
+ expect(runCompleteEvent.data.totalCost).toBe(2.5);
302
+ expect(runCompleteEvent.data.iterations).toBe(4);
303
+ expect(runCompleteEvent.data.totalStories).toBe(10);
304
+ expect(runCompleteEvent.data.storiesCompleted).toBe(3);
305
+ expect(runCompleteEvent.data.durationMs).toBeGreaterThanOrEqual(0);
306
+ });
307
+ });
308
+ });
@@ -0,0 +1,126 @@
1
+ /**
2
+ * Tests for src/execution/escalation.ts
3
+ *
4
+ * Covers: escalateTier, getTierConfig, calculateMaxIterations
5
+ */
6
+
7
+ import { describe, expect, it } from "bun:test";
8
+ import type { TierConfig } from "../../src/config";
9
+ import { calculateMaxIterations, escalateTier, getTierConfig } from "../../src/execution/escalation";
10
+
11
+ // ─────────────────────────────────────────────────────────────────────────────
12
+ // Test fixtures
13
+ // ─────────────────────────────────────────────────────────────────────────────
14
+
15
+ const defaultTierOrder: TierConfig[] = [
16
+ { tier: "fast", attempts: 5 },
17
+ { tier: "balanced", attempts: 3 },
18
+ { tier: "powerful", attempts: 2 },
19
+ ];
20
+
21
+ const customTierOrder: TierConfig[] = [
22
+ { tier: "haiku", attempts: 10 },
23
+ { tier: "sonnet", attempts: 5 },
24
+ { tier: "opus", attempts: 2 },
25
+ ];
26
+
27
+ // ─────────────────────────────────────────────────────────────────────────────
28
+ // escalateTier
29
+ // ─────────────────────────────────────────────────────────────────────────────
30
+
31
+ describe("escalateTier", () => {
32
+ it("returns next tier in order when not at max", () => {
33
+ expect(escalateTier("fast", defaultTierOrder)).toBe("balanced");
34
+ expect(escalateTier("balanced", defaultTierOrder)).toBe("powerful");
35
+ });
36
+
37
+ it("returns null when at max tier", () => {
38
+ expect(escalateTier("powerful", defaultTierOrder)).toBeNull();
39
+ });
40
+
41
+ it("returns null when tier not found in order", () => {
42
+ expect(escalateTier("unknown", defaultTierOrder)).toBeNull();
43
+ });
44
+
45
+ it("handles single-tier order", () => {
46
+ const singleTier: TierConfig[] = [{ tier: "only", attempts: 10 }];
47
+ expect(escalateTier("only", singleTier)).toBeNull();
48
+ });
49
+
50
+ it("works with custom tier names", () => {
51
+ expect(escalateTier("haiku", customTierOrder)).toBe("sonnet");
52
+ expect(escalateTier("sonnet", customTierOrder)).toBe("opus");
53
+ expect(escalateTier("opus", customTierOrder)).toBeNull();
54
+ });
55
+
56
+ it("returns null for empty tier order", () => {
57
+ expect(escalateTier("fast", [])).toBeNull();
58
+ });
59
+ });
60
+
61
+ // ─────────────────────────────────────────────────────────────────────────────
62
+ // getTierConfig
63
+ // ─────────────────────────────────────────────────────────────────────────────
64
+
65
+ describe("getTierConfig", () => {
66
+ it("returns tier config when tier exists", () => {
67
+ const config = getTierConfig("balanced", defaultTierOrder);
68
+ expect(config).toEqual({ tier: "balanced", attempts: 3 });
69
+ });
70
+
71
+ it("returns undefined when tier not found", () => {
72
+ expect(getTierConfig("unknown", defaultTierOrder)).toBeUndefined();
73
+ });
74
+
75
+ it("handles first tier", () => {
76
+ const config = getTierConfig("fast", defaultTierOrder);
77
+ expect(config).toEqual({ tier: "fast", attempts: 5 });
78
+ });
79
+
80
+ it("handles last tier", () => {
81
+ const config = getTierConfig("powerful", defaultTierOrder);
82
+ expect(config).toEqual({ tier: "powerful", attempts: 2 });
83
+ });
84
+
85
+ it("returns undefined for empty tier order", () => {
86
+ expect(getTierConfig("fast", [])).toBeUndefined();
87
+ });
88
+ });
89
+
90
+ // ─────────────────────────────────────────────────────────────────────────────
91
+ // calculateMaxIterations
92
+ // ─────────────────────────────────────────────────────────────────────────────
93
+
94
+ describe("calculateMaxIterations", () => {
95
+ it("sums all tier attempts", () => {
96
+ // 5 + 3 + 2 = 10
97
+ expect(calculateMaxIterations(defaultTierOrder)).toBe(10);
98
+ });
99
+
100
+ it("handles single tier", () => {
101
+ const singleTier: TierConfig[] = [{ tier: "only", attempts: 7 }];
102
+ expect(calculateMaxIterations(singleTier)).toBe(7);
103
+ });
104
+
105
+ it("returns 0 for empty tier order", () => {
106
+ expect(calculateMaxIterations([])).toBe(0);
107
+ });
108
+
109
+ it("handles large attempt counts", () => {
110
+ const largeTiers: TierConfig[] = [
111
+ { tier: "a", attempts: 100 },
112
+ { tier: "b", attempts: 200 },
113
+ { tier: "c", attempts: 150 },
114
+ ];
115
+ expect(calculateMaxIterations(largeTiers)).toBe(450);
116
+ });
117
+
118
+ it("handles zero attempts", () => {
119
+ const zeroTiers: TierConfig[] = [
120
+ { tier: "a", attempts: 0 },
121
+ { tier: "b", attempts: 5 },
122
+ { tier: "c", attempts: 0 },
123
+ ];
124
+ expect(calculateMaxIterations(zeroTiers)).toBe(5);
125
+ });
126
+ });