@intentius/chant 0.57.0 → 0.59.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 (558) hide show
  1. package/dist/audit/catalog.d.ts +51 -1
  2. package/dist/audit/catalog.d.ts.map +1 -1
  3. package/dist/audit/core.d.ts +36 -1
  4. package/dist/audit/core.d.ts.map +1 -1
  5. package/dist/audit/discover.d.ts +26 -9
  6. package/dist/audit/discover.d.ts.map +1 -1
  7. package/dist/audit/lineage.d.ts +10 -0
  8. package/dist/audit/lineage.d.ts.map +1 -0
  9. package/dist/audit/prior-art.d.ts +385 -0
  10. package/dist/audit/prior-art.d.ts.map +1 -0
  11. package/dist/audit/proof.d.ts +16 -0
  12. package/dist/audit/proof.d.ts.map +1 -1
  13. package/dist/audit/report-html.d.ts.map +1 -1
  14. package/dist/audit/report-model.d.ts +8 -1
  15. package/dist/audit/report-model.d.ts.map +1 -1
  16. package/dist/audit/report.d.ts.map +1 -1
  17. package/dist/audit/rules-doc.d.ts +3 -0
  18. package/dist/audit/rules-doc.d.ts.map +1 -1
  19. package/dist/audit/secrets.d.ts +15 -0
  20. package/dist/audit/secrets.d.ts.map +1 -1
  21. package/dist/audit/terraform-state.d.ts +46 -0
  22. package/dist/audit/terraform-state.d.ts.map +1 -0
  23. package/dist/build.d.ts +17 -1
  24. package/dist/build.d.ts.map +1 -1
  25. package/dist/claimed-fields.d.ts +161 -0
  26. package/dist/claimed-fields.d.ts.map +1 -0
  27. package/dist/cli/build-params-cli.d.ts +2 -2
  28. package/dist/cli/command-group.d.ts +14 -0
  29. package/dist/cli/command-group.d.ts.map +1 -1
  30. package/dist/cli/commands/audit.d.ts +17 -1
  31. package/dist/cli/commands/audit.d.ts.map +1 -1
  32. package/dist/cli/commands/build.d.ts.map +1 -1
  33. package/dist/cli/commands/carve-bridge.d.ts.map +1 -1
  34. package/dist/cli/commands/check-lexicon-docs.d.ts +1 -1
  35. package/dist/cli/commands/check-lexicon-examples.d.ts +16 -6
  36. package/dist/cli/commands/check-lexicon-examples.d.ts.map +1 -1
  37. package/dist/cli/commands/init-lexicon/templates/codegen.d.ts +1 -0
  38. package/dist/cli/commands/init-lexicon/templates/codegen.d.ts.map +1 -1
  39. package/dist/cli/commands/init-lexicon/templates/project.d.ts +7 -0
  40. package/dist/cli/commands/init-lexicon/templates/project.d.ts.map +1 -1
  41. package/dist/cli/commands/init-lexicon.d.ts.map +1 -1
  42. package/dist/cli/commands/init.d.ts.map +1 -1
  43. package/dist/cli/commands/lint.d.ts +13 -0
  44. package/dist/cli/commands/lint.d.ts.map +1 -1
  45. package/dist/cli/commands/onboard.d.ts +2 -0
  46. package/dist/cli/commands/onboard.d.ts.map +1 -1
  47. package/dist/cli/handlers/build.d.ts.map +1 -1
  48. package/dist/cli/handlers/lifecycle.d.ts.map +1 -1
  49. package/dist/cli/handlers/operator.d.ts +82 -16
  50. package/dist/cli/handlers/operator.d.ts.map +1 -1
  51. package/dist/cli/handlers/run.d.ts +49 -42
  52. package/dist/cli/handlers/run.d.ts.map +1 -1
  53. package/dist/cli/main.d.ts +12 -1
  54. package/dist/cli/main.d.ts.map +1 -1
  55. package/dist/cli/mcp/op-tools.d.ts +16 -1
  56. package/dist/cli/mcp/op-tools.d.ts.map +1 -1
  57. package/dist/cli/mcp/resource-handlers.d.ts.map +1 -1
  58. package/dist/cli/mcp/resources/context.d.ts.map +1 -1
  59. package/dist/cli/plugins.d.ts +22 -0
  60. package/dist/cli/plugins.d.ts.map +1 -1
  61. package/dist/cli/registry.d.ts +40 -9
  62. package/dist/cli/registry.d.ts.map +1 -1
  63. package/dist/codegen/generate-post-synth-barrel.d.ts +1 -1
  64. package/dist/components/auto-release.d.ts +10 -20
  65. package/dist/components/auto-release.d.ts.map +1 -1
  66. package/dist/components/capability.d.ts +4 -4
  67. package/dist/components/cli-support.d.ts +24 -22
  68. package/dist/components/cli-support.d.ts.map +1 -1
  69. package/dist/components/component.d.ts +21 -6
  70. package/dist/components/component.d.ts.map +1 -1
  71. package/dist/components/driver-output.d.ts.map +1 -1
  72. package/dist/components/driver.d.ts +73 -37
  73. package/dist/components/driver.d.ts.map +1 -1
  74. package/dist/components/index.d.ts +1 -2
  75. package/dist/components/index.d.ts.map +1 -1
  76. package/dist/components/manifest-persistence.d.ts +1 -9
  77. package/dist/components/manifest-persistence.d.ts.map +1 -1
  78. package/dist/components/run-progress.d.ts +6 -6
  79. package/dist/components/run-progress.d.ts.map +1 -1
  80. package/dist/components/verbs/run-agent.d.ts +10 -22
  81. package/dist/components/verbs/run-agent.d.ts.map +1 -1
  82. package/dist/components/verbs/verify.d.ts +2 -2
  83. package/dist/deep-observation.d.ts +17 -0
  84. package/dist/deep-observation.d.ts.map +1 -1
  85. package/dist/detectLexicon.d.ts +13 -0
  86. package/dist/detectLexicon.d.ts.map +1 -1
  87. package/dist/discovery/files.d.ts +4 -4
  88. package/dist/discovery/fold-import.d.ts.map +1 -1
  89. package/dist/discovery/param-deps.d.ts +33 -0
  90. package/dist/discovery/param-deps.d.ts.map +1 -1
  91. package/dist/discovery/sandbox/bundle.d.ts.map +1 -1
  92. package/dist/discovery/sandbox/config-wire.d.ts +2 -2
  93. package/dist/fold/foldable-helpers.d.ts +5 -1
  94. package/dist/fold/foldable-helpers.d.ts.map +1 -1
  95. package/dist/fold-provenance.d.ts +236 -0
  96. package/dist/fold-provenance.d.ts.map +1 -0
  97. package/dist/graph-ops.d.ts +1 -1
  98. package/dist/held-elsewhere.d.ts +85 -0
  99. package/dist/held-elsewhere.d.ts.map +1 -0
  100. package/dist/index.d.ts +3 -0
  101. package/dist/index.d.ts.map +1 -1
  102. package/dist/kubectl-context.d.ts +3 -3
  103. package/dist/lexicon-config.d.ts +4 -4
  104. package/dist/lexicon-plugin-helpers.d.ts +1 -1
  105. package/dist/lexicon.d.ts +273 -16
  106. package/dist/lexicon.d.ts.map +1 -1
  107. package/dist/lifecycle/change-set.d.ts +13 -0
  108. package/dist/lifecycle/change-set.d.ts.map +1 -1
  109. package/dist/lifecycle/deep-diff.d.ts +133 -6
  110. package/dist/lifecycle/deep-diff.d.ts.map +1 -1
  111. package/dist/lifecycle/deep-observe.d.ts.map +1 -1
  112. package/dist/lifecycle/gate-ledger.d.ts +79 -3
  113. package/dist/lifecycle/gate-ledger.d.ts.map +1 -1
  114. package/dist/lifecycle/index.d.ts +1 -0
  115. package/dist/lifecycle/index.d.ts.map +1 -1
  116. package/dist/lifecycle/release-ledger.d.ts +2 -2
  117. package/dist/lifecycle/run-ledger.d.ts +53 -0
  118. package/dist/lifecycle/run-ledger.d.ts.map +1 -0
  119. package/dist/lifecycle/symptoms.d.ts +3 -3
  120. package/dist/lifecycle/symptoms.d.ts.map +1 -1
  121. package/dist/lint/config.d.ts +81 -2
  122. package/dist/lint/config.d.ts.map +1 -1
  123. package/dist/lint/post-synth.d.ts +55 -2
  124. package/dist/lint/post-synth.d.ts.map +1 -1
  125. package/dist/lint/rules/__fixtures__/comp/comp004/pass/neo4j-cluster.component.d.ts +1 -1
  126. package/dist/lint/rules/__fixtures__/comp/comp004/pass/neo4j-cluster.component.d.ts.map +1 -1
  127. package/dist/lint/rules/__fixtures__/comp/comp004/pass/orders-table.component.d.ts +1 -1
  128. package/dist/lint/rules/__fixtures__/comp/comp004/pass/orders-table.component.d.ts.map +1 -1
  129. package/dist/lint/rules/comp/comp004-gate-needs-durable-runtime.d.ts +44 -0
  130. package/dist/lint/rules/comp/comp004-gate-needs-durable-runtime.d.ts.map +1 -0
  131. package/dist/lint/rules/comp/index.d.ts +1 -1
  132. package/dist/lint/rules/comp/index.d.ts.map +1 -1
  133. package/dist/lint/rules/op/index.d.ts +16 -0
  134. package/dist/lint/rules/op/index.d.ts.map +1 -0
  135. package/dist/lint/rules/op/ops012-activity-contract.d.ts +27 -0
  136. package/dist/lint/rules/op/ops012-activity-contract.d.ts.map +1 -0
  137. package/dist/lint/rules/op/ops013-step-output-ref.d.ts +61 -0
  138. package/dist/lint/rules/op/ops013-step-output-ref.d.ts.map +1 -0
  139. package/dist/lint/rules/op/ops014-converge-rule-refusals.d.ts +55 -0
  140. package/dist/lint/rules/op/ops014-converge-rule-refusals.d.ts.map +1 -0
  141. package/dist/lint/rules/op/support.d.ts +12 -0
  142. package/dist/lint/rules/op/support.d.ts.map +1 -0
  143. package/dist/lint/suppressions.d.ts +108 -0
  144. package/dist/lint/suppressions.d.ts.map +1 -0
  145. package/dist/live-endpoint.d.ts +1 -1
  146. package/dist/op/activities/activity-contracts.d.ts +54 -0
  147. package/dist/op/activities/activity-contracts.d.ts.map +1 -0
  148. package/dist/op/activities/apply.d.ts +262 -0
  149. package/dist/op/activities/apply.d.ts.map +1 -0
  150. package/dist/op/activities/build.d.ts +13 -0
  151. package/dist/op/activities/build.d.ts.map +1 -0
  152. package/dist/op/activities/converge.d.ts +110 -0
  153. package/dist/op/activities/converge.d.ts.map +1 -0
  154. package/dist/op/activities/env-teardown.d.ts +76 -0
  155. package/dist/op/activities/env-teardown.d.ts.map +1 -0
  156. package/dist/op/activities/guard-validate.d.ts +64 -0
  157. package/dist/op/activities/guard-validate.d.ts.map +1 -0
  158. package/dist/op/activities/http-check.d.ts +34 -0
  159. package/dist/op/activities/http-check.d.ts.map +1 -0
  160. package/dist/op/activities/index.d.ts +31 -0
  161. package/dist/op/activities/index.d.ts.map +1 -0
  162. package/dist/op/activities/lexicon-upgrade.d.ts +182 -0
  163. package/dist/op/activities/lexicon-upgrade.d.ts.map +1 -0
  164. package/dist/op/activities/lifecycle.d.ts +42 -0
  165. package/dist/op/activities/lifecycle.d.ts.map +1 -0
  166. package/dist/op/activities/pipeline-audit.d.ts +84 -0
  167. package/dist/op/activities/pipeline-audit.d.ts.map +1 -0
  168. package/dist/op/activities/policy.d.ts +17 -0
  169. package/dist/op/activities/policy.d.ts.map +1 -0
  170. package/dist/op/activities/reconcile.d.ts +149 -0
  171. package/dist/op/activities/reconcile.d.ts.map +1 -0
  172. package/dist/op/activities/shell.d.ts +13 -0
  173. package/dist/op/activities/shell.d.ts.map +1 -0
  174. package/dist/op/activities/teardown.d.ts +10 -0
  175. package/dist/op/activities/teardown.d.ts.map +1 -0
  176. package/dist/op/activities/util.d.ts +7 -0
  177. package/dist/op/activities/util.d.ts.map +1 -0
  178. package/dist/op/activities/wait.d.ts +28 -0
  179. package/dist/op/activities/wait.d.ts.map +1 -0
  180. package/dist/op/activities/workflow-audit.d.ts +92 -0
  181. package/dist/op/activities/workflow-audit.d.ts.map +1 -0
  182. package/dist/op/activity-contract-registry.d.ts +76 -0
  183. package/dist/op/activity-contract-registry.d.ts.map +1 -0
  184. package/dist/op/activity-contract.d.ts +25 -18
  185. package/dist/op/activity-contract.d.ts.map +1 -1
  186. package/dist/op/activity-failure.d.ts +28 -0
  187. package/dist/op/activity-failure.d.ts.map +1 -0
  188. package/dist/op/activity-profiles.d.ts +118 -0
  189. package/dist/op/activity-profiles.d.ts.map +1 -0
  190. package/dist/op/activity-registry.d.ts +22 -38
  191. package/dist/op/activity-registry.d.ts.map +1 -1
  192. package/dist/op/activity-runtime.d.ts +3 -14
  193. package/dist/op/activity-runtime.d.ts.map +1 -1
  194. package/dist/op/builders.d.ts +85 -30
  195. package/dist/op/builders.d.ts.map +1 -1
  196. package/dist/op/change-signal.d.ts +91 -0
  197. package/dist/op/change-signal.d.ts.map +1 -0
  198. package/dist/op/composites/apply-op.d.ts +118 -0
  199. package/dist/op/composites/apply-op.d.ts.map +1 -0
  200. package/dist/op/composites/converge-op.d.ts +120 -0
  201. package/dist/op/composites/converge-op.d.ts.map +1 -0
  202. package/dist/op/composites/index.d.ts +26 -0
  203. package/dist/op/composites/index.d.ts.map +1 -0
  204. package/dist/op/composites/lexicon-upgrade-op.d.ts +78 -0
  205. package/dist/op/composites/lexicon-upgrade-op.d.ts.map +1 -0
  206. package/dist/op/composites/pipeline-audit-op.d.ts +50 -0
  207. package/dist/op/composites/pipeline-audit-op.d.ts.map +1 -0
  208. package/dist/op/composites/reconcile-op.d.ts +57 -0
  209. package/dist/op/composites/reconcile-op.d.ts.map +1 -0
  210. package/dist/op/composites/watch-op.d.ts +60 -0
  211. package/dist/op/composites/watch-op.d.ts.map +1 -0
  212. package/dist/op/composites/workflow-audit-op.d.ts +57 -0
  213. package/dist/op/composites/workflow-audit-op.d.ts.map +1 -0
  214. package/dist/op/converge-rule.d.ts +12 -10
  215. package/dist/op/converge-rule.d.ts.map +1 -1
  216. package/dist/op/cron.d.ts +51 -0
  217. package/dist/op/cron.d.ts.map +1 -0
  218. package/dist/op/discover.d.ts +9 -0
  219. package/dist/op/discover.d.ts.map +1 -1
  220. package/dist/op/duration.d.ts +11 -0
  221. package/dist/op/duration.d.ts.map +1 -0
  222. package/dist/op/emulator-lifecycle.d.ts.map +1 -1
  223. package/dist/op/gate-name.d.ts +40 -0
  224. package/dist/op/gate-name.d.ts.map +1 -0
  225. package/dist/op/gate-summary.d.ts +43 -0
  226. package/dist/op/gate-summary.d.ts.map +1 -0
  227. package/dist/op/gate.d.ts +90 -0
  228. package/dist/op/gate.d.ts.map +1 -0
  229. package/dist/op/generate-pipeline.d.ts +9 -0
  230. package/dist/op/generate-pipeline.d.ts.map +1 -1
  231. package/dist/op/index.d.ts +29 -9
  232. package/dist/op/index.d.ts.map +1 -1
  233. package/dist/op/local-executor.d.ts +114 -28
  234. package/dist/op/local-executor.d.ts.map +1 -1
  235. package/dist/op/local-output.d.ts +11 -1
  236. package/dist/op/local-output.d.ts.map +1 -1
  237. package/dist/op/op-ir.d.ts +185 -0
  238. package/dist/op/op-ir.d.ts.map +1 -0
  239. package/dist/op/op-verb-class.d.ts +10 -2
  240. package/dist/op/op-verb-class.d.ts.map +1 -1
  241. package/dist/op/operator.d.ts +93 -5
  242. package/dist/op/operator.d.ts.map +1 -1
  243. package/dist/op/receipt-store.d.ts +1 -1
  244. package/dist/op/receipt-store.d.ts.map +1 -1
  245. package/dist/op/resource.d.ts +15 -1
  246. package/dist/op/resource.d.ts.map +1 -1
  247. package/dist/op/runtime.d.ts +200 -0
  248. package/dist/op/runtime.d.ts.map +1 -0
  249. package/dist/op/runtimes/local.d.ts +29 -0
  250. package/dist/op/runtimes/local.d.ts.map +1 -0
  251. package/dist/op/step-output-ref.d.ts +15 -13
  252. package/dist/op/step-output-ref.d.ts.map +1 -1
  253. package/dist/op/types.d.ts +96 -29
  254. package/dist/op/types.d.ts.map +1 -1
  255. package/dist/ownership.d.ts +1 -1
  256. package/dist/provenance.d.ts +21 -0
  257. package/dist/provenance.d.ts.map +1 -1
  258. package/dist/serializer-walker.d.ts.map +1 -1
  259. package/dist/terraform/bridge.d.ts +26 -7
  260. package/dist/terraform/bridge.d.ts.map +1 -1
  261. package/dist/terraform/carve-provider.d.ts +28 -2
  262. package/dist/terraform/carve-provider.d.ts.map +1 -1
  263. package/dist/terraform/data-source-shape.d.ts +77 -0
  264. package/dist/terraform/data-source-shape.d.ts.map +1 -0
  265. package/dist/terraform/graph.d.ts.map +1 -1
  266. package/dist/terraform/providers/kubernetes.d.ts +5 -0
  267. package/dist/terraform/providers/kubernetes.d.ts.map +1 -1
  268. package/dist/terraform/tier-map.d.ts +16 -4
  269. package/dist/terraform/tier-map.d.ts.map +1 -1
  270. package/dist/terraform/types.d.ts +8 -0
  271. package/dist/terraform/types.d.ts.map +1 -1
  272. package/package.json +6 -1
  273. package/src/audit/catalog.test.ts +79 -3
  274. package/src/audit/catalog.ts +111 -13
  275. package/src/audit/core.test.ts +26 -0
  276. package/src/audit/core.ts +84 -18
  277. package/src/audit/discover.test.ts +39 -0
  278. package/src/audit/discover.ts +202 -18
  279. package/src/audit/lineage.ts +100 -0
  280. package/src/audit/prior-art.ts +153 -0
  281. package/src/audit/proof.test.ts +67 -0
  282. package/src/audit/proof.ts +70 -0
  283. package/src/audit/report-html.ts +9 -4
  284. package/src/audit/report-model.ts +16 -2
  285. package/src/audit/report.test.ts +33 -0
  286. package/src/audit/report.ts +5 -3
  287. package/src/audit/rules-doc.test.ts +34 -2
  288. package/src/audit/rules-doc.ts +82 -9
  289. package/src/audit/secrets.ts +15 -3
  290. package/src/audit/terraform-state.test.ts +144 -0
  291. package/src/audit/terraform-state.ts +131 -0
  292. package/src/build.ts +31 -1
  293. package/src/claimed-fields.test.ts +145 -0
  294. package/src/claimed-fields.ts +208 -0
  295. package/src/cli/build-params-cli.ts +2 -2
  296. package/src/cli/command-group.ts +14 -0
  297. package/src/cli/commands/__fixtures__/audit-terraform/infra/.terraform/providers/lock.txt +1 -0
  298. package/src/cli/commands/__fixtures__/audit-terraform/infra/main.tf +1 -0
  299. package/src/cli/commands/__fixtures__/audit-terraform/infra/modules/foo/main.tf +1 -0
  300. package/src/cli/commands/__fixtures__/audit-terraform/infra/variables.tf +3 -0
  301. package/src/cli/commands/__fixtures__/init-lexicon-output/package.json +13 -4
  302. package/src/cli/commands/__fixtures__/init-lexicon-output/src/codegen/docs-cli.ts +4 -0
  303. package/src/cli/commands/__fixtures__/init-lexicon-output/src/codegen/package.ts +1 -0
  304. package/src/cli/commands/__fixtures__/init-lexicon-output/src/package-cli.ts +21 -0
  305. package/src/cli/commands/__snapshots__/init-lexicon.test.ts.snap +1 -0
  306. package/src/cli/commands/audit.test.ts +191 -11
  307. package/src/cli/commands/audit.ts +78 -28
  308. package/src/cli/commands/build.test.ts +330 -9
  309. package/src/cli/commands/build.ts +170 -24
  310. package/src/cli/commands/carve-bridge.test.ts +237 -14
  311. package/src/cli/commands/carve-bridge.ts +21 -17
  312. package/src/cli/commands/carve-emit-k8s.test.ts +17 -10
  313. package/src/cli/commands/check-lexicon-docs.ts +1 -1
  314. package/src/cli/commands/check-lexicon-examples.test.ts +29 -1
  315. package/src/cli/commands/check-lexicon-examples.ts +42 -10
  316. package/src/cli/commands/init-lexicon/templates/codegen.ts +9 -0
  317. package/src/cli/commands/init-lexicon/templates/project.ts +44 -4
  318. package/src/cli/commands/init-lexicon.test.ts +97 -2
  319. package/src/cli/commands/init-lexicon.ts +4 -2
  320. package/src/cli/commands/init.test.ts +35 -1
  321. package/src/cli/commands/init.ts +14 -3
  322. package/src/cli/commands/lint.test.ts +194 -1
  323. package/src/cli/commands/lint.ts +205 -7
  324. package/src/cli/commands/onboard.test.ts +75 -0
  325. package/src/cli/commands/onboard.ts +54 -23
  326. package/src/cli/handlers/build.test.ts +81 -1
  327. package/src/cli/handlers/build.ts +15 -1
  328. package/src/cli/handlers/graph.test.ts +3 -3
  329. package/src/cli/handlers/lifecycle.test.ts +195 -12
  330. package/src/cli/handlers/lifecycle.ts +123 -6
  331. package/src/cli/handlers/misc.ts +1 -1
  332. package/src/cli/handlers/operator.test.ts +183 -11
  333. package/src/cli/handlers/operator.ts +341 -55
  334. package/src/cli/handlers/run.test.ts +568 -1295
  335. package/src/cli/handlers/run.ts +446 -912
  336. package/src/cli/main.test.ts +22 -16
  337. package/src/cli/main.ts +66 -33
  338. package/src/cli/mcp/op-tools.ts +147 -88
  339. package/src/cli/mcp/resource-handlers.ts +10 -37
  340. package/src/cli/mcp/resources/context.ts +11 -12
  341. package/src/cli/mcp/server.test.ts +25 -27
  342. package/src/cli/mcp/server.ts +2 -2
  343. package/src/cli/plugins.test.ts +68 -2
  344. package/src/cli/plugins.ts +39 -0
  345. package/src/cli/registry.ts +40 -9
  346. package/src/codegen/docs.ts +1 -1
  347. package/src/codegen/generate-post-synth-barrel.test.ts +1 -1
  348. package/src/codegen/generate-post-synth-barrel.ts +1 -1
  349. package/src/components/README.md +4 -3
  350. package/src/components/__fixtures__/neo4j-fanout.json +1 -1
  351. package/src/components/auto-release.test.ts +4 -20
  352. package/src/components/auto-release.ts +17 -42
  353. package/src/components/capability.ts +4 -4
  354. package/src/components/cli-support.test.ts +28 -12
  355. package/src/components/cli-support.ts +60 -47
  356. package/src/components/component-schema.test.ts +18 -2
  357. package/src/components/component.schema.json +18 -5
  358. package/src/components/component.test.ts +2 -2
  359. package/src/components/component.ts +27 -7
  360. package/src/components/config-defaults.test.ts +2 -2
  361. package/src/components/driver-output.ts +14 -2
  362. package/src/components/driver.test.ts +109 -20
  363. package/src/components/driver.ts +201 -84
  364. package/src/components/index.ts +1 -6
  365. package/src/components/manifest-persistence.test.ts +2 -20
  366. package/src/components/manifest-persistence.ts +4 -24
  367. package/src/components/pilots/neo4j-fanout.pilot.ts +3 -3
  368. package/src/components/pilots/pilots-lint-clean.test.ts +11 -10
  369. package/src/components/run-progress.ts +6 -6
  370. package/src/components/verbs/run-agent.test.ts +19 -0
  371. package/src/components/verbs/run-agent.ts +12 -24
  372. package/src/components/verbs/verify.ts +2 -2
  373. package/src/deep-observation.test.ts +75 -0
  374. package/src/deep-observation.ts +48 -0
  375. package/src/detectLexicon.ts +18 -1
  376. package/src/discovery/collect.test.ts +3 -3
  377. package/src/discovery/files.ts +4 -4
  378. package/src/discovery/fold-import.test.ts +1 -1
  379. package/src/discovery/fold-import.ts +105 -2
  380. package/src/discovery/param-deps.test.ts +112 -1
  381. package/src/discovery/param-deps.ts +193 -0
  382. package/src/discovery/sandbox/bundle.ts +15 -1
  383. package/src/discovery/sandbox/config-wire.test.ts +2 -2
  384. package/src/discovery/sandbox/config-wire.ts +2 -2
  385. package/src/fold/fold.test.ts +56 -0
  386. package/src/fold/foldable-helpers.ts +82 -2
  387. package/src/fold-provenance-exclusion.test.ts +186 -0
  388. package/src/fold-provenance.test.ts +495 -0
  389. package/src/fold-provenance.ts +441 -0
  390. package/src/graph-ops.test.ts +3 -3
  391. package/src/graph-ops.ts +1 -1
  392. package/src/held-elsewhere.test.ts +102 -0
  393. package/src/held-elsewhere.ts +114 -0
  394. package/src/index.ts +3 -0
  395. package/src/kubectl-context.ts +3 -3
  396. package/src/lexicon-config.test.ts +5 -5
  397. package/src/lexicon-config.ts +4 -4
  398. package/src/lexicon-plugin-helpers.ts +1 -1
  399. package/src/lexicon.ts +296 -16
  400. package/src/lifecycle/change-set.test.ts +49 -0
  401. package/src/lifecycle/change-set.ts +67 -0
  402. package/src/lifecycle/converge-ledger.ts +1 -1
  403. package/src/lifecycle/deep-diff.test.ts +267 -14
  404. package/src/lifecycle/deep-diff.ts +217 -14
  405. package/src/lifecycle/deep-observe.test.ts +3 -1
  406. package/src/lifecycle/deep-observe.ts +16 -6
  407. package/src/lifecycle/gate-ledger.test.ts +74 -2
  408. package/src/lifecycle/gate-ledger.ts +154 -36
  409. package/src/lifecycle/index.ts +1 -0
  410. package/src/lifecycle/release-ledger.ts +2 -2
  411. package/src/lifecycle/run-ledger.test.ts +268 -0
  412. package/src/lifecycle/run-ledger.ts +201 -0
  413. package/src/lifecycle/symptoms.ts +3 -3
  414. package/src/lint/config.test.ts +82 -1
  415. package/src/lint/config.ts +112 -3
  416. package/src/lint/pipeline-change-gate.test.ts +2 -2
  417. package/src/lint/policy.test.ts +1 -1
  418. package/src/lint/post-synth.test.ts +31 -0
  419. package/src/lint/post-synth.ts +54 -1
  420. package/src/lint/rules/__fixtures__/comp/comp004/pass/neo4j-cluster.component.ts +2 -2
  421. package/src/lint/rules/__fixtures__/comp/comp004/pass/orders-table.component.ts +1 -1
  422. package/src/lint/rules/comp/comp.test.ts +27 -1
  423. package/src/lint/rules/comp/comp004-gate-needs-durable-runtime.ts +76 -0
  424. package/src/lint/rules/comp/index.ts +3 -3
  425. package/src/lint/rules/op/index.ts +23 -0
  426. package/src/lint/rules/op/ops012-activity-contract.test.ts +161 -0
  427. package/src/lint/rules/op/ops012-activity-contract.ts +64 -0
  428. package/src/lint/rules/op/ops013-step-output-ref.test.ts +237 -0
  429. package/src/lint/rules/op/ops013-step-output-ref.ts +98 -0
  430. package/src/lint/rules/op/ops014-converge-rule-refusals.test.ts +263 -0
  431. package/src/lint/rules/op/ops014-converge-rule-refusals.ts +179 -0
  432. package/src/lint/rules/op/support.ts +11 -0
  433. package/src/lint/suppressions.test.ts +163 -0
  434. package/src/lint/suppressions.ts +204 -0
  435. package/src/live-endpoint.ts +1 -1
  436. package/src/op/activities/activity-contracts.ts +66 -0
  437. package/src/op/activities/apply.test.ts +567 -0
  438. package/src/op/activities/apply.ts +662 -0
  439. package/src/op/activities/build.ts +26 -0
  440. package/src/op/activities/converge.test.ts +330 -0
  441. package/src/op/activities/converge.ts +449 -0
  442. package/src/op/activities/env-teardown.test.ts +187 -0
  443. package/src/op/activities/env-teardown.ts +171 -0
  444. package/src/op/activities/guard-validate.test.ts +139 -0
  445. package/src/op/activities/guard-validate.ts +223 -0
  446. package/src/op/activities/http-check.test.ts +48 -0
  447. package/src/op/activities/http-check.ts +55 -0
  448. package/src/op/activities/index.ts +119 -0
  449. package/src/op/activities/lexicon-upgrade.test.ts +492 -0
  450. package/src/op/activities/lexicon-upgrade.ts +673 -0
  451. package/src/op/activities/lifecycle.ts +85 -0
  452. package/src/op/activities/pipeline-audit.test.ts +67 -0
  453. package/src/op/activities/pipeline-audit.ts +201 -0
  454. package/src/op/activities/policy.test.ts +19 -0
  455. package/src/op/activities/policy.ts +37 -0
  456. package/src/op/activities/reconcile.test.ts +161 -0
  457. package/src/op/activities/reconcile.ts +354 -0
  458. package/src/op/activities/shell.ts +26 -0
  459. package/src/op/activities/teardown.ts +22 -0
  460. package/src/op/activities/util.ts +6 -0
  461. package/src/op/activities/wait.ts +69 -0
  462. package/src/op/activities/workflow-audit.test.ts +90 -0
  463. package/src/op/activities/workflow-audit.ts +203 -0
  464. package/src/op/activity-contract-registry.test.ts +98 -0
  465. package/src/op/activity-contract-registry.ts +114 -0
  466. package/src/op/activity-contract.test.ts +1 -1
  467. package/src/op/activity-contract.ts +37 -33
  468. package/src/op/activity-failure.ts +34 -0
  469. package/src/op/activity-profiles.test.ts +56 -0
  470. package/src/op/activity-profiles.ts +116 -0
  471. package/src/op/activity-registry.test.ts +37 -28
  472. package/src/op/activity-registry.ts +27 -56
  473. package/src/op/activity-runtime.ts +3 -47
  474. package/src/op/builders-exports.test.ts +0 -49
  475. package/src/op/builders-typed.test.ts +164 -0
  476. package/src/op/builders.ts +125 -41
  477. package/src/op/change-signal.test.ts +117 -0
  478. package/src/op/change-signal.ts +169 -0
  479. package/src/op/composites/apply-op.ts +218 -0
  480. package/src/op/composites/composites.test.ts +379 -0
  481. package/src/op/composites/converge-op.test.ts +168 -0
  482. package/src/op/composites/converge-op.ts +214 -0
  483. package/src/op/composites/index.ts +26 -0
  484. package/src/op/composites/lexicon-upgrade-op.test.ts +68 -0
  485. package/src/op/composites/lexicon-upgrade-op.ts +134 -0
  486. package/src/op/composites/pipeline-audit-op.test.ts +25 -0
  487. package/src/op/composites/pipeline-audit-op.ts +78 -0
  488. package/src/op/composites/reconcile-op.test.ts +51 -0
  489. package/src/op/composites/reconcile-op.ts +110 -0
  490. package/src/op/composites/watch-op.ts +108 -0
  491. package/src/op/composites/workflow-audit-op.test.ts +25 -0
  492. package/src/op/composites/workflow-audit-op.ts +88 -0
  493. package/src/op/converge-rule.ts +12 -10
  494. package/src/op/cron.test.ts +128 -0
  495. package/src/op/cron.ts +149 -0
  496. package/src/op/discover-root.test.ts +105 -1
  497. package/src/op/discover.test.ts +6 -2
  498. package/src/op/discover.ts +84 -16
  499. package/src/op/duration.ts +21 -0
  500. package/src/op/effect-step.test.ts +29 -14
  501. package/src/op/emulator-lifecycle.ts +1 -2
  502. package/src/op/gate-name.test.ts +65 -0
  503. package/src/op/gate-name.ts +60 -0
  504. package/src/op/gate-summary.ts +84 -0
  505. package/src/op/gate.ts +155 -0
  506. package/src/op/generate-pipeline.test.ts +65 -3
  507. package/src/op/generate-pipeline.ts +19 -2
  508. package/src/op/index.ts +48 -8
  509. package/src/op/local-executor.test.ts +178 -34
  510. package/src/op/local-executor.ts +371 -102
  511. package/src/op/local-output.test.ts +95 -11
  512. package/src/op/local-output.ts +33 -4
  513. package/src/op/op-ir.test.ts +292 -0
  514. package/src/op/op-ir.ts +419 -0
  515. package/src/op/op-verb-class.test.ts +2 -2
  516. package/src/op/op-verb-class.ts +19 -5
  517. package/src/op/op.test.ts +28 -7
  518. package/src/op/operator.test.ts +543 -6
  519. package/src/op/operator.ts +203 -20
  520. package/src/op/receipt-store.ts +1 -1
  521. package/src/op/resource.ts +20 -2
  522. package/src/op/runtime.ts +203 -0
  523. package/src/op/runtimes/local.test.ts +181 -0
  524. package/src/op/runtimes/local.ts +212 -0
  525. package/src/op/step-output-ref.ts +15 -13
  526. package/src/op/types.ts +106 -26
  527. package/src/ownership.ts +1 -1
  528. package/src/provenance.test.ts +6 -0
  529. package/src/provenance.ts +21 -1
  530. package/src/serializer-walker.test.ts +42 -0
  531. package/src/serializer-walker.ts +21 -2
  532. package/src/terraform/aws-resources.test.ts +13 -4
  533. package/src/terraform/bridge.test.ts +22 -9
  534. package/src/terraform/bridge.ts +89 -28
  535. package/src/terraform/carve-provider.ts +38 -2
  536. package/src/terraform/data-source-shape.ts +95 -0
  537. package/src/terraform/graph.ts +35 -7
  538. package/src/terraform/providers/kubernetes.ts +48 -2
  539. package/src/terraform/tier-map.ts +21 -6
  540. package/src/terraform/types.ts +8 -0
  541. package/dist/cli/handlers/op-progress.d.ts +0 -90
  542. package/dist/cli/handlers/op-progress.d.ts.map +0 -1
  543. package/dist/cli/handlers/run-client.d.ts +0 -127
  544. package/dist/cli/handlers/run-client.d.ts.map +0 -1
  545. package/dist/cli/handlers/run-report.d.ts +0 -5
  546. package/dist/cli/handlers/run-report.d.ts.map +0 -1
  547. package/dist/components/temporal-codegen-loader.d.ts +0 -27
  548. package/dist/components/temporal-codegen-loader.d.ts.map +0 -1
  549. package/dist/lint/rules/comp/comp004-gate-needs-temporal.d.ts +0 -37
  550. package/dist/lint/rules/comp/comp004-gate-needs-temporal.d.ts.map +0 -1
  551. package/src/cli/handlers/op-progress.test.ts +0 -268
  552. package/src/cli/handlers/op-progress.ts +0 -248
  553. package/src/cli/handlers/run-client.test.ts +0 -82
  554. package/src/cli/handlers/run-client.ts +0 -219
  555. package/src/cli/handlers/run-report.test.ts +0 -62
  556. package/src/cli/handlers/run-report.ts +0 -122
  557. package/src/components/temporal-codegen-loader.ts +0 -41
  558. package/src/lint/rules/comp/comp004-gate-needs-temporal.ts +0 -66
@@ -1,26 +1,22 @@
1
- import { describe, test, expect, vi, beforeEach } from "vitest";
2
- import { createMockTemporalClient } from "@intentius/chant-test-utils";
1
+ import { describe, test, expect, vi, beforeEach, afterEach } from "vitest";
2
+ import { existsSync, mkdtempSync, readFileSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
3
5
  import type { ParsedArgs } from "../registry";
4
- import { EventEmitter } from "node:events";
5
6
 
6
7
  const discoverOpsMock = vi.fn();
7
8
  const loadChantConfigMock = vi.fn();
8
- const loadTemporalClientMock = vi.fn();
9
- const resolveProfileMock = vi.fn();
10
- const existsSyncMock = vi.fn();
11
9
  const writeFileSyncMock = vi.fn();
12
10
  const mkdirSyncMock = vi.fn();
13
- const spawnChildMock = vi.fn();
14
- const generateReportMock = vi.fn();
15
- const writeReportMock = vi.fn();
16
- const waitForTemporalSpy = vi.fn();
17
11
  const runComponentsMock = vi.fn();
18
- const resolveComponentTargetsMock = vi.fn();
19
- const findComponentGateMock = vi.fn();
20
- const loadComponentTemporalCodegenMock = vi.fn();
12
+ const listComponentsMock = vi.fn();
21
13
  const maybeRecordAutoReleaseMock = vi.fn();
22
14
  const maybePersistBuildManifestMock = vi.fn();
23
- const listComponentsMock = vi.fn();
15
+ const loadPluginsMock = vi.fn();
16
+ const recordGateApprovalMock = vi.fn();
17
+
18
+ const { memoryGateLedgerPort } = await vi.importActual<typeof import("../../op/gate")>("../../op/gate");
19
+ let gateLedger = memoryGateLedgerPort();
24
20
 
25
21
  vi.mock("../../op/discover", () => ({ discoverOps: () => discoverOpsMock() }));
26
22
  vi.mock("../../config", async () => {
@@ -29,87 +25,67 @@ vi.mock("../../config", async () => {
29
25
  });
30
26
  vi.mock("../../components/auto-release", () => ({
31
27
  maybeRecordAutoRelease: (...args: unknown[]) => maybeRecordAutoReleaseMock(...args),
32
- extractRunDigestFromPhaseOutputs: (phaseOutputs: Record<string, Record<string, unknown>> | undefined) => {
33
- for (const output of Object.values(phaseOutputs ?? {})) {
34
- if (output && typeof output === "object" && "digest" in output) return (output as { digest: string }).digest;
35
- }
36
- return undefined;
37
- },
38
28
  }));
39
29
  vi.mock("../../components/manifest-persistence", () => ({
40
30
  maybePersistBuildManifest: (...args: unknown[]) => maybePersistBuildManifestMock(...args),
41
- extractRunManifestFromPhaseOutputs: (phaseOutputs: Record<string, Record<string, unknown>> | undefined) => {
42
- for (const output of Object.values(phaseOutputs ?? {})) {
43
- if (output && typeof output === "object" && "manifest" in output) return (output as { manifest: unknown }).manifest;
44
- }
45
- return undefined;
46
- },
47
- }));
48
- vi.mock("./run-client", () => ({
49
- loadTemporalClient: () => loadTemporalClientMock(),
50
- connectionOptions: (profile: { address: string }) => ({ address: profile.address }),
51
- resolveProfile: (...args: unknown[]) => resolveProfileMock(...args),
52
- resolveWorkflowId: (name: string) => `chant-op-${name}`,
53
- // Test fixtures already use the short event-type form fetchNormalizedHistory
54
- // produces (see mock-temporal-client.ts) — a passthrough keeps them valid.
55
- fetchNormalizedHistory: (handle: { fetchHistory(): Promise<unknown> }) => handle.fetchHistory(),
56
31
  }));
57
32
  vi.mock("node:fs", async () => {
58
33
  const actual = await vi.importActual<typeof import("node:fs")>("node:fs");
59
34
  return {
60
35
  ...actual,
61
- existsSync: (p: string) => existsSyncMock(p),
62
36
  writeFileSync: (...args: unknown[]) => writeFileSyncMock(...args),
63
37
  mkdirSync: (...args: unknown[]) => mkdirSyncMock(...args),
64
38
  };
65
39
  });
66
- vi.mock("node:child_process", async () => {
67
- const actual = await vi.importActual<typeof import("node:child_process")>("node:child_process");
68
- return { ...actual, spawn: (...args: unknown[]) => spawnChildMock(...args) };
69
- });
70
- vi.mock("./run-report", () => ({
71
- generateReport: (...args: unknown[]) => generateReportMock(...args),
72
- writeReport: (...args: unknown[]) => writeReportMock(...args),
73
- }));
74
40
  vi.mock("../../components/cli-support", () => ({
75
41
  runComponents: (...args: unknown[]) => runComponentsMock(...args),
76
- resolveComponentTargets: (...args: unknown[]) => resolveComponentTargetsMock(...args),
77
- findComponentGate: (...args: unknown[]) => findComponentGateMock(...args),
78
42
  listComponents: (...args: unknown[]) => listComponentsMock(...args),
79
43
  }));
80
- vi.mock("../../components/temporal-codegen-loader", () => ({
81
- loadComponentTemporalCodegen: () => loadComponentTemporalCodegenMock(),
44
+ vi.mock("../plugins", () => ({
45
+ loadPlugins: (...args: unknown[]) => loadPluginsMock(...args),
82
46
  }));
47
+ vi.mock("./operator", () => ({
48
+ recordGateApproval: (...args: unknown[]) => recordGateApprovalMock(...args),
49
+ }));
50
+ // The gate ledger a local run consults (#2119) is the `chant/lifecycle` orphan
51
+ // branch by default. Point it at memory here: these tests exercise the CLI's
52
+ // gate handling, and a unit test must never write a fact to the real repo.
53
+ vi.mock("../../op/gate", async () => {
54
+ const actual = await vi.importActual<typeof import("../../op/gate")>("../../op/gate");
55
+ return { ...actual, gitGateLedgerPort: () => gateLedger };
56
+ });
57
+ // The local runtime appends each run's record to that same branch (#2118).
58
+ // Real behavior in a project, and exactly what this suite must not do in
59
+ // chant's own checkout, since the append is against `process.cwd()`. Only the
60
+ // write and the read-back are stubbed — `buildRunRecord` stays real, so
61
+ // `--json` is still asserted against the record the executor actually built.
62
+ vi.mock("../../lifecycle/run-ledger", async (importOriginal) => {
63
+ const actual = await importOriginal<typeof import("../../lifecycle/run-ledger")>();
64
+ return {
65
+ ...actual,
66
+ appendRunRecord: async (input: Parameters<typeof actual.appendRunRecord>[0]) => ({
67
+ commit: "0".repeat(40),
68
+ record: { version: 1 as const, ...input, id: input.id ?? "test-run-id" },
69
+ }),
70
+ readRunLedger: async () => ({ records: [], malformed: 0 }),
71
+ };
72
+ });
83
73
 
84
- // Speed up runOp polling — POLL_INTERVAL_MS is 3000 in production. We use
85
- // fake timers in the runOp suite below; vi.advanceTimersByTime drives the loop.
86
-
87
- const { runOpList, runOpStatus, runOpLog, runOpSignal, runOpCancel, runOp, runOpComponents } = await import("./run");
74
+ const { runOpList, runOpStatus, runOpLog, runOpSignalRenamed, runOpApprove, runOpCancel, runOp, runOpComponents } =
75
+ await import("./run");
88
76
 
89
77
  function makeArgs(overrides: Partial<ParsedArgs> = {}): ParsedArgs {
90
78
  return {
91
79
  command: "run", path: ".",
92
80
  format: "", fix: false, watch: false, verbose: false, help: false, live: false,
93
- // These suites exercise the Temporal path; local mode is the CLI default,
94
- // so opt in explicitly here. Local-mode behavior is covered separately below.
95
- temporal: true,
96
81
  ...overrides,
97
82
  };
98
83
  }
99
84
 
100
- function makeOp(name: string, depends: string[] = []): [string, { config: { name: string; phases: unknown[]; taskQueue?: string; depends?: string[]; overview: string } }] {
85
+ function makeOp(name: string, depends: string[] = []): [string, { config: { name: string; phases: unknown[]; depends?: string[]; overview: string } }] {
101
86
  return [name, { config: { name, phases: [], depends, overview: `${name} overview` } }];
102
87
  }
103
88
 
104
- function setupTemporalClient(mock: ReturnType<typeof createMockTemporalClient>) {
105
- loadTemporalClientMock.mockResolvedValue({
106
- Connection: { connect: vi.fn(async () => ({})) },
107
- Client: vi.fn(function () { return mock.client; }) as unknown as new () => unknown,
108
- });
109
- loadChantConfigMock.mockResolvedValue({ config: {} });
110
- resolveProfileMock.mockReturnValue({ address: "localhost:7233", namespace: "default", taskQueue: "q" });
111
- }
112
-
113
89
  function makeStdoutSpy() {
114
90
  const buf: string[] = [];
115
91
  vi.spyOn(console, "log").mockImplementation((s: string) => { buf.push(s); });
@@ -122,495 +98,35 @@ function makeStderrSpy() {
122
98
  return buf;
123
99
  }
124
100
 
125
- describe("runOpList", () => {
126
- beforeEach(() => {
127
- discoverOpsMock.mockReset();
128
- loadTemporalClientMock.mockReset();
129
- loadChantConfigMock.mockReset();
130
- resolveProfileMock.mockReset();
131
- });
132
-
133
- test("warns when no Ops discovered, returns 0", async () => {
134
- discoverOpsMock.mockResolvedValue({ ops: new Map(), errors: [] });
135
- const stderr = makeStderrSpy();
136
- const exit = await runOpList({ args: makeArgs(), plugins: [], serializers: [] });
137
- expect(exit).toBe(0);
138
- expect(stderr.join("\n")).toContain("No Op definitions found");
139
- });
140
-
141
- test("prints table with one row per Op when Temporal connection fails", async () => {
142
- discoverOpsMock.mockResolvedValue({
143
- ops: new Map([makeOp("alb-deploy"), makeOp("infra")]),
144
- errors: [],
145
- });
146
- // No Temporal — make loadTemporalClient throw so degraded path is exercised
147
- loadTemporalClientMock.mockRejectedValue(new Error("not installed"));
148
- const stdout = makeStdoutSpy();
149
- const exit = await runOpList({ args: makeArgs(), plugins: [], serializers: [] });
150
- expect(exit).toBe(0);
151
- const out = stdout.join("\n");
152
- expect(out).toContain("NAME");
153
- expect(out).toContain("alb-deploy");
154
- expect(out).toContain("infra");
155
- });
156
-
157
- test("annotates Ops with Temporal status when client is available", async () => {
158
- discoverOpsMock.mockResolvedValue({ ops: new Map([makeOp("alb-deploy")]), errors: [] });
159
- setupTemporalClient(createMockTemporalClient({
160
- describeByWorkflowId: {
161
- "chant-op-alb-deploy": {
162
- workflowId: "chant-op-alb-deploy", runId: "r1",
163
- status: { name: "RUNNING" }, startTime: new Date(),
164
- taskQueue: "alb-deploy", type: { name: "albDeployWorkflow" },
165
- },
166
- },
167
- }));
168
- const stdout = makeStdoutSpy();
169
- const exit = await runOpList({ args: makeArgs(), plugins: [], serializers: [] });
170
- expect(exit).toBe(0);
171
- expect(stdout.join("\n")).toContain("RUNNING");
172
- });
173
- });
174
-
175
- // ── chant run list --components (#599) ──────────────────────────────────────
176
-
177
- describe("runOpList --components", () => {
178
- beforeEach(() => {
179
- listComponentsMock.mockReset();
180
- loadTemporalClientMock.mockReset();
181
- loadChantConfigMock.mockReset();
182
- resolveProfileMock.mockReset();
183
- });
184
-
185
- test("without --temporal → exit 1, actionable message, no discovery", async () => {
186
- const stderr = makeStderrSpy();
187
- const exit = await runOpList({ args: makeArgs({ components: true, temporal: false }), plugins: [], serializers: [] });
188
- expect(exit).toBe(1);
189
- expect(stderr.join("\n")).toContain("not available in local mode");
190
- expect(listComponentsMock).not.toHaveBeenCalled();
191
- });
192
-
193
- test("warns when no components discovered, returns 0", async () => {
194
- listComponentsMock.mockResolvedValue({ success: true, components: [], errors: [] });
195
- const stderr = makeStderrSpy();
196
- const exit = await runOpList({ args: makeArgs({ components: true }), plugins: [], serializers: [] });
197
- expect(exit).toBe(0);
198
- expect(stderr.join("\n")).toContain("No component definitions found");
199
- });
200
-
201
- test("discovery error → exit 1 with the error message", async () => {
202
- listComponentsMock.mockResolvedValue({ success: false, components: [], errors: ["bad component file"] });
203
- const stderr = makeStderrSpy();
204
- const exit = await runOpList({ args: makeArgs({ components: true }), plugins: [], serializers: [] });
205
- expect(exit).toBe(1);
206
- expect(stderr.join("\n")).toContain("bad component file");
207
- });
208
-
209
- test("prints table with one row per component when Temporal connection fails", async () => {
210
- listComponentsMock.mockResolvedValue({
211
- success: true,
212
- components: [
213
- { name: "search-service", archetype: "service", dependsOn: ["shared-alb"], hasBuild: true, phases: ["Build", "Apply"], filePath: "src/search.component.ts" },
214
- ],
215
- errors: [],
216
- });
217
- loadTemporalClientMock.mockRejectedValue(new Error("not installed"));
218
- const stdout = makeStdoutSpy();
219
- const exit = await runOpList({ args: makeArgs({ components: true }), plugins: [], serializers: [] });
220
- expect(exit).toBe(0);
221
- const out = stdout.join("\n");
222
- expect(out).toContain("NAME");
223
- expect(out).toContain("search-service");
224
- expect(out).toContain("shared-alb");
225
- });
226
-
227
- test("annotates components with Temporal status when client is available", async () => {
228
- listComponentsMock.mockResolvedValue({
229
- success: true,
230
- components: [
231
- { name: "gated-svc", archetype: "service", dependsOn: [], hasBuild: true, phases: ["Apply"], filePath: "src/gated.component.ts" },
232
- ],
233
- errors: [],
234
- });
235
- setupTemporalClient(createMockTemporalClient({
236
- describeByWorkflowId: {
237
- "chant-component-gated-svc": {
238
- workflowId: "chant-component-gated-svc", runId: "r1",
239
- status: { name: "RUNNING" }, startTime: new Date(),
240
- taskQueue: "gated-svc", type: { name: "gatedSvcComponentWorkflow" },
241
- },
242
- },
243
- }));
244
- const stdout = makeStdoutSpy();
245
- const exit = await runOpList({ args: makeArgs({ components: true }), plugins: [], serializers: [] });
246
- expect(exit).toBe(0);
247
- expect(stdout.join("\n")).toContain("RUNNING");
248
- });
249
- });
250
-
251
- describe("runOpStatus", () => {
252
- beforeEach(() => {
253
- discoverOpsMock.mockReset();
254
- loadTemporalClientMock.mockReset();
255
- loadChantConfigMock.mockReset();
256
- resolveProfileMock.mockReset();
257
- });
258
-
259
- test("missing op name → exit 1", async () => {
260
- const stderr = makeStderrSpy();
261
- const exit = await runOpStatus({ args: makeArgs({ extraPositional: undefined }), plugins: [], serializers: [] });
262
- expect(exit).toBe(1);
263
- expect(stderr.join("\n")).toContain("Op name is required");
264
- });
265
-
266
- test("connection error → exit 1 with message", async () => {
267
- loadTemporalClientMock.mockRejectedValue(new Error("UNAVAILABLE"));
268
- loadChantConfigMock.mockResolvedValue({ config: {} });
269
- resolveProfileMock.mockReturnValue({ address: "localhost:7233", namespace: "default", taskQueue: "q" });
270
- const stderr = makeStderrSpy();
271
- const exit = await runOpStatus({ args: makeArgs({ extraPositional: "alb-deploy" }), plugins: [], serializers: [] });
272
- expect(exit).toBe(1);
273
- expect(stderr.join("\n")).toContain("UNAVAILABLE");
274
- });
275
-
276
- test("happy path: prints workflow id, run id, status, activity counts", async () => {
277
- setupTemporalClient(createMockTemporalClient({
278
- describeByWorkflowId: {
279
- "chant-op-alb-deploy": {
280
- workflowId: "chant-op-alb-deploy", runId: "r1",
281
- status: { name: "COMPLETED" },
282
- startTime: new Date("2026-05-01T00:00:00Z"),
283
- closeTime: new Date("2026-05-01T01:00:00Z"),
284
- taskQueue: "alb-deploy", type: { name: "albDeployWorkflow" },
285
- },
286
- },
287
- historyByWorkflowId: {
288
- "chant-op-alb-deploy": [
289
- { eventType: "ActivityTaskScheduled" },
290
- { eventType: "ActivityTaskScheduled" },
291
- { eventType: "ActivityTaskCompleted" },
292
- ],
293
- },
294
- }));
295
- const stdout = makeStdoutSpy();
296
- const exit = await runOpStatus({ args: makeArgs({ extraPositional: "alb-deploy" }), plugins: [], serializers: [] });
297
- expect(exit).toBe(0);
298
- const out = stdout.join("\n");
299
- expect(out).toContain("chant-op-alb-deploy");
300
- expect(out).toContain("COMPLETED");
301
- expect(out).toContain("1/2 completed");
302
- });
303
-
304
- test("a pending gate prints a Gate line via the gateState query (#1676)", async () => {
305
- setupTemporalClient(createMockTemporalClient({
306
- describeByWorkflowId: {
307
- "chant-op-alb-deploy": {
308
- workflowId: "chant-op-alb-deploy", runId: "r1",
309
- status: { name: "RUNNING" },
310
- startTime: new Date("2026-05-01T00:00:00Z"),
311
- taskQueue: "alb-deploy", type: { name: "albDeployWorkflow" },
312
- },
313
- },
314
- historyByWorkflowId: { "chant-op-alb-deploy": [] },
315
- queryResultByWorkflowId: {
316
- "chant-op-alb-deploy:gateState": {
317
- signalName: "gate-dns-delegation",
318
- description: "Approve DNS delegation",
319
- since: "2026-05-01T00:05:00.000Z",
320
- },
321
- },
322
- }));
323
- const stdout = makeStdoutSpy();
324
- const exit = await runOpStatus({ args: makeArgs({ extraPositional: "alb-deploy" }), plugins: [], serializers: [] });
325
- expect(exit).toBe(0);
326
- const out = stdout.join("\n");
327
- expect(out).toContain("Gate");
328
- expect(out).toContain("gate-dns-delegation");
329
- expect(out).toContain("Approve DNS delegation");
330
- expect(out).toContain("2026-05-01T00:05:00.000Z");
331
- });
332
-
333
- test("an Op with no pending gate (query unregistered or answers null) prints no Gate line", async () => {
334
- setupTemporalClient(createMockTemporalClient({
335
- describeByWorkflowId: {
336
- "chant-op-alb-deploy": {
337
- workflowId: "chant-op-alb-deploy", runId: "r1",
338
- status: { name: "COMPLETED" },
339
- startTime: new Date("2026-05-01T00:00:00Z"),
340
- closeTime: new Date("2026-05-01T01:00:00Z"),
341
- taskQueue: "alb-deploy", type: { name: "albDeployWorkflow" },
342
- },
343
- },
344
- historyByWorkflowId: { "chant-op-alb-deploy": [] },
345
- }));
346
- const stdout = makeStdoutSpy();
347
- const exit = await runOpStatus({ args: makeArgs({ extraPositional: "alb-deploy" }), plugins: [], serializers: [] });
348
- expect(exit).toBe(0);
349
- expect(stdout.join("\n")).not.toContain("Gate");
350
- });
351
- });
352
-
353
- // ── chant run status <name> --components (#599) ─────────────────────────────
354
-
355
- describe("runOpStatus --components", () => {
356
- beforeEach(() => {
357
- loadTemporalClientMock.mockReset();
358
- loadChantConfigMock.mockReset();
359
- resolveProfileMock.mockReset();
360
- });
361
-
362
- test("missing component name → exit 1 with component-flavored message", async () => {
363
- const stderr = makeStderrSpy();
364
- const exit = await runOpStatus({ args: makeArgs({ extraPositional: undefined, components: true }), plugins: [], serializers: [] });
365
- expect(exit).toBe(1);
366
- expect(stderr.join("\n")).toContain("Component name is required");
367
- });
368
-
369
- test("without --temporal → exit 1, actionable message", async () => {
370
- const stderr = makeStderrSpy();
371
- const exit = await runOpStatus({ args: makeArgs({ extraPositional: "gated-svc", components: true, temporal: false }), plugins: [], serializers: [] });
372
- expect(exit).toBe(1);
373
- expect(stderr.join("\n")).toContain("not available in local mode");
374
- });
375
-
376
- test("connection error → exit 1 with message", async () => {
377
- loadTemporalClientMock.mockRejectedValue(new Error("UNAVAILABLE"));
378
- loadChantConfigMock.mockResolvedValue({ config: {} });
379
- resolveProfileMock.mockReturnValue({ address: "localhost:7233", namespace: "default", taskQueue: "q" });
380
- const stderr = makeStderrSpy();
381
- const exit = await runOpStatus({ args: makeArgs({ extraPositional: "gated-svc", components: true }), plugins: [], serializers: [] });
382
- expect(exit).toBe(1);
383
- expect(stderr.join("\n")).toContain("UNAVAILABLE");
384
- });
385
-
386
- test("happy path: queries the component workflow id, prints status + activity counts", async () => {
387
- setupTemporalClient(createMockTemporalClient({
388
- describeByWorkflowId: {
389
- "chant-component-gated-svc": {
390
- workflowId: "chant-component-gated-svc", runId: "r1",
391
- status: { name: "RUNNING" },
392
- startTime: new Date("2026-05-01T00:00:00Z"),
393
- taskQueue: "gated-svc", type: { name: "gatedSvcComponentWorkflow" },
394
- },
395
- },
396
- historyByWorkflowId: {
397
- "chant-component-gated-svc": [
398
- { eventType: "ActivityTaskScheduled" },
399
- { eventType: "ActivityTaskScheduled" },
400
- { eventType: "ActivityTaskCompleted" },
401
- ],
402
- },
403
- }));
404
- const stdout = makeStdoutSpy();
405
- const exit = await runOpStatus({ args: makeArgs({ extraPositional: "gated-svc", components: true }), plugins: [], serializers: [] });
406
- expect(exit).toBe(0);
407
- const out = stdout.join("\n");
408
- expect(out).toContain("Component: gated-svc");
409
- expect(out).toContain("chant-component-gated-svc");
410
- expect(out).toContain("RUNNING");
411
- expect(out).toContain("1/2 completed");
412
- });
413
- });
414
-
415
- describe("runOpLog", () => {
416
- beforeEach(() => {
417
- loadTemporalClientMock.mockReset();
418
- loadChantConfigMock.mockReset();
419
- resolveProfileMock.mockReset();
420
- });
421
-
422
- test("missing op name → exit 1", async () => {
423
- const stderr = makeStderrSpy();
424
- const exit = await runOpLog({ args: makeArgs({ extraPositional: undefined }), plugins: [], serializers: [] });
425
- expect(exit).toBe(1);
426
- expect(stderr.join("\n")).toContain("Op name is required");
427
- });
428
-
429
- test("prints one row per matching workflow execution", async () => {
430
- setupTemporalClient(createMockTemporalClient({
431
- list: [
432
- { workflowId: "chant-op-alb-deploy", runId: "r1", type: { name: "albDeployWorkflow" }, status: { name: "COMPLETED" }, startTime: new Date("2026-05-01T00:00:00Z"), closeTime: new Date("2026-05-01T01:00:00Z") },
433
- { workflowId: "chant-op-alb-deploy", runId: "r2", type: { name: "albDeployWorkflow" }, status: { name: "RUNNING" }, startTime: new Date("2026-05-02T00:00:00Z") },
434
- ],
435
- }));
436
- const stdout = makeStdoutSpy();
437
- const exit = await runOpLog({ args: makeArgs({ extraPositional: "alb-deploy" }), plugins: [], serializers: [] });
438
- expect(exit).toBe(0);
439
- const out = stdout.join("\n");
440
- expect(out).toContain("RUN-ID");
441
- expect(out).toContain("r1");
442
- expect(out).toContain("r2");
443
- expect(out).toContain("COMPLETED");
444
- expect(out).toContain("RUNNING");
445
- });
446
- });
447
-
448
- // ── chant run log <name> --components (#599) ────────────────────────────────
449
-
450
- describe("runOpLog --components", () => {
451
- beforeEach(() => {
452
- loadTemporalClientMock.mockReset();
453
- loadChantConfigMock.mockReset();
454
- resolveProfileMock.mockReset();
455
- loadComponentTemporalCodegenMock.mockReset();
456
- });
457
-
458
- test("missing component name → exit 1 with component-flavored message", async () => {
459
- const stderr = makeStderrSpy();
460
- const exit = await runOpLog({ args: makeArgs({ extraPositional: undefined, components: true }), plugins: [], serializers: [] });
461
- expect(exit).toBe(1);
462
- expect(stderr.join("\n")).toContain("Component name is required");
463
- });
464
-
465
- test("without --temporal → exit 1, actionable message", async () => {
466
- const stderr = makeStderrSpy();
467
- const exit = await runOpLog({ args: makeArgs({ extraPositional: "gated-svc", components: true, temporal: false }), plugins: [], serializers: [] });
468
- expect(exit).toBe(1);
469
- expect(stderr.join("\n")).toContain("not available in local mode");
470
- });
471
-
472
- test("codegen unavailable → exit 1 with the loader's error message", async () => {
473
- setupTemporalClient(createMockTemporalClient());
474
- loadComponentTemporalCodegenMock.mockRejectedValue(new Error("no durable component codegen available"));
475
- const stderr = makeStderrSpy();
476
- const exit = await runOpLog({ args: makeArgs({ extraPositional: "gated-svc", components: true }), plugins: [], serializers: [] });
477
- expect(exit).toBe(1);
478
- expect(stderr.join("\n")).toContain("no durable component codegen available");
479
- });
480
-
481
- test("prints one row per matching component workflow execution, keyed by the component's workflow type", async () => {
482
- setupTemporalClient(createMockTemporalClient({
483
- list: [
484
- { workflowId: "chant-component-gated-svc", runId: "r1", type: { name: "gatedSvcComponentWorkflow" }, status: { name: "COMPLETED" }, startTime: new Date("2026-05-01T00:00:00Z"), closeTime: new Date("2026-05-01T01:00:00Z") },
485
- { workflowId: "chant-component-gated-svc", runId: "r2", type: { name: "gatedSvcComponentWorkflow" }, status: { name: "RUNNING" }, startTime: new Date("2026-05-02T00:00:00Z") },
486
- ],
487
- }));
488
- loadComponentTemporalCodegenMock.mockResolvedValue({
489
- serializeComponent: vi.fn(),
490
- componentWorkflowFnName: (name: string) => `${name}ComponentWorkflow`,
491
- });
492
- const stdout = makeStdoutSpy();
493
- const exit = await runOpLog({ args: makeArgs({ extraPositional: "gated-svc", components: true }), plugins: [], serializers: [] });
494
- expect(exit).toBe(0);
495
- const out = stdout.join("\n");
496
- expect(out).toContain("RUN-ID");
497
- expect(out).toContain("r1");
498
- expect(out).toContain("r2");
499
- expect(out).toContain("COMPLETED");
500
- expect(out).toContain("RUNNING");
501
- });
502
- });
503
-
504
- // ── chant run <name> --components --temporal: config defaults reach codegen ──
505
- // The interpret/local path fills chant.config's sbom/signing/vulnPolicy
506
- // defaults via runComponents; the durable path inlines the composition into
507
- // generated workflow code, so it must apply the same pass before serializing
508
- // — otherwise the Temporal path silently drops project defaults the local
509
- // path honors.
510
- describe("runComponentTemporal — applies chant.config defaults before codegen", () => {
511
- beforeEach(() => {
512
- loadTemporalClientMock.mockReset();
513
- loadChantConfigMock.mockReset();
514
- resolveProfileMock.mockReset();
515
- resolveComponentTargetsMock.mockReset();
516
- loadComponentTemporalCodegenMock.mockReset();
517
- writeFileSyncMock.mockReset();
518
- mkdirSyncMock.mockReset();
519
- });
520
-
521
- test("serializeComponent receives a component with config's sbom.format filled in", async () => {
522
- resolveComponentTargetsMock.mockResolvedValue({
523
- success: true,
524
- targets: [{
525
- name: "search-svc",
526
- dependsOn: [],
527
- deploy: [{ phase: "Build", steps: [{ kind: "generate-sbom", artifactType: "image", path: "img" }] }],
528
- }],
529
- });
530
- // config sets a project-wide SBOM format the step itself omits.
531
- loadChantConfigMock.mockResolvedValue({ config: { sbom: { format: "cyclonedx" } } });
532
- resolveProfileMock.mockReturnValue({ autoStart: false, address: "localhost:7233", namespace: "default", taskQueue: "q" });
533
-
534
- const serializeSpy = vi.fn().mockReturnValue({});
535
- loadComponentTemporalCodegenMock.mockResolvedValue({
536
- serializeComponent: serializeSpy,
537
- componentWorkflowFnName: (n: string) => `${n}ComponentWorkflow`,
538
- });
539
- // Fail the client connect right after codegen so the handler returns
540
- // without driving the whole workflow-start machinery — the spy has already
541
- // captured the (resolved) component by then.
542
- loadTemporalClientMock.mockRejectedValue(new Error("client-unavailable"));
543
- makeStderrSpy();
544
-
545
- await runOpComponents({ args: makeArgs({ path: "search-svc", components: true, temporal: true }), plugins: [], serializers: [] });
546
-
547
- expect(serializeSpy).toHaveBeenCalledTimes(1);
548
- const passedComponent = serializeSpy.mock.calls[0]![0] as { deploy: Array<{ steps: Array<{ format?: string }> }> };
549
- expect(passedComponent.deploy[0]!.steps[0]!.format).toBe("cyclonedx");
550
- });
551
- });
552
-
553
- describe("runOpSignal", () => {
554
- beforeEach(() => {
555
- loadTemporalClientMock.mockReset();
556
- loadChantConfigMock.mockReset();
557
- resolveProfileMock.mockReset();
558
- });
559
-
560
- test("missing op or signal name → exit 1", async () => {
561
- const stderr = makeStderrSpy();
562
- const exit = await runOpSignal({ args: makeArgs({ extraPositional: "op-only" }), plugins: [], serializers: [] });
563
- expect(exit).toBe(1);
564
- expect(stderr.join("\n")).toContain("Usage:");
565
- });
566
-
567
- test("happy path: signal is sent and success message logged", async () => {
568
- const mockClient = createMockTemporalClient();
569
- setupTemporalClient(mockClient);
101
+ describe("runOpSignalRenamed", () => {
102
+ test("`run signal` points at `run approve` and never sends anything", async () => {
570
103
  const stderr = makeStderrSpy();
571
- const exit = await runOpSignal({
104
+ const exit = await runOpSignalRenamed({
572
105
  args: makeArgs({ extraPositional: "alb-deploy", extraPositional2: "gate-dns" }),
573
106
  plugins: [], serializers: [],
574
107
  });
575
- expect(exit).toBe(0);
576
- expect(mockClient.calls.signalCalls).toEqual([
577
- { workflowId: "chant-op-alb-deploy", signalName: "gate-dns" },
578
- ]);
579
- expect(stderr.join("\n")).toContain("Signal");
580
- expect(stderr.join("\n")).toContain("gate-dns");
581
- });
582
-
583
- test("--components: signals the component workflow id, not the Op one (#589)", async () => {
584
- const mockClient = createMockTemporalClient();
585
- setupTemporalClient(mockClient);
586
- const stderr = makeStderrSpy();
587
- const exit = await runOpSignal({
588
- args: makeArgs({ extraPositional: "gated-svc", extraPositional2: "release-approval", components: true }),
589
- plugins: [], serializers: [],
590
- });
591
- expect(exit).toBe(0);
592
- expect(mockClient.calls.signalCalls).toEqual([
593
- { workflowId: "chant-component-gated-svc", signalName: "release-approval" },
594
- ]);
595
- expect(stderr.join("\n")).toContain("component");
108
+ expect(exit).toBe(1);
109
+ const out = stderr.join("\n");
110
+ expect(out).toContain("`chant run signal` is now `chant run approve`");
111
+ expect(out).toContain("chant run approve alb-deploy gate-dns");
596
112
  });
597
113
  });
598
114
 
599
115
  describe("runOpCancel", () => {
600
116
  beforeEach(() => {
601
- loadTemporalClientMock.mockReset();
602
- loadChantConfigMock.mockReset();
603
- resolveProfileMock.mockReset();
117
+ discoverOpsMock.mockReset();
118
+ loadChantConfigMock.mockReset().mockResolvedValue({ config: {} });
119
+ loadPluginsMock.mockReset().mockResolvedValue([]);
604
120
  });
605
121
 
606
- test("missing op name → exit 1", async () => {
122
+ test("missing op name -> exit 1", async () => {
607
123
  const stderr = makeStderrSpy();
608
124
  const exit = await runOpCancel({ args: makeArgs({ extraPositional: undefined }), plugins: [], serializers: [] });
609
125
  expect(exit).toBe(1);
610
126
  expect(stderr.join("\n")).toContain("Op name is required");
611
127
  });
612
128
 
613
- test("requires --force → exit 1 without it", async () => {
129
+ test("requires --force -> exit 1 without it", async () => {
614
130
  const stderr = makeStderrSpy();
615
131
  const exit = await runOpCancel({
616
132
  args: makeArgs({ extraPositional: "alb-deploy", force: false }),
@@ -619,52 +135,15 @@ describe("runOpCancel", () => {
619
135
  expect(exit).toBe(1);
620
136
  expect(stderr.join("\n")).toContain("--force");
621
137
  });
622
-
623
- test("with --force: cancel is sent and success logged", async () => {
624
- const mockClient = createMockTemporalClient();
625
- setupTemporalClient(mockClient);
626
- const stderr = makeStderrSpy();
627
- const exit = await runOpCancel({
628
- args: makeArgs({ extraPositional: "alb-deploy", force: true }),
629
- plugins: [], serializers: [],
630
- });
631
- expect(exit).toBe(0);
632
- expect(mockClient.calls.cancelCalls).toEqual([{ workflowId: "chant-op-alb-deploy" }]);
633
- expect(stderr.join("\n")).toContain("Cancellation requested");
634
- });
635
-
636
- test("--components: cancels the component workflow id, not the Op one (#589)", async () => {
637
- const mockClient = createMockTemporalClient();
638
- setupTemporalClient(mockClient);
639
- const stderr = makeStderrSpy();
640
- const exit = await runOpCancel({
641
- args: makeArgs({ extraPositional: "gated-svc", force: true, components: true }),
642
- plugins: [], serializers: [],
643
- });
644
- expect(exit).toBe(0);
645
- expect(mockClient.calls.cancelCalls).toEqual([{ workflowId: "chant-component-gated-svc" }]);
646
- expect(stderr.join("\n")).toContain("component");
647
- });
648
138
  });
649
139
 
650
- // ── runOp (the main `chant run <name>` command) ─────────────────────────────
651
-
652
- function makeFakeChildProcess(): { proc: EventEmitter & { kill: () => void } } {
653
- const proc = Object.assign(new EventEmitter(), { kill: vi.fn() });
654
- return { proc };
655
- }
140
+ // ── runOp (the main `chant run <name>` command) ───────────────────────
656
141
 
657
142
  describe("runOp", () => {
658
143
  beforeEach(() => {
659
144
  discoverOpsMock.mockReset();
660
- loadTemporalClientMock.mockReset();
661
- loadChantConfigMock.mockReset();
662
- resolveProfileMock.mockReset();
663
- existsSyncMock.mockReset();
664
- spawnChildMock.mockReset();
665
- generateReportMock.mockReset();
666
- writeReportMock.mockReset();
667
- waitForTemporalSpy.mockReset();
145
+ loadChantConfigMock.mockReset().mockResolvedValue({ config: {} });
146
+ loadPluginsMock.mockReset().mockResolvedValue([]);
668
147
  });
669
148
 
670
149
  test("path defaults to '.' → exit 1 with hint", async () => {
@@ -693,297 +172,226 @@ describe("runOp", () => {
693
172
  expect(stderr.join("\n")).toContain("No *.op.ts files found");
694
173
  });
695
174
 
696
- test("profile resolution failure → exit 1", async () => {
697
- discoverOpsMock.mockResolvedValue({ ops: new Map([makeOp("alb-deploy")]), errors: [] });
698
- loadChantConfigMock.mockResolvedValue({ config: {} });
699
- resolveProfileMock.mockImplementation(() => { throw new Error("Profile not found: prod"); });
700
- const stderr = makeStderrSpy();
701
- const exit = await runOp({ args: makeArgs({ path: "alb-deploy" }), plugins: [], serializers: [] });
702
- expect(exit).toBe(1);
703
- expect(stderr.join("\n")).toContain("Profile not found: prod");
704
- });
175
+ });
705
176
 
706
- test("missing dist/ops/<name>/worker.ts → exit 1 with build hint", async () => {
707
- discoverOpsMock.mockResolvedValue({ ops: new Map([makeOp("alb-deploy")]), errors: [] });
708
- loadChantConfigMock.mockResolvedValue({ config: {} });
709
- resolveProfileMock.mockReturnValue({ address: "localhost:7233", namespace: "default", taskQueue: "q" });
710
- existsSyncMock.mockReturnValue(false);
711
- const stderr = makeStderrSpy();
712
- const exit = await runOp({ args: makeArgs({ path: "alb-deploy" }), plugins: [], serializers: [] });
713
- expect(exit).toBe(1);
714
- expect(stderr.join("\n")).toContain("worker.ts not found");
715
- expect(stderr.join("\n")).toContain("`chant build` first");
716
- });
177
+ // ── local mode dispatcher + guards ──────────────────────────────────────────
717
178
 
718
- test("--report path: prints generated report from describe + history", async () => {
719
- discoverOpsMock.mockResolvedValue({ ops: new Map([makeOp("alb-deploy")]), errors: [] });
720
- setupTemporalClient(createMockTemporalClient({
721
- describeByWorkflowId: {
722
- "chant-op-alb-deploy": {
723
- workflowId: "chant-op-alb-deploy", runId: "r1",
724
- status: { name: "COMPLETED" }, startTime: new Date(),
725
- taskQueue: "alb-deploy", type: { name: "albDeployWorkflow" },
726
- },
727
- },
728
- historyByWorkflowId: { "chant-op-alb-deploy": [] },
729
- }));
730
- generateReportMock.mockReturnValue("# Report\nDeploy completed.");
731
- const stdoutSpy = vi.spyOn(process.stdout, "write").mockImplementation(() => true);
179
+ function localOp(name: string, steps: unknown[]) {
180
+ return [name, { config: { name, overview: `${name} overview`, phases: [{ name: "Phase", steps }] } }] as const;
181
+ }
732
182
 
733
- const exit = await runOp({ args: makeArgs({ path: "alb-deploy", report: true }), plugins: [], serializers: [] });
183
+ describe("runOp dispatcher", () => {
184
+ beforeEach(() => {
185
+ discoverOpsMock.mockReset();
186
+ loadChantConfigMock.mockReset().mockResolvedValue({ config: {} });
187
+ loadPluginsMock.mockReset().mockResolvedValue([]);
188
+ });
734
189
 
190
+ test("no --on → the built-in local runtime runs the Op in this process", async () => {
191
+ discoverOpsMock.mockResolvedValue({
192
+ ops: new Map([localOp("hello", [{ kind: "activity", fn: "shellCmd", args: { cmd: "true" } }])]),
193
+ errors: [],
194
+ });
195
+ const stderrWrite = vi.spyOn(process.stderr, "write").mockImplementation(() => true);
196
+ const exit = await runOp({ args: makeArgs({ path: "hello" }), plugins: [], serializers: [] });
735
197
  expect(exit).toBe(0);
736
- expect(generateReportMock).toHaveBeenCalledTimes(1);
737
- expect(stdoutSpy).toHaveBeenCalledWith("# Report\nDeploy completed.");
738
- stdoutSpy.mockRestore();
198
+ expect(loadPluginsMock).not.toHaveBeenCalled();
199
+ stderrWrite.mockRestore();
739
200
  });
740
201
 
741
- test("happy path: spawns worker, starts workflow, polls until COMPLETED, writes report, exits 0", async () => {
742
- vi.useFakeTimers();
743
- try {
744
- discoverOpsMock.mockResolvedValue({ ops: new Map([makeOp("alb-deploy")]), errors: [] });
745
- const mockClient = createMockTemporalClient({
746
- describeByWorkflowId: {
747
- "chant-op-alb-deploy": {
748
- workflowId: "chant-op-alb-deploy", runId: "r1",
749
- status: { name: "COMPLETED" }, startTime: new Date(),
750
- taskQueue: "alb-deploy", type: { name: "albDeployWorkflow" },
751
- },
752
- },
753
- historyByWorkflowId: {
754
- "chant-op-alb-deploy": [{ eventType: "ActivityTaskScheduled" }, { eventType: "ActivityTaskCompleted" }],
755
- },
756
- });
757
- setupTemporalClient(mockClient);
758
- existsSyncMock.mockReturnValue(true);
759
- const { proc } = makeFakeChildProcess();
760
- spawnChildMock.mockReturnValue(proc);
761
- generateReportMock.mockReturnValue("# Report");
762
- writeReportMock.mockReturnValue("/tmp/report.md");
763
- const stderrWriteSpy = vi.spyOn(process.stderr, "write").mockImplementation(() => true);
764
-
765
- const promise = runOp({ args: makeArgs({ path: "alb-deploy" }), plugins: [], serializers: [] });
766
- // Drive the polling loop forward.
767
- await vi.advanceTimersByTimeAsync(5000);
768
-
769
- const exit = await promise;
770
- expect(exit).toBe(0);
771
- expect(spawnChildMock).toHaveBeenCalledTimes(1);
772
- expect(spawnChildMock.mock.calls[0][0]).toBe("npx");
773
- expect(mockClient.calls.startCalls).toHaveLength(1);
774
- expect(mockClient.calls.startCalls[0].opts.workflowId).toBe("chant-op-alb-deploy");
775
- expect(generateReportMock).toHaveBeenCalledTimes(1);
776
- expect(writeReportMock).toHaveBeenCalledTimes(1);
777
- expect(proc.kill).toHaveBeenCalled();
778
-
779
- stderrWriteSpy.mockRestore();
780
- } finally {
781
- vi.useRealTimers();
782
- }
202
+ test("--report → exit 1 naming the removal, nothing run (#2116)", async () => {
203
+ discoverOpsMock.mockResolvedValue({ ops: new Map([makeOp("hello")]), errors: [] });
204
+ const stderr = makeStderrSpy();
205
+ const exit = await runOp({ args: makeArgs({ path: "hello", report: true }), plugins: [], serializers: [] });
206
+ expect(exit).toBe(1);
207
+ expect(stderr.join("\n")).toContain("#2116 removed");
208
+ expect(discoverOpsMock).not.toHaveBeenCalled();
783
209
  });
784
210
 
785
- // ── --progress-json (#1676) ──────────────────────────────────────────────
786
-
787
- test("--progress-json streams one NDJSON StepRecord per settled step, plus a final skipped record", async () => {
788
- vi.useFakeTimers();
789
- try {
790
- discoverOpsMock.mockResolvedValue({
791
- ops: new Map([[
792
- "alb-deploy",
793
- {
794
- config: {
795
- name: "alb-deploy", overview: "o",
796
- phases: [
797
- { name: "Build", steps: [{ kind: "activity", fn: "build" }] },
798
- { name: "Deploy", steps: [{ kind: "activity", fn: "deploy" }] },
799
- ],
800
- },
801
- },
802
- ]]),
803
- errors: [],
804
- });
805
- setupTemporalClient(createMockTemporalClient({
806
- describeByWorkflowId: {
807
- "chant-op-alb-deploy": {
808
- workflowId: "chant-op-alb-deploy", runId: "r1",
809
- status: { name: "COMPLETED" }, startTime: new Date(),
810
- taskQueue: "alb-deploy", type: { name: "albDeployWorkflow" },
811
- },
812
- },
813
- historyByWorkflowId: {
814
- "chant-op-alb-deploy": [
815
- { eventId: "1", eventType: "ActivityTaskScheduled", activityTaskScheduledEventAttributes: { activityId: "1", activityType: { name: "build" } } },
816
- { eventType: "ActivityTaskCompleted", activityTaskCompletedEventAttributes: { scheduledEventId: "1" } },
817
- ],
818
- },
819
- }));
820
- existsSyncMock.mockReturnValue(true);
821
- const { proc } = makeFakeChildProcess();
822
- spawnChildMock.mockReturnValue(proc);
823
- generateReportMock.mockReturnValue("# Report");
824
- writeReportMock.mockReturnValue("/tmp/report.md");
825
- vi.spyOn(process.stderr, "write").mockImplementation(() => true);
826
- const stdoutLines: string[] = [];
827
- const stdoutSpy = vi.spyOn(process.stdout, "write").mockImplementation((chunk) => { stdoutLines.push(String(chunk)); return true; });
828
-
829
- const promise = runOp({ args: makeArgs({ path: "alb-deploy", progressJson: true }), plugins: [], serializers: [] });
830
- await vi.advanceTimersByTimeAsync(5000);
831
- const exit = await promise;
832
-
833
- expect(exit).toBe(0);
834
- const records = stdoutLines.filter((l) => l.trim()).map((l) => JSON.parse(l));
835
- expect(records).toEqual([
836
- { phase: "Build", fn: "build", status: "ok", durationMs: 0 },
837
- { phase: "Deploy", fn: "deploy", status: "skipped", durationMs: 0 },
838
- ]);
839
- stdoutSpy.mockRestore();
840
- } finally {
841
- vi.useRealTimers();
842
- }
843
- }, 10_000);
844
-
845
- test("without --progress-json, nothing is written to stdout during the poll loop", async () => {
846
- vi.useFakeTimers();
847
- try {
848
- discoverOpsMock.mockResolvedValue({ ops: new Map([makeOp("alb-deploy")]), errors: [] });
849
- setupTemporalClient(createMockTemporalClient({
850
- describeByWorkflowId: {
851
- "chant-op-alb-deploy": {
852
- workflowId: "chant-op-alb-deploy", runId: "r1",
853
- status: { name: "COMPLETED" }, startTime: new Date(),
854
- taskQueue: "alb-deploy", type: { name: "albDeployWorkflow" },
855
- },
856
- },
857
- historyByWorkflowId: { "chant-op-alb-deploy": [] },
858
- }));
859
- existsSyncMock.mockReturnValue(true);
860
- const { proc } = makeFakeChildProcess();
861
- spawnChildMock.mockReturnValue(proc);
862
- generateReportMock.mockReturnValue("# Report");
863
- writeReportMock.mockReturnValue("/tmp/report.md");
864
- vi.spyOn(process.stderr, "write").mockImplementation(() => true);
865
- const stdoutSpy = vi.spyOn(process.stdout, "write").mockImplementation(() => true);
866
-
867
- const promise = runOp({ args: makeArgs({ path: "alb-deploy" }), plugins: [], serializers: [] });
868
- await vi.advanceTimersByTimeAsync(5000);
869
- await promise;
870
-
871
- expect(stdoutSpy).not.toHaveBeenCalled();
872
- stdoutSpy.mockRestore();
873
- } finally {
874
- vi.useRealTimers();
875
- }
211
+ test("gate in local mode → exit 3 and the approve line (#2119)", async () => {
212
+ gateLedger = memoryGateLedgerPort();
213
+ discoverOpsMock.mockResolvedValue({
214
+ ops: new Map([localOp("gated", [{ kind: "gate", gate: "approve-prod" }])]),
215
+ errors: [],
216
+ });
217
+ // `renderHuman` writes straight to process.stderr, not through console.error.
218
+ const stderrWrite = vi.spyOn(process.stderr, "write").mockImplementation(() => true);
219
+ const exit = await runOp({ args: makeArgs({ path: "gated" }), plugins: [], serializers: [] });
220
+ expect(exit).toBe(3);
221
+ const out = stderrWrite.mock.calls.map((c) => String(c[0])).join("");
222
+ stderrWrite.mockRestore();
223
+ expect(out).toContain('is gated on "approve-prod"');
224
+ expect(out).toContain("chant approve gated approve-prod");
225
+ expect(gateLedger.appended).toHaveLength(1);
876
226
  });
877
227
 
878
- test("workflow ends in FAILED → exit 1, worker still killed", async () => {
879
- vi.useFakeTimers();
880
- try {
881
- discoverOpsMock.mockResolvedValue({ ops: new Map([makeOp("alb-deploy")]), errors: [] });
882
- const mockClient = createMockTemporalClient({
883
- describeByWorkflowId: {
884
- "chant-op-alb-deploy": {
885
- workflowId: "chant-op-alb-deploy", runId: "r1",
886
- status: { name: "FAILED" }, startTime: new Date(),
887
- taskQueue: "alb-deploy", type: { name: "albDeployWorkflow" },
888
- },
889
- },
890
- historyByWorkflowId: { "chant-op-alb-deploy": [] },
891
- });
892
- setupTemporalClient(mockClient);
893
- existsSyncMock.mockReturnValue(true);
894
- const { proc } = makeFakeChildProcess();
895
- spawnChildMock.mockReturnValue(proc);
896
- generateReportMock.mockReturnValue("# Report");
897
- writeReportMock.mockReturnValue("/tmp/report.md");
898
- vi.spyOn(process.stderr, "write").mockImplementation(() => true);
899
-
900
- const promise = runOp({ args: makeArgs({ path: "alb-deploy" }), plugins: [], serializers: [] });
901
- await vi.advanceTimersByTimeAsync(5000);
228
+ test("an approved gate lets the run through and exits 0", async () => {
229
+ gateLedger = memoryGateLedgerPort({
230
+ pending: [{
231
+ version: 1, kind: "pending", op: "gated", gate: "approve-prod",
232
+ timestamp: "2026-09-05T10:00:00.000Z", expiresAt: "2099-01-01T00:00:00.000Z",
233
+ }],
234
+ resolutions: [{
235
+ version: 1, op: "gated", gate: "approve-prod",
236
+ resolvedBy: "alex", timestamp: "2026-09-05T11:00:00.000Z",
237
+ }],
238
+ });
239
+ discoverOpsMock.mockResolvedValue({
240
+ ops: new Map([localOp("gated", [
241
+ { kind: "gate", gate: "approve-prod" },
242
+ { kind: "activity", fn: "shellCmd", args: { cmd: "true" } },
243
+ ])]),
244
+ errors: [],
245
+ });
246
+ const stderrWrite = vi.spyOn(process.stderr, "write").mockImplementation(() => true);
247
+ const exit = await runOp({ args: makeArgs({ path: "gated" }), plugins: [], serializers: [] });
248
+ expect(exit).toBe(0);
249
+ expect(stderrWrite.mock.calls.map((c) => String(c[0])).join("")).toContain("[approved] alex");
250
+ stderrWrite.mockRestore();
251
+ });
902
252
 
903
- const exit = await promise;
904
- expect(exit).toBe(1);
905
- expect(proc.kill).toHaveBeenCalled();
906
- } finally {
907
- vi.useRealTimers();
908
- }
253
+ test("--json → the run's ledger record on stdout (#2118)", async () => {
254
+ discoverOpsMock.mockResolvedValue({
255
+ ops: new Map([localOp("hello", [{ kind: "activity", fn: "shellCmd", args: { cmd: "true" } }])]),
256
+ errors: [],
257
+ });
258
+ const stdoutWrite = vi.spyOn(process.stdout, "write").mockImplementation(() => true);
259
+ vi.spyOn(process.stderr, "write").mockImplementation(() => true);
260
+ const exit = await runOp({ args: makeArgs({ path: "hello", json: true }), plugins: [], serializers: [] });
261
+ expect(exit).toBe(0);
262
+ const printed = stdoutWrite.mock.calls.map((c) => String(c[0])).join("");
263
+ const parsed = JSON.parse(printed.trim());
264
+ expect(parsed.op).toBe("hello");
265
+ expect(parsed.version).toBe(1);
266
+ expect(parsed.status).toBe("ok");
267
+ expect(parsed.phases[0].steps[0]).toMatchObject({ fn: "shellCmd", status: "ok" });
268
+ vi.restoreAllMocks();
909
269
  });
910
270
  });
911
271
 
912
- // ── local mode dispatcher + guards ──────────────────────────────────────────
272
+ /**
273
+ * chant #2243 — a gated run's exit code is the one thing `--gated-exit`
274
+ * remaps, and the gate is reported where CI can see it without opening a log.
275
+ *
276
+ * The motivating shape is a push-to-main terraform apply: GitHub Actions has
277
+ * no neutral conclusion for a `run:` step, so exit 3 paints the branch red on
278
+ * every merge until someone approves. `--gated-exit 0` says a pending
279
+ * approval is not a broken build; nothing else about the run changes, and a
280
+ * failure is still a failure.
281
+ */
282
+ describe("runOp: --gated-exit (#2243)", () => {
283
+ const gateStep = { kind: "gate", gate: "approve-prod" };
913
284
 
914
- function localOp(name: string, steps: unknown[]) {
915
- return [name, { config: { name, overview: `${name} overview`, phases: [{ name: "Phase", steps }] } }] as const;
916
- }
285
+ function summaryFile(): string {
286
+ return join(mkdtempSync(join(tmpdir(), "chant-gated-exit-")), "summary.md");
287
+ }
917
288
 
918
- describe("runOp dispatcher", () => {
919
289
  beforeEach(() => {
920
290
  discoverOpsMock.mockReset();
921
- loadTemporalClientMock.mockReset();
922
- loadChantConfigMock.mockReset();
923
- resolveProfileMock.mockReset();
924
- existsSyncMock.mockReset();
925
- spawnChildMock.mockReset();
291
+ loadChantConfigMock.mockReset().mockResolvedValue({ config: {} });
292
+ loadPluginsMock.mockReset().mockResolvedValue([]);
293
+ gateLedger = memoryGateLedgerPort();
294
+ delete process.env.GITHUB_STEP_SUMMARY;
295
+ });
296
+
297
+ afterEach(() => {
298
+ delete process.env.GITHUB_STEP_SUMMARY;
299
+ vi.restoreAllMocks();
300
+ });
301
+
302
+ test("unmapped: without the flag a gated run still exits 3", async () => {
303
+ discoverOpsMock.mockResolvedValue({ ops: new Map([localOp("gated", [gateStep])]), errors: [] });
304
+ vi.spyOn(process.stderr, "write").mockImplementation(() => true);
305
+ const exit = await runOp({ args: makeArgs({ path: "gated" }), plugins: [], serializers: [] });
306
+ expect(exit).toBe(3);
926
307
  });
927
308
 
928
- test("no flag → local executor (no Temporal client or worker spawned)", async () => {
929
- discoverOpsMock.mockResolvedValue({
930
- ops: new Map([localOp("hello", [{ kind: "activity", fn: "shellCmd", args: { cmd: "true" } }])]),
931
- errors: [],
309
+ test("mapped: --gated-exit 0 exits 0 and writes the gate to GITHUB_STEP_SUMMARY", async () => {
310
+ const file = summaryFile();
311
+ process.env.GITHUB_STEP_SUMMARY = file;
312
+ discoverOpsMock.mockResolvedValue({ ops: new Map([localOp("gated", [gateStep])]), errors: [] });
313
+ vi.spyOn(process.stderr, "write").mockImplementation(() => true);
314
+ const stderr = makeStderrSpy();
315
+
316
+ const exit = await runOp({
317
+ args: makeArgs({ path: "gated", gatedExit: 0 }),
318
+ plugins: [], serializers: [],
932
319
  });
933
- const stderrWrite = vi.spyOn(process.stderr, "write").mockImplementation(() => true);
934
- const exit = await runOp({ args: makeArgs({ path: "hello", temporal: false }), plugins: [], serializers: [] });
320
+
935
321
  expect(exit).toBe(0);
936
- expect(loadTemporalClientMock).not.toHaveBeenCalled();
937
- expect(spawnChildMock).not.toHaveBeenCalled();
938
- stderrWrite.mockRestore();
939
- });
322
+ // The run genuinely stopped: the pending fact is on the ledger, and
323
+ // nothing after the gate ran.
324
+ expect(gateLedger.appended).toHaveLength(1);
325
+ const summary = readFileSync(file, "utf8");
326
+ expect(summary).toContain("approve-prod");
327
+ expect(summary).toContain("chant approve gated approve-prod");
328
+ expect(summary).toContain("_gates/gated.jsonl");
329
+ // A green run that applied nothing says so on stderr too.
330
+ expect(stderr.join("\n")).toContain("exiting 0");
331
+ });
332
+
333
+ test("mapped: the --json payload still reports the gate, not a success", async () => {
334
+ const file = summaryFile();
335
+ process.env.GITHUB_STEP_SUMMARY = file;
336
+ discoverOpsMock.mockResolvedValue({ ops: new Map([localOp("gated", [gateStep])]), errors: [] });
337
+ const stdoutWrite = vi.spyOn(process.stdout, "write").mockImplementation(() => true);
338
+ vi.spyOn(process.stderr, "write").mockImplementation(() => true);
339
+ makeStderrSpy();
940
340
 
941
- test("--temporal → Temporal path (missing worker.ts → exit 1)", async () => {
942
- discoverOpsMock.mockResolvedValue({ ops: new Map([makeOp("hello")]), errors: [] });
943
- loadChantConfigMock.mockResolvedValue({ config: {} });
944
- resolveProfileMock.mockReturnValue({ address: "localhost:7233", namespace: "default", taskQueue: "q" });
945
- existsSyncMock.mockReturnValue(false);
946
- const stderr = makeStderrSpy();
947
- const exit = await runOp({ args: makeArgs({ path: "hello", temporal: true }), plugins: [], serializers: [] });
948
- expect(exit).toBe(1);
949
- expect(stderr.join("\n")).toContain("worker.ts not found");
341
+ const exit = await runOp({
342
+ args: makeArgs({ path: "gated", gatedExit: 0, json: true }),
343
+ plugins: [], serializers: [],
344
+ });
345
+
346
+ expect(exit).toBe(0);
347
+ const parsed = JSON.parse(stdoutWrite.mock.calls.map((c) => String(c[0])).join("").trim());
348
+ expect(parsed.status).toBe("gated");
349
+ expect(parsed.gate).toMatchObject({ name: "approve-prod" });
350
+ expect(parsed.approve).toBe("chant approve gated approve-prod");
351
+ // The summary is written whether or not stdout is machine-readable.
352
+ expect(readFileSync(file, "utf8")).toContain("chant approve gated approve-prod");
950
353
  });
951
354
 
952
- test("gate in local mode → fast-fail before execution, suggests --temporal", async () => {
355
+ test("a run that fails for any other reason is still red under the flag", async () => {
953
356
  discoverOpsMock.mockResolvedValue({
954
- ops: new Map([localOp("gated", [{ kind: "gate", signalName: "approve-prod" }])]),
357
+ ops: new Map([localOp("broken", [{ kind: "activity", fn: "shellCmd", args: { cmd: "exit 7" } }])]),
955
358
  errors: [],
956
359
  });
957
- const stderr = makeStderrSpy();
958
- const exit = await runOp({ args: makeArgs({ path: "gated", temporal: false }), plugins: [], serializers: [] });
360
+ const file = summaryFile();
361
+ process.env.GITHUB_STEP_SUMMARY = file;
362
+ vi.spyOn(process.stderr, "write").mockImplementation(() => true);
363
+
364
+ const exit = await runOp({
365
+ args: makeArgs({ path: "broken", gatedExit: 0 }),
366
+ plugins: [], serializers: [],
367
+ });
368
+
959
369
  expect(exit).toBe(1);
960
- expect(stderr.join("\n")).toContain("--temporal");
961
- expect(loadTemporalClientMock).not.toHaveBeenCalled();
370
+ // Nothing is pending, so nothing is reported as pending.
371
+ expect(existsSync(file)).toBe(false);
962
372
  });
963
373
 
964
- test("--local and --temporal together → exit 1 before any work", async () => {
374
+ test("a value that is not an exit status is refused before the Op runs", async () => {
375
+ discoverOpsMock.mockResolvedValue({ ops: new Map([localOp("gated", [gateStep])]), errors: [] });
965
376
  const stderr = makeStderrSpy();
966
- const exit = await runOp({ args: makeArgs({ path: "hello", local: true, temporal: true }), plugins: [], serializers: [] });
377
+ const exit = await runOp({
378
+ args: makeArgs({ path: "gated", gatedExit: 300 }),
379
+ plugins: [], serializers: [],
380
+ });
967
381
  expect(exit).toBe(1);
968
- expect(stderr.join("\n")).toContain("mutually exclusive");
382
+ expect(stderr.join("\n")).toContain("--gated-exit expects a whole number from 0 to 255");
969
383
  expect(discoverOpsMock).not.toHaveBeenCalled();
970
- expect(loadTemporalClientMock).not.toHaveBeenCalled();
971
384
  });
972
385
 
973
- test("--json → structured result on stdout", async () => {
974
- discoverOpsMock.mockResolvedValue({
975
- ops: new Map([localOp("hello", [{ kind: "activity", fn: "shellCmd", args: { cmd: "true" } }])]),
976
- errors: [],
977
- });
978
- const stdoutWrite = vi.spyOn(process.stdout, "write").mockImplementation(() => true);
386
+ test("no GITHUB_STEP_SUMMARY, no file: the variable is the whole forge coupling", async () => {
387
+ discoverOpsMock.mockResolvedValue({ ops: new Map([localOp("gated", [gateStep])]), errors: [] });
979
388
  vi.spyOn(process.stderr, "write").mockImplementation(() => true);
980
- const exit = await runOp({ args: makeArgs({ path: "hello", temporal: false, json: true }), plugins: [], serializers: [] });
389
+ const exit = await runOp({
390
+ args: makeArgs({ path: "gated", gatedExit: 0 }),
391
+ plugins: [], serializers: [],
392
+ });
981
393
  expect(exit).toBe(0);
982
- const printed = stdoutWrite.mock.calls.map((c) => String(c[0])).join("");
983
- const parsed = JSON.parse(printed.trim());
984
- expect(parsed.op).toBe("hello");
985
- expect(parsed.ok).toBe(true);
986
- vi.restoreAllMocks();
394
+ expect(process.env.GITHUB_STEP_SUMMARY).toBeUndefined();
987
395
  });
988
396
  });
989
397
 
@@ -1001,18 +409,15 @@ describe("runOp: --sandbox with a policyGate step (chant #2003)", () => {
1001
409
 
1002
410
  beforeEach(() => {
1003
411
  discoverOpsMock.mockReset();
1004
- loadTemporalClientMock.mockReset();
1005
- loadChantConfigMock.mockReset();
1006
- resolveProfileMock.mockReset();
1007
- existsSyncMock.mockReset();
1008
- spawnChildMock.mockReset();
412
+ loadChantConfigMock.mockReset().mockResolvedValue({ config: { lexicons: ["stub"] } });
413
+ loadPluginsMock.mockReset().mockResolvedValue([]);
1009
414
  });
1010
415
 
1011
- test("local mode → refuses before any phase runs, naming the combination", async () => {
416
+ test("refuses before any phase runs, naming the combination", async () => {
1012
417
  discoverOpsMock.mockResolvedValue({ ops: new Map([policyGateOp("gate")]), errors: [] });
1013
418
  const stderr = makeStderrSpy();
1014
419
 
1015
- const exit = await runOp({ args: makeArgs({ path: "gate", temporal: false, sandbox: true }), plugins: [], serializers: [] });
420
+ const exit = await runOp({ args: makeArgs({ path: "gate", sandbox: true }), plugins: [], serializers: [] });
1016
421
 
1017
422
  expect(exit).toBe(1);
1018
423
  const out = stderr.join("\n");
@@ -1022,19 +427,6 @@ describe("runOp: --sandbox with a policyGate step (chant #2003)", () => {
1022
427
  expect(out).not.toContain("This is a chant bug");
1023
428
  });
1024
429
 
1025
- test("--temporal → the same refusal, as a CLI error rather than an activity failure", async () => {
1026
- discoverOpsMock.mockResolvedValue({ ops: new Map([policyGateOp("gate")]), errors: [] });
1027
- const stderr = makeStderrSpy();
1028
-
1029
- const exit = await runOp({ args: makeArgs({ path: "gate", temporal: true, sandbox: true }), plugins: [], serializers: [] });
1030
-
1031
- expect(exit).toBe(1);
1032
- expect(stderr.join("\n")).toContain("policyGate");
1033
- // Refused before the project config is even loaded — nothing is scheduled.
1034
- expect(loadChantConfigMock).not.toHaveBeenCalled();
1035
- expect(loadTemporalClientMock).not.toHaveBeenCalled();
1036
- });
1037
-
1038
430
  test("a policyGate nested in an effect step is found too", async () => {
1039
431
  discoverOpsMock.mockResolvedValue({
1040
432
  ops: new Map([
@@ -1046,7 +438,7 @@ describe("runOp: --sandbox with a policyGate step (chant #2003)", () => {
1046
438
  });
1047
439
  const stderr = makeStderrSpy();
1048
440
 
1049
- const exit = await runOp({ args: makeArgs({ path: "gate", temporal: false, sandbox: true }), plugins: [], serializers: [] });
441
+ const exit = await runOp({ args: makeArgs({ path: "gate", sandbox: true }), plugins: [], serializers: [] });
1050
442
 
1051
443
  expect(exit).toBe(1);
1052
444
  expect(stderr.join("\n")).toContain("policyGate");
@@ -1054,19 +446,68 @@ describe("runOp: --sandbox with a policyGate step (chant #2003)", () => {
1054
446
 
1055
447
  test("without --sandbox the same Op is not refused", async () => {
1056
448
  discoverOpsMock.mockResolvedValue({ ops: new Map([policyGateOp("gate")]), errors: [] });
1057
- loadChantConfigMock.mockResolvedValue({ config: {} });
1058
- resolveProfileMock.mockReturnValue({ address: "localhost:7233", namespace: "default", taskQueue: "q" });
1059
- existsSyncMock.mockReturnValue(false);
449
+ const start = vi.fn(async () => ({
450
+ op: "gate",
451
+ runId: "stub-1",
452
+ result: async () => ({ op: "gate", runId: "stub-1", state: "completed", startedAt: "2026-01-01T00:00:00.000Z" }),
453
+ }));
454
+ makeStdoutSpy();
1060
455
  const stderr = makeStderrSpy();
1061
456
 
1062
- // The Temporal path, so the run stops at the missing worker.ts rather than
1063
- // actually building a project: reaching that point proves the pre-flight
1064
- // let it through.
1065
- const exit = await runOp({ args: makeArgs({ path: "gate", temporal: true }), plugins: [], serializers: [] });
457
+ // On a stub runtime, so reaching `start` proves the pre-flight let it
458
+ // through without the policyGate itself having to run here.
459
+ const exit = await runOp({
460
+ args: makeArgs({ path: "gate", on: "stub" }),
461
+ plugins: [{ name: "stub", opRuntime: { name: "stub", start } } as never], serializers: [],
462
+ });
1066
463
 
1067
- expect(exit).toBe(1);
1068
- expect(stderr.join("\n")).toContain("worker.ts not found");
1069
- expect(loadChantConfigMock).toHaveBeenCalled();
464
+ expect(exit).toBe(0);
465
+ expect(start).toHaveBeenCalledTimes(1);
466
+ expect(stderr.join("\n")).not.toContain("policyGate");
467
+ });
468
+
469
+ // #2192 — `--profile` is core's flag and the runtime's meaning. The handler
470
+ // only has to hand it over; fountain's own tests cover what it does with it.
471
+ test("--profile reaches the hosting runtime's start options", async () => {
472
+ discoverOpsMock.mockResolvedValue({ ops: new Map([makeOp("prod-apply")]), errors: [] });
473
+ const start = vi.fn(async () => ({
474
+ op: "prod-apply",
475
+ runId: "stub-1",
476
+ result: async () => ({ op: "prod-apply", runId: "stub-1", state: "completed", startedAt: "2026-01-01T00:00:00.000Z" }),
477
+ }));
478
+ makeStdoutSpy();
479
+ makeStderrSpy();
480
+
481
+ const exit = await runOp({
482
+ args: makeArgs({ path: "prod-apply", on: "stub", profile: "staging" }),
483
+ plugins: [{ name: "stub", opRuntime: { name: "stub", start } } as never], serializers: [],
484
+ });
485
+
486
+ expect(exit).toBe(0);
487
+ expect(start).toHaveBeenCalledWith(expect.anything(), expect.objectContaining({ profile: "staging" }));
488
+ });
489
+
490
+ test("without --profile the runtime is left to its own default", async () => {
491
+ discoverOpsMock.mockResolvedValue({ ops: new Map([makeOp("prod-apply")]), errors: [] });
492
+ let seen: Record<string, unknown> | undefined;
493
+ const start = vi.fn(async (_op: unknown, opts: Record<string, unknown>) => {
494
+ seen = opts;
495
+ return {
496
+ op: "prod-apply",
497
+ runId: "stub-1",
498
+ result: async () => ({ op: "prod-apply", runId: "stub-1", state: "completed", startedAt: "2026-01-01T00:00:00.000Z" }),
499
+ };
500
+ });
501
+ makeStdoutSpy();
502
+ makeStderrSpy();
503
+
504
+ await runOp({
505
+ args: makeArgs({ path: "prod-apply", on: "stub" }),
506
+ plugins: [{ name: "stub", opRuntime: { name: "stub", start } } as never], serializers: [],
507
+ });
508
+
509
+ expect(seen).toBeDefined();
510
+ expect(seen).not.toHaveProperty("profile");
1070
511
  });
1071
512
 
1072
513
  test("--sandbox on an Op with no policyGate step is untouched", async () => {
@@ -1076,27 +517,220 @@ describe("runOp: --sandbox with a policyGate step (chant #2003)", () => {
1076
517
  });
1077
518
  const stderrWrite = vi.spyOn(process.stderr, "write").mockImplementation(() => true);
1078
519
 
1079
- const exit = await runOp({ args: makeArgs({ path: "hello", temporal: false, sandbox: true }), plugins: [], serializers: [] });
520
+ const exit = await runOp({ args: makeArgs({ path: "hello", sandbox: true }), plugins: [], serializers: [] });
1080
521
 
1081
522
  expect(exit).toBe(0);
1082
523
  stderrWrite.mockRestore();
1083
524
  });
1084
525
  });
1085
526
 
1086
- describe("Temporal-only subcommand guards", () => {
527
+ /**
528
+ * #2116 — `run list/status/log/cancel --components` reported a component's
529
+ * durable run state. Nothing keeps one any more, so each refuses with a line
530
+ * that says so and points at the command that still answers. The Op-path
531
+ * counterparts run on the resolved runtime, covered below.
532
+ */
533
+ describe("run <sub> --components refuses (#2116)", () => {
1087
534
  const cases: Array<[string, (ctx: { args: ParsedArgs; plugins: never[]; serializers: never[] }) => Promise<number>]> = [
1088
535
  ["list", runOpList],
1089
536
  ["status", runOpStatus],
1090
537
  ["log", runOpLog],
1091
- ["signal", runOpSignal],
1092
538
  ["cancel", runOpCancel],
1093
539
  ];
1094
540
 
1095
- test.each(cases)("run %s without --temporal → exit 1 + actionable message", async (_name, handler) => {
541
+ test.each(cases)("run %s --components -> exit 1 naming the removal", async (_name, handler) => {
542
+ const stderr = makeStderrSpy();
543
+ const exit = await handler({
544
+ args: makeArgs({ components: true, force: true, extraPositional: "x", extraPositional2: "y" }),
545
+ plugins: [], serializers: [],
546
+ });
547
+ expect(exit).toBe(1);
548
+ expect(stderr.join("\n")).toContain("durable run state, which #2116 removed");
549
+ });
550
+
551
+ test("run list --components points at `chant list --components`", async () => {
552
+ const stderr = makeStderrSpy();
553
+ const exit = await runOpList({ args: makeArgs({ components: true }), plugins: [], serializers: [] });
554
+ expect(exit).toBe(1);
555
+ expect(stderr.join("\n")).toContain("chant list --components");
556
+ expect(listComponentsMock).not.toHaveBeenCalled();
557
+ });
558
+ });
559
+
560
+ // ── the runtime seam (#2121) ────────────────────────────────────────────────
561
+
562
+ describe("run subcommands on the resolved runtime", () => {
563
+ function makeStubRuntime(overrides: Record<string, unknown> = {}) {
564
+ return {
565
+ name: "stub",
566
+ start: vi.fn(async (op: { name: string }) => ({
567
+ op: op.name,
568
+ runId: "stub-1",
569
+ result: async () => ({
570
+ op: op.name, runId: "stub-1", state: "completed", startedAt: "2026-01-01T00:00:00.000Z",
571
+ endedAt: "2026-01-01T00:00:05.000Z",
572
+ }),
573
+ })),
574
+ status: vi.fn(async (op: string) => ({
575
+ op, runId: "stub-1", state: "running", startedAt: "2026-01-01T00:00:00.000Z",
576
+ })),
577
+ // A provider's `log` returns run-ledger records (#2118), newest first.
578
+ log: vi.fn(async (op: string) => [
579
+ {
580
+ version: 1, id: "stub-1", op, env: "staging", status: "ok",
581
+ started: "2026-01-01T00:00:00.000Z", ended: "2026-01-01T00:00:05.000Z",
582
+ labels: {}, outcomes: {}, phases: [],
583
+ },
584
+ ]),
585
+ list: vi.fn(async (ops: Array<{ name: string }>) =>
586
+ new Map(ops.map((o) => [o.name, { op: o.name, runId: "stub-1", state: "completed", startedAt: "2026-01-01T00:00:00.000Z" }]))),
587
+ cancel: vi.fn(async () => undefined),
588
+ ...overrides,
589
+ };
590
+ }
591
+
592
+ function stubPlugin(runtime: unknown) {
593
+ return { name: "stub", opRuntime: runtime } as never;
594
+ }
595
+
596
+ beforeEach(() => {
597
+ discoverOpsMock.mockReset();
598
+ loadChantConfigMock.mockReset().mockResolvedValue({ config: { lexicons: ["stub"] } });
599
+ loadPluginsMock.mockReset().mockResolvedValue([]);
600
+ });
601
+
602
+ test("--on stub reaches the stub lexicon's start", async () => {
603
+ const runtime = makeStubRuntime();
604
+ discoverOpsMock.mockResolvedValue({ ops: new Map([makeOp("hello")]), errors: [] });
605
+ makeStdoutSpy();
606
+ const exit = await runOp({
607
+ args: makeArgs({ path: "hello", on: "stub" }),
608
+ plugins: [stubPlugin(runtime)], serializers: [],
609
+ });
610
+ expect(exit).toBe(0);
611
+ expect(runtime.start).toHaveBeenCalledTimes(1);
612
+ expect((runtime.start.mock.calls[0][0] as { name: string }).name).toBe("hello");
613
+ });
614
+
615
+ test("run status/log/list/cancel --on stub reach their methods", async () => {
616
+ const runtime = makeStubRuntime();
617
+ discoverOpsMock.mockResolvedValue({ ops: new Map([makeOp("hello")]), errors: [] });
618
+ makeStdoutSpy();
619
+ makeStderrSpy();
620
+ const ctx = (over: Partial<ParsedArgs>) => ({
621
+ args: makeArgs({ on: "stub", ...over }),
622
+ plugins: [stubPlugin(runtime)], serializers: [],
623
+ });
624
+
625
+ expect(await runOpStatus(ctx({ extraPositional: "hello" }))).toBe(0);
626
+ expect(await runOpLog(ctx({ extraPositional: "hello" }))).toBe(0);
627
+ expect(await runOpList(ctx({}))).toBe(0);
628
+ expect(await runOpCancel(ctx({ extraPositional: "hello", force: true }))).toBe(0);
629
+
630
+ expect(runtime.status).toHaveBeenCalledWith("hello");
631
+ expect(runtime.log).toHaveBeenCalledWith("hello", undefined);
632
+ expect(runtime.list).toHaveBeenCalledTimes(1);
633
+ expect(runtime.cancel).toHaveBeenCalledWith("hello", { force: true });
634
+ });
635
+
636
+ test("--on nope → exit 1 naming the configured lexicons", async () => {
637
+ discoverOpsMock.mockResolvedValue({ ops: new Map([makeOp("hello")]), errors: [] });
638
+ loadChantConfigMock.mockResolvedValue({ config: { lexicons: ["aws", "k8s"] } });
639
+ const stderr = makeStderrSpy();
640
+ const exit = await runOp({
641
+ args: makeArgs({ path: "hello", on: "nope" }),
642
+ plugins: [], serializers: [],
643
+ });
644
+ expect(exit).toBe(1);
645
+ expect(stderr.join("\n")).toContain("is not a configured lexicon");
646
+ expect(stderr.join("\n")).toContain("aws, k8s");
647
+ });
648
+
649
+ test("--on a lexicon with no opRuntime → exit 1 naming the lexicon", async () => {
650
+ discoverOpsMock.mockResolvedValue({ ops: new Map([makeOp("hello")]), errors: [] });
651
+ const stderr = makeStderrSpy();
652
+ const exit = await runOp({
653
+ args: makeArgs({ path: "hello", on: "plain" }),
654
+ plugins: [{ name: "plain" } as never], serializers: [],
655
+ });
656
+ expect(exit).toBe(1);
657
+ expect(stderr.join("\n")).toContain('lexicon "plain" does not host Op runs');
658
+ });
659
+
660
+ test("--components on a runtime that cannot host them → one line, exit 1", async () => {
661
+ const runtime = makeStubRuntime();
662
+ const stderr = makeStderrSpy();
663
+ const exit = await runOpComponents({
664
+ args: makeArgs({ path: "search-service", components: true, on: "stub" }),
665
+ plugins: [stubPlugin(runtime)], serializers: [],
666
+ });
667
+ expect(exit).toBe(1);
668
+ expect(stderr.join("\n")).toContain('--components is not supported on the "stub" runtime');
669
+ expect(runComponentsMock).not.toHaveBeenCalled();
670
+ });
671
+ });
672
+
673
+ describe("runOpApprove", () => {
674
+ beforeEach(() => {
675
+ recordGateApprovalMock.mockReset();
676
+ loadChantConfigMock.mockReset().mockResolvedValue({ config: { lexicons: ["stub"] } });
677
+ loadPluginsMock.mockReset().mockResolvedValue([]);
678
+ });
679
+
680
+ test("missing op or gate name → exit 1, nothing recorded", async () => {
681
+ const stderr = makeStderrSpy();
682
+ const exit = await runOpApprove({
683
+ args: makeArgs({ extraPositional: "alb-deploy" }),
684
+ plugins: [], serializers: [],
685
+ });
686
+ expect(exit).toBe(1);
687
+ expect(stderr.join("\n")).toContain("chant run approve <op> <gate>");
688
+ expect(recordGateApprovalMock).not.toHaveBeenCalled();
689
+ });
690
+
691
+ test("writes the resolution, then calls the provider's resolveGate", async () => {
692
+ const record = { version: 1, op: "alb-deploy", gate: "release", resolvedBy: "alex", timestamp: "2026-01-01T00:00:00.000Z" };
693
+ recordGateApprovalMock.mockResolvedValue({ ok: true, record });
694
+ const resolveGate = vi.fn(async () => undefined);
695
+ const stderr = makeStderrSpy();
696
+
697
+ const exit = await runOpApprove({
698
+ args: makeArgs({ on: "stub", extraPositional: "alb-deploy", extraPositional2: "release", approver: "alex" }),
699
+ plugins: [{ name: "stub", opRuntime: { name: "stub", resolveGate } } as never], serializers: [],
700
+ });
701
+
702
+ expect(exit).toBe(0);
703
+ expect(recordGateApprovalMock).toHaveBeenCalledWith("alb-deploy", "release", {
704
+ actor: "alex", note: undefined, url: undefined,
705
+ });
706
+ expect(resolveGate).toHaveBeenCalledWith("alb-deploy", "release", record);
707
+ expect(stderr.join("\n")).toContain('Runtime "stub" was notified');
708
+ });
709
+
710
+ test("a provider with no resolveGate still records the fact and exits 0", async () => {
711
+ const record = { version: 1, op: "alb-deploy", gate: "release", resolvedBy: "ci", timestamp: "2026-01-01T00:00:00.000Z" };
712
+ recordGateApprovalMock.mockResolvedValue({ ok: true, record });
1096
713
  const stderr = makeStderrSpy();
1097
- const exit = await handler({ args: makeArgs({ temporal: false, extraPositional: "x", extraPositional2: "y" }), plugins: [], serializers: [] });
714
+
715
+ const exit = await runOpApprove({
716
+ args: makeArgs({ extraPositional: "alb-deploy", extraPositional2: "release" }),
717
+ plugins: [], serializers: [],
718
+ });
719
+
720
+ expect(exit).toBe(0);
721
+ expect(recordGateApprovalMock).toHaveBeenCalledTimes(1);
722
+ expect(stderr.join("\n")).toContain("when the op next runs");
723
+ });
724
+
725
+ test("a refused ledger write → exit 1 without waking the runtime", async () => {
726
+ recordGateApprovalMock.mockResolvedValue({ ok: false });
727
+ const resolveGate = vi.fn(async () => undefined);
728
+ const exit = await runOpApprove({
729
+ args: makeArgs({ on: "stub", extraPositional: "alb-deploy", extraPositional2: "release" }),
730
+ plugins: [{ name: "stub", opRuntime: { name: "stub", resolveGate } } as never], serializers: [],
731
+ });
1098
732
  expect(exit).toBe(1);
1099
- expect(stderr.join("\n")).toContain("not available in local mode");
733
+ expect(resolveGate).not.toHaveBeenCalled();
1100
734
  });
1101
735
  });
1102
736
 
@@ -1112,10 +746,10 @@ describe("runOp dispatcher: --components routes to runOpComponents", () => {
1112
746
 
1113
747
  test("runOp with --components dispatches to runComponents, not Op discovery", async () => {
1114
748
  discoverOpsMock.mockReset();
1115
- runComponentsMock.mockResolvedValue({ success: true, selected: ["svc"], run: { order: ["svc"], waves: [["svc"]], results: [{ component: "svc", ok: true, records: [] }], ok: true } });
749
+ runComponentsMock.mockResolvedValue({ success: true, selected: ["svc"], run: { order: ["svc"], waves: [["svc"]], results: [{ component: "svc", ok: true, status: "ok", records: [] }], ok: true, status: "ok" } });
1116
750
  vi.spyOn(process.stderr, "write").mockImplementation(() => true);
1117
751
 
1118
- const exit = await runOp({ args: makeArgs({ path: "svc", components: true, temporal: false }), plugins: [], serializers: [] });
752
+ const exit = await runOp({ args: makeArgs({ path: "svc", components: true }), plugins: [], serializers: [] });
1119
753
 
1120
754
  expect(exit).toBe(0);
1121
755
  expect(runComponentsMock).toHaveBeenCalledWith(expect.any(String), "svc", { env: undefined, componentOutputs: {}, buildParams: [] });
@@ -1123,15 +757,14 @@ describe("runOp dispatcher: --components routes to runOpComponents", () => {
1123
757
  vi.restoreAllMocks();
1124
758
  });
1125
759
 
1126
- // chant #1116 — --report is Op/Temporal-only (reads a past workflow run);
1127
- // the component driver never checked it, so it was silently ignored and the
1128
- // command fell through to a real dispatch. Hard-error instead, before
1129
- // runComponents is ever reached.
760
+ // chant #1116 — the component driver never checked --report, so it was
761
+ // silently ignored and the command fell through to a real dispatch.
762
+ // Hard-error instead, before runComponents is ever reached.
1130
763
  test("--report combined with --components → exit 1 before any dispatch, no fall-through (#1116)", async () => {
1131
764
  discoverOpsMock.mockReset();
1132
765
  const stderr = makeStderrSpy();
1133
766
 
1134
- const exit = await runOp({ args: makeArgs({ path: "svc", components: true, report: true, temporal: false }), plugins: [], serializers: [] });
767
+ const exit = await runOp({ args: makeArgs({ path: "svc", components: true, report: true }), plugins: [], serializers: [] });
1135
768
 
1136
769
  expect(exit).toBe(1);
1137
770
  expect(stderr.join("\n")).toContain("not supported with --components");
@@ -1144,10 +777,10 @@ describe("runOp dispatcher: --components routes to runOpComponents", () => {
1144
777
  // real dispatch through runComponents — mocked here, never a real cloud call.
1145
778
  test("plain --components (no --report) still dispatches to runComponents (#1116 regression guard)", async () => {
1146
779
  discoverOpsMock.mockReset();
1147
- runComponentsMock.mockResolvedValue({ success: true, selected: ["svc"], run: { order: ["svc"], waves: [["svc"]], results: [{ component: "svc", ok: true, records: [] }], ok: true } });
780
+ runComponentsMock.mockResolvedValue({ success: true, selected: ["svc"], run: { order: ["svc"], waves: [["svc"]], results: [{ component: "svc", ok: true, status: "ok", records: [] }], ok: true, status: "ok" } });
1148
781
  vi.spyOn(process.stderr, "write").mockImplementation(() => true);
1149
782
 
1150
- const exit = await runOp({ args: makeArgs({ path: "svc", components: true, report: false, temporal: false }), plugins: [], serializers: [] });
783
+ const exit = await runOp({ args: makeArgs({ path: "svc", components: true, report: false }), plugins: [], serializers: [] });
1151
784
 
1152
785
  expect(exit).toBe(0);
1153
786
  expect(runComponentsMock).toHaveBeenCalled();
@@ -1165,7 +798,7 @@ describe("runOpComponents", () => {
1165
798
 
1166
799
  test("no component name → exit 1 with hint", async () => {
1167
800
  const stderr = makeStderrSpy();
1168
- const exit = await runOpComponents({ args: makeArgs({ path: ".", temporal: false }), plugins: [], serializers: [] });
801
+ const exit = await runOpComponents({ args: makeArgs({ path: "." }), plugins: [], serializers: [] });
1169
802
  expect(exit).toBe(1);
1170
803
  expect(stderr.join("\n")).toContain("Component name is required");
1171
804
  expect(runComponentsMock).not.toHaveBeenCalled();
@@ -1181,11 +814,11 @@ describe("runOpComponents", () => {
1181
814
  runComponentsMock.mockResolvedValue({
1182
815
  success: true,
1183
816
  selected: ["svc"],
1184
- run: { order: ["svc"], waves: [["svc"]], results: [{ component: "svc", ok: true, records: [] }], ok: true },
817
+ run: { order: ["svc"], waves: [["svc"]], results: [{ component: "svc", ok: true, status: "ok", records: [] }], ok: true, status: "ok" },
1185
818
  });
1186
819
  const stderr = makeStderrSpy();
1187
820
 
1188
- const exit = await runOpComponents({ args: makeArgs({ path: "svc", temporal: false }), plugins: [], serializers: [] });
821
+ const exit = await runOpComponents({ args: makeArgs({ path: "svc" }), plugins: [], serializers: [] });
1189
822
 
1190
823
  expect(exit).toBe(0);
1191
824
  expect(runComponentsMock).toHaveBeenCalledWith(expect.any(String), "svc", expect.objectContaining({
@@ -1203,13 +836,13 @@ describe("runOpComponents", () => {
1203
836
  runComponentsMock.mockResolvedValue({
1204
837
  success: true,
1205
838
  selected: ["svc"],
1206
- run: { order: ["svc"], waves: [["svc"]], results: [{ component: "svc", ok: true, records: [] }], ok: true },
839
+ run: { order: ["svc"], waves: [["svc"]], results: [{ component: "svc", ok: true, status: "ok", records: [] }], ok: true, status: "ok" },
1207
840
  });
1208
841
  vi.spyOn(process.stderr, "write").mockImplementation(() => true);
1209
842
  const stderr = makeStderrSpy();
1210
843
 
1211
844
  const exit = await runOpComponents({
1212
- args: makeArgs({ path: "svc", temporal: false, param: ["tier=production"] }),
845
+ args: makeArgs({ path: "svc", param: ["tier=production"] }),
1213
846
  plugins: [],
1214
847
  serializers: [],
1215
848
  });
@@ -1228,7 +861,7 @@ describe("runOpComponents", () => {
1228
861
  });
1229
862
  const stderr = makeStderrSpy();
1230
863
 
1231
- const exit = await runOpComponents({ args: makeArgs({ path: "svc", temporal: false }), plugins: [], serializers: [] });
864
+ const exit = await runOpComponents({ args: makeArgs({ path: "svc" }), plugins: [], serializers: [] });
1232
865
 
1233
866
  expect(exit).toBe(1);
1234
867
  expect(stderr.join("\n")).toMatch(/"tier"/);
@@ -1242,7 +875,7 @@ describe("runOpComponents", () => {
1242
875
  const stderr = makeStderrSpy();
1243
876
 
1244
877
  const exit = await runOpComponents({
1245
- args: makeArgs({ path: "svc", temporal: false, param: ["tier=bogus"] }),
878
+ args: makeArgs({ path: "svc", param: ["tier=bogus"] }),
1246
879
  plugins: [],
1247
880
  serializers: [],
1248
881
  });
@@ -1261,13 +894,14 @@ describe("runOpComponents", () => {
1261
894
  run: {
1262
895
  order: ["svc"],
1263
896
  waves: [["svc"]],
1264
- results: [{ component: "svc", ok: true, records: [{ component: "svc", phase: "Apply", kind: "cfn-deploy", status: "ok", durationMs: 5 }] }],
897
+ results: [{ component: "svc", ok: true, status: "ok", records: [{ component: "svc", phase: "Apply", kind: "cfn-deploy", status: "ok", durationMs: 5 }] }],
1265
898
  ok: true,
899
+ status: "ok",
1266
900
  },
1267
901
  });
1268
902
  const stderrWrite = vi.spyOn(process.stderr, "write").mockImplementation(() => true);
1269
903
 
1270
- const exit = await runOpComponents({ args: makeArgs({ path: "svc", temporal: false }), plugins: [], serializers: [] });
904
+ const exit = await runOpComponents({ args: makeArgs({ path: "svc" }), plugins: [], serializers: [] });
1271
905
 
1272
906
  expect(exit).toBe(0);
1273
907
  expect(runComponentsMock).toHaveBeenCalledWith(expect.any(String), "svc", { env: undefined, componentOutputs: {}, buildParams: [] });
@@ -1277,21 +911,21 @@ describe("runOpComponents", () => {
1277
911
  });
1278
912
 
1279
913
  test("threads --env through to runComponents", async () => {
1280
- runComponentsMock.mockResolvedValue({ success: true, selected: ["svc"], run: { order: ["svc"], waves: [["svc"]], results: [{ component: "svc", ok: true, records: [] }], ok: true } });
914
+ runComponentsMock.mockResolvedValue({ success: true, selected: ["svc"], run: { order: ["svc"], waves: [["svc"]], results: [{ component: "svc", ok: true, status: "ok", records: [] }], ok: true, status: "ok" } });
1281
915
  vi.spyOn(process.stderr, "write").mockImplementation(() => true);
1282
916
 
1283
- await runOpComponents({ args: makeArgs({ path: "svc", env: "staging", temporal: false }), plugins: [], serializers: [] });
917
+ await runOpComponents({ args: makeArgs({ path: "svc", env: "staging" }), plugins: [], serializers: [] });
1284
918
 
1285
919
  expect(runComponentsMock).toHaveBeenCalledWith(expect.any(String), "svc", { env: "staging", componentOutputs: {}, buildParams: [] });
1286
920
  vi.restoreAllMocks();
1287
921
  });
1288
922
 
1289
923
  test("--json emits the DriverRunResult as JSON on stdout", async () => {
1290
- const run = { order: ["svc"], waves: [["svc"]], results: [{ component: "svc", ok: true, records: [] }], ok: true };
924
+ const run = { order: ["svc"], waves: [["svc"]], results: [{ component: "svc", ok: true, status: "ok", records: [] }], ok: true, status: "ok" };
1291
925
  runComponentsMock.mockResolvedValue({ success: true, selected: ["svc"], run });
1292
926
  const stdoutWrite = vi.spyOn(process.stdout, "write").mockImplementation(() => true);
1293
927
 
1294
- const exit = await runOpComponents({ args: makeArgs({ path: "svc", json: true, temporal: false }), plugins: [], serializers: [] });
928
+ const exit = await runOpComponents({ args: makeArgs({ path: "svc", json: true }), plugins: [], serializers: [] });
1295
929
 
1296
930
  expect(exit).toBe(0);
1297
931
  const printed = stdoutWrite.mock.calls.map((c) => String(c[0])).join("");
@@ -1309,7 +943,7 @@ describe("runOpComponents", () => {
1309
943
  return {
1310
944
  success: true,
1311
945
  selected: ["svc"],
1312
- run: { order: ["svc"], waves: [["svc"]], results: [{ component: "svc", ok: true, records: [] }], ok: true },
946
+ run: { order: ["svc"], waves: [["svc"]], results: [{ component: "svc", ok: true, status: "ok", records: [] }], ok: true, status: "ok" },
1313
947
  };
1314
948
  },
1315
949
  );
@@ -1317,7 +951,7 @@ describe("runOpComponents", () => {
1317
951
  vi.spyOn(process.stderr, "write").mockImplementation(() => true);
1318
952
 
1319
953
  const exit = await runOpComponents({
1320
- args: makeArgs({ path: "svc", progressJson: true, temporal: false }),
954
+ args: makeArgs({ path: "svc", progressJson: true }),
1321
955
  plugins: [],
1322
956
  serializers: [],
1323
957
  });
@@ -1337,12 +971,12 @@ describe("runOpComponents", () => {
1337
971
  runComponentsMock.mockResolvedValue({
1338
972
  success: true,
1339
973
  selected: ["svc"],
1340
- run: { order: ["svc"], waves: [["svc"]], results: [{ component: "svc", ok: true, records: [] }], ok: true },
974
+ run: { order: ["svc"], waves: [["svc"]], results: [{ component: "svc", ok: true, status: "ok", records: [] }], ok: true, status: "ok" },
1341
975
  });
1342
976
  const stdoutWrite = vi.spyOn(process.stdout, "write").mockImplementation(() => true);
1343
977
  vi.spyOn(process.stderr, "write").mockImplementation(() => true);
1344
978
 
1345
- await runOpComponents({ args: makeArgs({ path: "svc", temporal: false }), plugins: [], serializers: [] });
979
+ await runOpComponents({ args: makeArgs({ path: "svc" }), plugins: [], serializers: [] });
1346
980
 
1347
981
  expect(runComponentsMock).toHaveBeenCalledWith(expect.any(String), "svc", {
1348
982
  env: undefined,
@@ -1362,15 +996,16 @@ describe("runOpComponents", () => {
1362
996
  order: ["shared-alb", "search-service"],
1363
997
  waves: [["shared-alb"], ["search-service"]],
1364
998
  results: [
1365
- { component: "shared-alb", ok: true, records: [] },
1366
- { component: "search-service", ok: true, records: [] },
999
+ { component: "shared-alb", ok: true, status: "ok", records: [] },
1000
+ { component: "search-service", ok: true, status: "ok", records: [] },
1367
1001
  ],
1368
1002
  ok: true,
1003
+ status: "ok",
1369
1004
  },
1370
1005
  });
1371
1006
  const stderrWrite = vi.spyOn(process.stderr, "write").mockImplementation(() => true);
1372
1007
 
1373
- const exit = await runOpComponents({ args: makeArgs({ path: "all", temporal: false }), plugins: [], serializers: [] });
1008
+ const exit = await runOpComponents({ args: makeArgs({ path: "all" }), plugins: [], serializers: [] });
1374
1009
 
1375
1010
  expect(exit).toBe(0);
1376
1011
  expect(runComponentsMock).toHaveBeenCalledWith(expect.any(String), "all", { env: undefined, componentOutputs: {}, buildParams: [] });
@@ -1387,14 +1022,15 @@ describe("runOpComponents", () => {
1387
1022
  run: {
1388
1023
  order: ["svc"],
1389
1024
  waves: [["svc"]],
1390
- results: [{ component: "svc", ok: false, records: [{ component: "svc", phase: "Apply", kind: "cfn-deploy", status: "fail", durationMs: 5, error: "boom" }] }],
1025
+ results: [{ component: "svc", ok: false, status: "fail", records: [{ component: "svc", phase: "Apply", kind: "cfn-deploy", status: "fail", durationMs: 5, error: "boom" }] }],
1391
1026
  ok: false,
1027
+ status: "fail",
1392
1028
  failedComponent: "svc",
1393
1029
  },
1394
1030
  });
1395
1031
  const stderrWrite = vi.spyOn(process.stderr, "write").mockImplementation(() => true);
1396
1032
 
1397
- const exit = await runOpComponents({ args: makeArgs({ path: "svc", temporal: false }), plugins: [], serializers: [] });
1033
+ const exit = await runOpComponents({ args: makeArgs({ path: "svc" }), plugins: [], serializers: [] });
1398
1034
 
1399
1035
  expect(exit).toBe(1);
1400
1036
  const printed = stderrWrite.mock.calls.map((c) => String(c[0])).join("");
@@ -1406,26 +1042,33 @@ describe("runOpComponents", () => {
1406
1042
  runComponentsMock.mockResolvedValue({ success: false, selected: [], error: 'Component "missing" not found. Known components: svc' });
1407
1043
  const stderr = makeStderrSpy();
1408
1044
 
1409
- const exit = await runOpComponents({ args: makeArgs({ path: "missing", temporal: false }), plugins: [], serializers: [] });
1045
+ const exit = await runOpComponents({ args: makeArgs({ path: "missing" }), plugins: [], serializers: [] });
1410
1046
 
1411
1047
  expect(exit).toBe(1);
1412
1048
  expect(stderr.join("\n")).toContain('Component "missing" not found');
1413
1049
  });
1414
1050
 
1415
- test("a gate in the component (local mode) → exit 1 with an actionable, Temporal-pointing message", async () => {
1051
+ test("a gate in the component → exit 3 and the approve line (#2119)", async () => {
1416
1052
  runComponentsMock.mockResolvedValue({
1417
1053
  success: false,
1418
1054
  selected: ["svc"],
1419
- gateUnsupported: { component: "svc", signalName: "release-approval" },
1055
+ gated: {
1056
+ component: "svc",
1057
+ gate: {
1058
+ version: 1, kind: "pending", op: "svc", gate: "release-approval",
1059
+ timestamp: "2026-09-05T12:00:00.000Z", expiresAt: "2026-09-07T12:00:00.000Z",
1060
+ },
1061
+ },
1420
1062
  });
1421
1063
  const stderr = makeStderrSpy();
1422
1064
 
1423
- const exit = await runOpComponents({ args: makeArgs({ path: "svc", temporal: false }), plugins: [], serializers: [] });
1065
+ const exit = await runOpComponents({ args: makeArgs({ path: "svc" }), plugins: [], serializers: [] });
1424
1066
 
1425
- expect(exit).toBe(1);
1067
+ expect(exit).toBe(3);
1426
1068
  const out = stderr.join("\n");
1427
- expect(out).toContain('gate "release-approval"');
1428
- expect(out).toContain("--temporal");
1069
+ expect(out).toContain('gated on "release-approval"');
1070
+ expect(out).toContain("chant approve svc release-approval");
1071
+ expect(out).toContain("expires : 2026-09-07T12:00:00.000Z");
1429
1072
  });
1430
1073
 
1431
1074
  // ── auto-release recording post-run (#597) ────────────────────────────────
@@ -1438,14 +1081,15 @@ describe("runOpComponents", () => {
1438
1081
  run: {
1439
1082
  order: ["svc"],
1440
1083
  waves: [["svc"]],
1441
- results: [{ component: "svc", ok: true, records: [{ component: "svc", phase: "Publish", kind: "publish-image", status: "ok", durationMs: 5, output: { digest: "sha256:abc" } }] }],
1084
+ results: [{ component: "svc", ok: true, status: "ok", records: [{ component: "svc", phase: "Publish", kind: "publish-image", status: "ok", durationMs: 5, output: { digest: "sha256:abc" } }] }],
1442
1085
  ok: true,
1086
+ status: "ok",
1443
1087
  },
1444
1088
  });
1445
1089
  maybeRecordAutoReleaseMock.mockResolvedValue({ recorded: true, commit: "a".repeat(40), record: { version: 1, component: "svc", env: "staging", digest: "sha256:abc", gitSha: "x", runId: "local-1", timestamp: "t", actor: "a" } });
1446
1090
  vi.spyOn(process.stderr, "write").mockImplementation(() => true);
1447
1091
 
1448
- const exit = await runOpComponents({ args: makeArgs({ path: "svc", env: "staging", temporal: false }), plugins: [], serializers: [] });
1092
+ const exit = await runOpComponents({ args: makeArgs({ path: "svc", env: "staging" }), plugins: [], serializers: [] });
1449
1093
 
1450
1094
  expect(exit).toBe(0);
1451
1095
  expect(maybeRecordAutoReleaseMock).toHaveBeenCalledTimes(1);
@@ -1463,14 +1107,15 @@ describe("runOpComponents", () => {
1463
1107
  run: {
1464
1108
  order: ["svc"],
1465
1109
  waves: [["svc"]],
1466
- results: [{ component: "svc", ok: false, records: [{ component: "svc", phase: "Apply", kind: "cfn-deploy", status: "fail", durationMs: 5, error: "boom" }] }],
1110
+ results: [{ component: "svc", ok: false, status: "fail", records: [{ component: "svc", phase: "Apply", kind: "cfn-deploy", status: "fail", durationMs: 5, error: "boom" }] }],
1467
1111
  ok: false,
1112
+ status: "fail",
1468
1113
  failedComponent: "svc",
1469
1114
  },
1470
1115
  });
1471
1116
  vi.spyOn(process.stderr, "write").mockImplementation(() => true);
1472
1117
 
1473
- const exit = await runOpComponents({ args: makeArgs({ path: "svc", temporal: false }), plugins: [], serializers: [] });
1118
+ const exit = await runOpComponents({ args: makeArgs({ path: "svc" }), plugins: [], serializers: [] });
1474
1119
 
1475
1120
  expect(exit).toBe(1);
1476
1121
  expect(maybeRecordAutoReleaseMock).not.toHaveBeenCalled();
@@ -1481,12 +1126,12 @@ describe("runOpComponents", () => {
1481
1126
  runComponentsMock.mockResolvedValue({
1482
1127
  success: true,
1483
1128
  selected: ["svc"],
1484
- run: { order: ["svc"], waves: [["svc"]], results: [{ component: "svc", ok: true, records: [] }], ok: true },
1129
+ run: { order: ["svc"], waves: [["svc"]], results: [{ component: "svc", ok: true, status: "ok", records: [] }], ok: true, status: "ok" },
1485
1130
  });
1486
1131
  maybeRecordAutoReleaseMock.mockResolvedValue({ recorded: false, reason: "opted-out" });
1487
1132
  vi.spyOn(process.stderr, "write").mockImplementation(() => true);
1488
1133
 
1489
- const exit = await runOpComponents({ args: makeArgs({ path: "svc", temporal: false, noReleaseRecord: true }), plugins: [], serializers: [] });
1134
+ const exit = await runOpComponents({ args: makeArgs({ path: "svc", noReleaseRecord: true }), plugins: [], serializers: [] });
1490
1135
 
1491
1136
  expect(exit).toBe(0);
1492
1137
  expect(maybeRecordAutoReleaseMock).toHaveBeenCalledTimes(1);
@@ -1499,13 +1144,13 @@ describe("runOpComponents", () => {
1499
1144
  runComponentsMock.mockResolvedValue({
1500
1145
  success: true,
1501
1146
  selected: ["svc"],
1502
- run: { order: ["svc"], waves: [["svc"]], results: [{ component: "svc", ok: true, records: [] }], ok: true },
1147
+ run: { order: ["svc"], waves: [["svc"]], results: [{ component: "svc", ok: true, status: "ok", records: [] }], ok: true, status: "ok" },
1503
1148
  });
1504
1149
  loadChantConfigMock.mockResolvedValue({ config: { release: { autoRecord: false } } });
1505
1150
  maybeRecordAutoReleaseMock.mockResolvedValue({ recorded: false, reason: "opted-out" });
1506
1151
  vi.spyOn(process.stderr, "write").mockImplementation(() => true);
1507
1152
 
1508
- await runOpComponents({ args: makeArgs({ path: "svc", temporal: false }), plugins: [], serializers: [] });
1153
+ await runOpComponents({ args: makeArgs({ path: "svc" }), plugins: [], serializers: [] });
1509
1154
 
1510
1155
  const [, options] = maybeRecordAutoReleaseMock.mock.calls[0];
1511
1156
  expect(options).toMatchObject({ disabled: true });
@@ -1516,13 +1161,13 @@ describe("runOpComponents", () => {
1516
1161
  runComponentsMock.mockResolvedValue({
1517
1162
  success: true,
1518
1163
  selected: ["svc"],
1519
- run: { order: ["svc"], waves: [["svc"]], results: [{ component: "svc", ok: true, records: [] }], ok: true },
1164
+ run: { order: ["svc"], waves: [["svc"]], results: [{ component: "svc", ok: true, status: "ok", records: [] }], ok: true, status: "ok" },
1520
1165
  });
1521
1166
  maybeRecordAutoReleaseMock.mockResolvedValue({ recorded: false, reason: "error", error: "ledger push failed" });
1522
1167
  const stderr = makeStderrSpy();
1523
1168
  vi.spyOn(process.stderr, "write").mockImplementation(() => true);
1524
1169
 
1525
- const exit = await runOpComponents({ args: makeArgs({ path: "svc", temporal: false }), plugins: [], serializers: [] });
1170
+ const exit = await runOpComponents({ args: makeArgs({ path: "svc" }), plugins: [], serializers: [] });
1526
1171
 
1527
1172
  expect(exit).toBe(0);
1528
1173
  expect(stderr.join("\n")).toContain("ledger push failed");
@@ -1543,12 +1188,13 @@ describe("runOpComponents", () => {
1543
1188
  waves: [["svc"]],
1544
1189
  results: [{ component: "svc", ok: true, records: [{ component: "svc", phase: "Build", kind: "docker-build", status: "ok", durationMs: 5, output: buildOutput }] }],
1545
1190
  ok: true,
1191
+ status: "ok",
1546
1192
  },
1547
1193
  });
1548
1194
  maybePersistBuildManifestMock.mockResolvedValue({ persisted: true, commit: "a".repeat(40), manifestDigest: "sha256:manifestabc" });
1549
1195
  vi.spyOn(process.stderr, "write").mockImplementation(() => true);
1550
1196
 
1551
- const exit = await runOpComponents({ args: makeArgs({ path: "svc", env: "staging", temporal: false }), plugins: [], serializers: [] });
1197
+ const exit = await runOpComponents({ args: makeArgs({ path: "svc", env: "staging" }), plugins: [], serializers: [] });
1552
1198
 
1553
1199
  expect(exit).toBe(0);
1554
1200
  expect(maybePersistBuildManifestMock).toHaveBeenCalledTimes(1);
@@ -1566,14 +1212,15 @@ describe("runOpComponents", () => {
1566
1212
  run: {
1567
1213
  order: ["svc"],
1568
1214
  waves: [["svc"]],
1569
- results: [{ component: "svc", ok: false, records: [{ component: "svc", phase: "Apply", kind: "cfn-deploy", status: "fail", durationMs: 5, error: "boom" }] }],
1215
+ results: [{ component: "svc", ok: false, status: "fail", records: [{ component: "svc", phase: "Apply", kind: "cfn-deploy", status: "fail", durationMs: 5, error: "boom" }] }],
1570
1216
  ok: false,
1217
+ status: "fail",
1571
1218
  failedComponent: "svc",
1572
1219
  },
1573
1220
  });
1574
1221
  vi.spyOn(process.stderr, "write").mockImplementation(() => true);
1575
1222
 
1576
- const exit = await runOpComponents({ args: makeArgs({ path: "svc", temporal: false }), plugins: [], serializers: [] });
1223
+ const exit = await runOpComponents({ args: makeArgs({ path: "svc" }), plugins: [], serializers: [] });
1577
1224
 
1578
1225
  expect(exit).toBe(1);
1579
1226
  expect(maybePersistBuildManifestMock).not.toHaveBeenCalled();
@@ -1584,12 +1231,12 @@ describe("runOpComponents", () => {
1584
1231
  runComponentsMock.mockResolvedValue({
1585
1232
  success: true,
1586
1233
  selected: ["svc"],
1587
- run: { order: ["svc"], waves: [["svc"]], results: [{ component: "svc", ok: true, records: [] }], ok: true },
1234
+ run: { order: ["svc"], waves: [["svc"]], results: [{ component: "svc", ok: true, status: "ok", records: [] }], ok: true, status: "ok" },
1588
1235
  });
1589
1236
  maybePersistBuildManifestMock.mockResolvedValue({ persisted: false, reason: "opted-out" });
1590
1237
  vi.spyOn(process.stderr, "write").mockImplementation(() => true);
1591
1238
 
1592
- const exit = await runOpComponents({ args: makeArgs({ path: "svc", temporal: false, noReleaseRecord: true }), plugins: [], serializers: [] });
1239
+ const exit = await runOpComponents({ args: makeArgs({ path: "svc", noReleaseRecord: true }), plugins: [], serializers: [] });
1593
1240
 
1594
1241
  expect(exit).toBe(0);
1595
1242
  expect(maybePersistBuildManifestMock).toHaveBeenCalledTimes(1);
@@ -1602,13 +1249,13 @@ describe("runOpComponents", () => {
1602
1249
  runComponentsMock.mockResolvedValue({
1603
1250
  success: true,
1604
1251
  selected: ["svc"],
1605
- run: { order: ["svc"], waves: [["svc"]], results: [{ component: "svc", ok: true, records: [] }], ok: true },
1252
+ run: { order: ["svc"], waves: [["svc"]], results: [{ component: "svc", ok: true, status: "ok", records: [] }], ok: true, status: "ok" },
1606
1253
  });
1607
1254
  maybePersistBuildManifestMock.mockResolvedValue({ persisted: false, reason: "error", error: "manifest push failed" });
1608
1255
  const stderr = makeStderrSpy();
1609
1256
  vi.spyOn(process.stderr, "write").mockImplementation(() => true);
1610
1257
 
1611
- const exit = await runOpComponents({ args: makeArgs({ path: "svc", temporal: false }), plugins: [], serializers: [] });
1258
+ const exit = await runOpComponents({ args: makeArgs({ path: "svc" }), plugins: [], serializers: [] });
1612
1259
 
1613
1260
  expect(exit).toBe(0);
1614
1261
  expect(stderr.join("\n")).toContain("manifest push failed");
@@ -1616,377 +1263,3 @@ describe("runOpComponents", () => {
1616
1263
  });
1617
1264
  });
1618
1265
  });
1619
-
1620
- // ── chant run --components <name> --temporal (#589) ─────────────────────────
1621
-
1622
- describe("runOpComponents: --temporal routes to the durable path", () => {
1623
- beforeEach(() => {
1624
- resolveComponentTargetsMock.mockReset();
1625
- findComponentGateMock.mockReset();
1626
- loadComponentTemporalCodegenMock.mockReset();
1627
- // chant #1108 — runOpComponents now resolves build-time parameters (which
1628
- // needs chant.config.ts's declared `buildParams`) BEFORE dispatching to
1629
- // either the local or --temporal path, so every test in this block hits
1630
- // loadChantConfig at least once now, even ones that never reach the rest
1631
- // of the durable path (e.g. "unknown component"). Individual tests below
1632
- // still override this where they care about a specific config shape.
1633
- loadChantConfigMock.mockReset().mockResolvedValue({ config: {} });
1634
- resolveProfileMock.mockReset();
1635
- loadTemporalClientMock.mockReset();
1636
- spawnChildMock.mockReset();
1637
- existsSyncMock.mockReset();
1638
- findComponentGateMock.mockReturnValue(undefined);
1639
- maybeRecordAutoReleaseMock.mockReset().mockResolvedValue({ recorded: false, reason: "no-digest" });
1640
- maybePersistBuildManifestMock.mockReset().mockResolvedValue({ persisted: false, reason: "no-manifest" });
1641
- });
1642
-
1643
- test("all --temporal → exit 1, not supported", async () => {
1644
- const stderr = makeStderrSpy();
1645
- const exit = await runOpComponents({ args: makeArgs({ path: "all", temporal: true }), plugins: [], serializers: [] });
1646
- expect(exit).toBe(1);
1647
- expect(stderr.join("\n")).toContain("not supported");
1648
- expect(resolveComponentTargetsMock).not.toHaveBeenCalled();
1649
- });
1650
-
1651
- test("unknown component → exit 1 with the resolver's error message", async () => {
1652
- resolveComponentTargetsMock.mockResolvedValue({ success: false, targets: [], error: 'Component "missing" not found.' });
1653
- const stderr = makeStderrSpy();
1654
-
1655
- const exit = await runOpComponents({ args: makeArgs({ path: "missing", temporal: true }), plugins: [], serializers: [] });
1656
-
1657
- expect(exit).toBe(1);
1658
- expect(stderr.join("\n")).toContain('Component "missing" not found');
1659
- });
1660
-
1661
- // ── build-time parameters (chant #1108) — resolved before discovery here too ─
1662
-
1663
- test("resolved build-time parameters are forwarded into resolveComponentTargets on the --temporal path", async () => {
1664
- process.env.LOOM_ENV = "staging";
1665
- resolveComponentTargetsMock.mockResolvedValue({
1666
- success: true,
1667
- targets: [{ name: "gated-svc", dependsOn: [], deploy: [] }],
1668
- });
1669
- resolveProfileMock.mockReturnValue({ address: "localhost:7233", namespace: "default", taskQueue: "q" });
1670
- loadComponentTemporalCodegenMock.mockResolvedValue({
1671
- serializeComponent: () => ({ "components/gated-svc/worker.ts": "// worker" }),
1672
- componentWorkflowFnName: (name: string) => `${name}ComponentWorkflow`,
1673
- });
1674
- const mockClient = createMockTemporalClient({
1675
- describeByWorkflowId: {
1676
- "chant-component-gated-svc": {
1677
- workflowId: "chant-component-gated-svc", runId: "r1",
1678
- status: { name: "COMPLETED" }, startTime: new Date(),
1679
- taskQueue: "gated-svc", type: { name: "gatedSvcComponentWorkflow" },
1680
- },
1681
- },
1682
- historyByWorkflowId: { "chant-component-gated-svc": [] },
1683
- });
1684
- // setupTemporalClient sets its own default loadChantConfigMock resolved
1685
- // value, so the test's own (buildParams-declaring) config must be set
1686
- // AFTER calling it, not before.
1687
- setupTemporalClient(mockClient);
1688
- loadChantConfigMock.mockResolvedValue({
1689
- config: { buildParams: { env: { type: "string", env: "LOOM_ENV", default: "dev" } } },
1690
- });
1691
- const { proc } = makeFakeChildProcess();
1692
- spawnChildMock.mockReturnValue(proc);
1693
- vi.spyOn(process.stderr, "write").mockImplementation(() => true);
1694
-
1695
- try {
1696
- vi.useFakeTimers();
1697
- const promise = runOpComponents({ args: makeArgs({ path: "gated-svc", temporal: true }), plugins: [], serializers: [] });
1698
- await vi.advanceTimersByTimeAsync(5000);
1699
- const exit = await promise;
1700
-
1701
- expect(exit).toBe(0);
1702
- expect(resolveComponentTargetsMock).toHaveBeenCalledWith(
1703
- expect.any(String),
1704
- "gated-svc",
1705
- undefined,
1706
- [{ name: "env", value: "staging", source: "env" }],
1707
- );
1708
- } finally {
1709
- vi.useRealTimers();
1710
- vi.restoreAllMocks();
1711
- delete process.env.LOOM_ENV;
1712
- }
1713
- });
1714
-
1715
- test("an unresolved required build-time parameter → exit 1, never reaches resolveComponentTargets", async () => {
1716
- loadChantConfigMock.mockResolvedValue({
1717
- config: { buildParams: { tier: { type: "string" } } },
1718
- });
1719
- const stderr = makeStderrSpy();
1720
-
1721
- const exit = await runOpComponents({ args: makeArgs({ path: "gated-svc", temporal: true }), plugins: [], serializers: [] });
1722
-
1723
- expect(exit).toBe(1);
1724
- expect(stderr.join("\n")).toMatch(/"tier"/);
1725
- expect(resolveComponentTargetsMock).not.toHaveBeenCalled();
1726
- });
1727
-
1728
- test("compiles the component, spawns the worker, submits the workflow, polls to COMPLETED", async () => {
1729
- vi.useFakeTimers();
1730
- try {
1731
- resolveComponentTargetsMock.mockResolvedValue({
1732
- success: true,
1733
- targets: [{ name: "gated-svc", dependsOn: [], deploy: [] }],
1734
- });
1735
- loadChantConfigMock.mockResolvedValue({ config: {} });
1736
- resolveProfileMock.mockReturnValue({ address: "localhost:7233", namespace: "default", taskQueue: "q" });
1737
- const serializeComponentMock = vi.fn().mockReturnValue({
1738
- "components/gated-svc/workflow.ts": "// wf",
1739
- "components/gated-svc/activities.ts": "// act",
1740
- "components/gated-svc/worker.ts": "// worker",
1741
- });
1742
- loadComponentTemporalCodegenMock.mockResolvedValue({
1743
- serializeComponent: serializeComponentMock,
1744
- componentWorkflowFnName: (name: string) => `${name}ComponentWorkflow`,
1745
- });
1746
- const mockClient = createMockTemporalClient({
1747
- describeByWorkflowId: {
1748
- "chant-component-gated-svc": {
1749
- workflowId: "chant-component-gated-svc", runId: "r1",
1750
- status: { name: "COMPLETED" }, startTime: new Date(),
1751
- taskQueue: "gated-svc", type: { name: "gatedSvcComponentWorkflow" },
1752
- },
1753
- },
1754
- historyByWorkflowId: { "chant-component-gated-svc": [] },
1755
- });
1756
- setupTemporalClient(mockClient);
1757
- const { proc } = makeFakeChildProcess();
1758
- spawnChildMock.mockReturnValue(proc);
1759
- vi.spyOn(process.stderr, "write").mockImplementation(() => true);
1760
-
1761
- const promise = runOpComponents({ args: makeArgs({ path: "gated-svc", temporal: true, env: "staging" }), plugins: [], serializers: [] });
1762
- await vi.advanceTimersByTimeAsync(5000);
1763
- const exit = await promise;
1764
-
1765
- expect(exit).toBe(0);
1766
- expect(spawnChildMock).toHaveBeenCalledTimes(1);
1767
- expect(spawnChildMock.mock.calls[0][0]).toBe("npx");
1768
- expect(mockClient.calls.startCalls).toHaveLength(1);
1769
- expect(mockClient.calls.startCalls[0].opts.workflowId).toBe("chant-component-gated-svc");
1770
- expect(proc.kill).toHaveBeenCalled();
1771
- // --env must reach the codegen call, not just the local-mode path (the bug this locks in: #589 review).
1772
- expect(serializeComponentMock).toHaveBeenCalledWith(
1773
- expect.objectContaining({ name: "gated-svc" }),
1774
- expect.objectContaining({ env: "staging" }),
1775
- );
1776
- } finally {
1777
- vi.useRealTimers();
1778
- }
1779
- });
1780
-
1781
- test("a failed (non-COMPLETED) workflow → exit 1", async () => {
1782
- vi.useFakeTimers();
1783
- try {
1784
- resolveComponentTargetsMock.mockResolvedValue({
1785
- success: true,
1786
- targets: [{ name: "gated-svc", dependsOn: [], deploy: [] }],
1787
- });
1788
- loadChantConfigMock.mockResolvedValue({ config: {} });
1789
- resolveProfileMock.mockReturnValue({ address: "localhost:7233", namespace: "default", taskQueue: "q" });
1790
- loadComponentTemporalCodegenMock.mockResolvedValue({
1791
- serializeComponent: () => ({ "components/gated-svc/worker.ts": "// worker" }),
1792
- componentWorkflowFnName: (name: string) => `${name}ComponentWorkflow`,
1793
- });
1794
- const mockClient = createMockTemporalClient({
1795
- describeByWorkflowId: {
1796
- "chant-component-gated-svc": {
1797
- workflowId: "chant-component-gated-svc", runId: "r1",
1798
- status: { name: "FAILED" }, startTime: new Date(),
1799
- taskQueue: "gated-svc", type: { name: "gatedSvcComponentWorkflow" },
1800
- },
1801
- },
1802
- historyByWorkflowId: { "chant-component-gated-svc": [] },
1803
- });
1804
- setupTemporalClient(mockClient);
1805
- const { proc } = makeFakeChildProcess();
1806
- spawnChildMock.mockReturnValue(proc);
1807
- vi.spyOn(process.stderr, "write").mockImplementation(() => true);
1808
-
1809
- const promise = runOpComponents({ args: makeArgs({ path: "gated-svc", temporal: true }), plugins: [], serializers: [] });
1810
- await vi.advanceTimersByTimeAsync(5000);
1811
- const exit = await promise;
1812
-
1813
- expect(exit).toBe(1);
1814
- expect(proc.kill).toHaveBeenCalled();
1815
- // (#597) a failed workflow never reaches auto-release recording.
1816
- expect(maybeRecordAutoReleaseMock).not.toHaveBeenCalled();
1817
- } finally {
1818
- vi.useRealTimers();
1819
- }
1820
- });
1821
-
1822
- // ── auto-release recording post-run (#597) ────────────────────────────────
1823
-
1824
- describe("auto-release recording", () => {
1825
- async function runCompletedWorkflow(overrides: Partial<ParsedArgs> = {}, resultByWorkflowId?: unknown) {
1826
- resolveComponentTargetsMock.mockResolvedValue({
1827
- success: true,
1828
- targets: [{ name: "gated-svc", dependsOn: [], deploy: [] }],
1829
- });
1830
- loadChantConfigMock.mockResolvedValue({ config: {} });
1831
- resolveProfileMock.mockReturnValue({ address: "localhost:7233", namespace: "default", taskQueue: "q" });
1832
- loadComponentTemporalCodegenMock.mockResolvedValue({
1833
- serializeComponent: () => ({ "components/gated-svc/worker.ts": "// worker" }),
1834
- componentWorkflowFnName: (name: string) => `${name}ComponentWorkflow`,
1835
- });
1836
- const mockClient = createMockTemporalClient({
1837
- describeByWorkflowId: {
1838
- "chant-component-gated-svc": {
1839
- workflowId: "chant-component-gated-svc", runId: "r1",
1840
- status: { name: "COMPLETED" }, startTime: new Date(),
1841
- taskQueue: "gated-svc", type: { name: "gatedSvcComponentWorkflow" },
1842
- },
1843
- },
1844
- historyByWorkflowId: { "chant-component-gated-svc": [] },
1845
- resultByWorkflowId: resultByWorkflowId ? { "chant-component-gated-svc": resultByWorkflowId } : undefined,
1846
- });
1847
- setupTemporalClient(mockClient);
1848
- const { proc } = makeFakeChildProcess();
1849
- spawnChildMock.mockReturnValue(proc);
1850
- vi.spyOn(process.stderr, "write").mockImplementation(() => true);
1851
-
1852
- const promise = runOpComponents({ args: makeArgs({ path: "gated-svc", temporal: true, ...overrides }), plugins: [], serializers: [] });
1853
- await vi.advanceTimersByTimeAsync(5000);
1854
- const exit = await promise;
1855
- return { exit };
1856
- }
1857
-
1858
- test("a COMPLETED workflow → reads the digest via handle.result() and calls maybeRecordAutoRelease once", async () => {
1859
- vi.useFakeTimers();
1860
- try {
1861
- maybeRecordAutoReleaseMock.mockResolvedValue({
1862
- recorded: true, commit: "a".repeat(40),
1863
- record: { version: 1, component: "gated-svc", env: "staging", digest: "sha256:temporal-digest", gitSha: "x", runId: "r1", timestamp: "t", actor: "a" },
1864
- });
1865
-
1866
- const { exit } = await runCompletedWorkflow({ env: "staging" }, { phaseOutputs: { Publish: { digest: "sha256:temporal-digest" } }, componentOutputs: {} });
1867
-
1868
- expect(exit).toBe(0);
1869
- expect(maybeRecordAutoReleaseMock).toHaveBeenCalledTimes(1);
1870
- const [runInfo, options] = maybeRecordAutoReleaseMock.mock.calls[0];
1871
- expect(runInfo).toMatchObject({ component: "gated-svc", env: "staging", success: true, digest: "sha256:temporal-digest", runId: "r1" });
1872
- expect(options).toMatchObject({ disabled: false });
1873
- } finally {
1874
- vi.useRealTimers();
1875
- }
1876
- });
1877
-
1878
- test("--no-release-record → disabled: true is threaded through on the Temporal path too", async () => {
1879
- vi.useFakeTimers();
1880
- try {
1881
- maybeRecordAutoReleaseMock.mockResolvedValue({ recorded: false, reason: "opted-out" });
1882
-
1883
- await runCompletedWorkflow({ noReleaseRecord: true });
1884
-
1885
- const [, options] = maybeRecordAutoReleaseMock.mock.calls[0];
1886
- expect(options).toMatchObject({ disabled: true });
1887
- } finally {
1888
- vi.useRealTimers();
1889
- }
1890
- });
1891
-
1892
- test("no digest in the workflow result → maybeRecordAutoRelease still called, with digest: undefined", async () => {
1893
- vi.useFakeTimers();
1894
- try {
1895
- maybeRecordAutoReleaseMock.mockResolvedValue({ recorded: false, reason: "no-digest" });
1896
-
1897
- const { exit } = await runCompletedWorkflow({}, { phaseOutputs: {}, componentOutputs: {} });
1898
-
1899
- expect(exit).toBe(0);
1900
- const [runInfo] = maybeRecordAutoReleaseMock.mock.calls[0];
1901
- expect(runInfo.digest).toBeUndefined();
1902
- } finally {
1903
- vi.useRealTimers();
1904
- }
1905
- });
1906
- });
1907
-
1908
- // ── build-manifest persistence post-run (#609) ──────────────────────────────
1909
-
1910
- describe("build-manifest persistence", () => {
1911
- async function runCompletedWorkflow(overrides: Partial<ParsedArgs> = {}, resultByWorkflowId?: unknown) {
1912
- resolveComponentTargetsMock.mockResolvedValue({
1913
- success: true,
1914
- targets: [{ name: "gated-svc", dependsOn: [], deploy: [] }],
1915
- });
1916
- loadChantConfigMock.mockResolvedValue({ config: {} });
1917
- resolveProfileMock.mockReturnValue({ address: "localhost:7233", namespace: "default", taskQueue: "q" });
1918
- loadComponentTemporalCodegenMock.mockResolvedValue({
1919
- serializeComponent: () => ({ "components/gated-svc/worker.ts": "// worker" }),
1920
- componentWorkflowFnName: (name: string) => `${name}ComponentWorkflow`,
1921
- });
1922
- const mockClient = createMockTemporalClient({
1923
- describeByWorkflowId: {
1924
- "chant-component-gated-svc": {
1925
- workflowId: "chant-component-gated-svc", runId: "r1",
1926
- status: { name: "COMPLETED" }, startTime: new Date(),
1927
- taskQueue: "gated-svc", type: { name: "gatedSvcComponentWorkflow" },
1928
- },
1929
- },
1930
- historyByWorkflowId: { "chant-component-gated-svc": [] },
1931
- resultByWorkflowId: resultByWorkflowId ? { "chant-component-gated-svc": resultByWorkflowId } : undefined,
1932
- });
1933
- setupTemporalClient(mockClient);
1934
- const { proc } = makeFakeChildProcess();
1935
- spawnChildMock.mockReturnValue(proc);
1936
- vi.spyOn(process.stderr, "write").mockImplementation(() => true);
1937
-
1938
- const promise = runOpComponents({ args: makeArgs({ path: "gated-svc", temporal: true, ...overrides }), plugins: [], serializers: [] });
1939
- await vi.advanceTimersByTimeAsync(5000);
1940
- const exit = await promise;
1941
- return { exit };
1942
- }
1943
-
1944
- const sampleManifest = { version: 1, component: "gated-svc", createdAt: "t", contents: [], manifestDigest: "sha256:manifest-temporal" };
1945
-
1946
- test("a COMPLETED workflow → reads the manifest via handle.result() and calls maybePersistBuildManifest once", async () => {
1947
- vi.useFakeTimers();
1948
- try {
1949
- maybePersistBuildManifestMock.mockResolvedValue({ persisted: true, commit: "a".repeat(40), manifestDigest: "sha256:manifest-temporal" });
1950
-
1951
- const { exit } = await runCompletedWorkflow({ env: "staging" }, { phaseOutputs: { Build: { manifest: sampleManifest } }, componentOutputs: {} });
1952
-
1953
- expect(exit).toBe(0);
1954
- expect(maybePersistBuildManifestMock).toHaveBeenCalledTimes(1);
1955
- const [runInfo, options] = maybePersistBuildManifestMock.mock.calls[0];
1956
- expect(runInfo).toMatchObject({ success: true, manifest: sampleManifest });
1957
- expect(options).toMatchObject({ disabled: false });
1958
- } finally {
1959
- vi.useRealTimers();
1960
- }
1961
- });
1962
-
1963
- test("--no-release-record also disables manifest persistence on the Temporal path", async () => {
1964
- vi.useFakeTimers();
1965
- try {
1966
- maybePersistBuildManifestMock.mockResolvedValue({ persisted: false, reason: "opted-out" });
1967
-
1968
- await runCompletedWorkflow({ noReleaseRecord: true });
1969
-
1970
- const [, options] = maybePersistBuildManifestMock.mock.calls[0];
1971
- expect(options).toMatchObject({ disabled: true });
1972
- } finally {
1973
- vi.useRealTimers();
1974
- }
1975
- });
1976
-
1977
- test("no manifest in the workflow result → maybePersistBuildManifest still called, with manifest: undefined", async () => {
1978
- vi.useFakeTimers();
1979
- try {
1980
- maybePersistBuildManifestMock.mockResolvedValue({ persisted: false, reason: "no-manifest" });
1981
-
1982
- const { exit } = await runCompletedWorkflow({}, { phaseOutputs: {}, componentOutputs: {} });
1983
-
1984
- expect(exit).toBe(0);
1985
- const [runInfo] = maybePersistBuildManifestMock.mock.calls[0];
1986
- expect(runInfo.manifest).toBeUndefined();
1987
- } finally {
1988
- vi.useRealTimers();
1989
- }
1990
- });
1991
- });
1992
- });