@intentius/chant 0.56.0 → 0.58.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 (515) 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 +20 -1
  4. package/dist/audit/core.d.ts.map +1 -1
  5. package/dist/audit/discover.d.ts +14 -8
  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/check-lexicon-docs.d.ts +1 -1
  34. package/dist/cli/commands/check-lexicon-examples.d.ts +16 -6
  35. package/dist/cli/commands/check-lexicon-examples.d.ts.map +1 -1
  36. package/dist/cli/commands/import.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.map +1 -1
  44. package/dist/cli/commands/onboard.d.ts +2 -0
  45. package/dist/cli/commands/onboard.d.ts.map +1 -1
  46. package/dist/cli/handlers/build.d.ts.map +1 -1
  47. package/dist/cli/handlers/lifecycle.d.ts.map +1 -1
  48. package/dist/cli/handlers/operator.d.ts +82 -16
  49. package/dist/cli/handlers/operator.d.ts.map +1 -1
  50. package/dist/cli/handlers/run.d.ts +49 -42
  51. package/dist/cli/handlers/run.d.ts.map +1 -1
  52. package/dist/cli/main.d.ts +28 -1
  53. package/dist/cli/main.d.ts.map +1 -1
  54. package/dist/cli/mcp/op-tools.d.ts +16 -1
  55. package/dist/cli/mcp/op-tools.d.ts.map +1 -1
  56. package/dist/cli/mcp/resource-handlers.d.ts.map +1 -1
  57. package/dist/cli/mcp/resources/context.d.ts.map +1 -1
  58. package/dist/cli/registry.d.ts +26 -9
  59. package/dist/cli/registry.d.ts.map +1 -1
  60. package/dist/codegen/generate-post-synth-barrel.d.ts +1 -1
  61. package/dist/components/auto-release.d.ts +10 -20
  62. package/dist/components/auto-release.d.ts.map +1 -1
  63. package/dist/components/capability.d.ts +4 -4
  64. package/dist/components/cli-support.d.ts +20 -21
  65. package/dist/components/cli-support.d.ts.map +1 -1
  66. package/dist/components/component.d.ts +3 -3
  67. package/dist/components/component.d.ts.map +1 -1
  68. package/dist/components/driver-output.d.ts.map +1 -1
  69. package/dist/components/driver.d.ts +66 -36
  70. package/dist/components/driver.d.ts.map +1 -1
  71. package/dist/components/index.d.ts +1 -2
  72. package/dist/components/index.d.ts.map +1 -1
  73. package/dist/components/manifest-persistence.d.ts +1 -9
  74. package/dist/components/manifest-persistence.d.ts.map +1 -1
  75. package/dist/components/run-progress.d.ts +6 -6
  76. package/dist/components/run-progress.d.ts.map +1 -1
  77. package/dist/components/verbs/run-agent.d.ts +9 -15
  78. package/dist/components/verbs/run-agent.d.ts.map +1 -1
  79. package/dist/components/verbs/verify.d.ts +2 -2
  80. package/dist/deep-observation.d.ts +17 -0
  81. package/dist/deep-observation.d.ts.map +1 -1
  82. package/dist/discovery/files.d.ts +4 -4
  83. package/dist/discovery/fold-import.d.ts.map +1 -1
  84. package/dist/discovery/param-deps.d.ts +33 -0
  85. package/dist/discovery/param-deps.d.ts.map +1 -1
  86. package/dist/discovery/sandbox/bundle.d.ts.map +1 -1
  87. package/dist/discovery/sandbox/config-wire.d.ts +2 -2
  88. package/dist/fold/foldable-helpers.d.ts +5 -1
  89. package/dist/fold/foldable-helpers.d.ts.map +1 -1
  90. package/dist/fold-provenance.d.ts +236 -0
  91. package/dist/fold-provenance.d.ts.map +1 -0
  92. package/dist/graph-ops.d.ts +1 -1
  93. package/dist/held-elsewhere.d.ts +85 -0
  94. package/dist/held-elsewhere.d.ts.map +1 -0
  95. package/dist/import/ir-utils.d.ts +1 -1
  96. package/dist/import/ir-utils.d.ts.map +1 -1
  97. package/dist/import/parser.d.ts +33 -0
  98. package/dist/import/parser.d.ts.map +1 -1
  99. package/dist/index.d.ts +3 -0
  100. package/dist/index.d.ts.map +1 -1
  101. package/dist/kubectl-context.d.ts +3 -3
  102. package/dist/lexicon-config.d.ts +4 -4
  103. package/dist/lexicon-plugin-helpers.d.ts +1 -1
  104. package/dist/lexicon.d.ts +104 -14
  105. package/dist/lexicon.d.ts.map +1 -1
  106. package/dist/lifecycle/change-set.d.ts +13 -0
  107. package/dist/lifecycle/change-set.d.ts.map +1 -1
  108. package/dist/lifecycle/deep-diff.d.ts +133 -6
  109. package/dist/lifecycle/deep-diff.d.ts.map +1 -1
  110. package/dist/lifecycle/deep-observe.d.ts.map +1 -1
  111. package/dist/lifecycle/gate-ledger.d.ts +71 -3
  112. package/dist/lifecycle/gate-ledger.d.ts.map +1 -1
  113. package/dist/lifecycle/index.d.ts +1 -0
  114. package/dist/lifecycle/index.d.ts.map +1 -1
  115. package/dist/lifecycle/release-ledger.d.ts +2 -2
  116. package/dist/lifecycle/run-ledger.d.ts +53 -0
  117. package/dist/lifecycle/run-ledger.d.ts.map +1 -0
  118. package/dist/lifecycle/symptoms.d.ts +3 -3
  119. package/dist/lifecycle/symptoms.d.ts.map +1 -1
  120. package/dist/lint/config.d.ts +81 -2
  121. package/dist/lint/config.d.ts.map +1 -1
  122. package/dist/lint/post-synth.d.ts +55 -2
  123. package/dist/lint/post-synth.d.ts.map +1 -1
  124. package/dist/lint/rules/__fixtures__/comp/comp004/pass/neo4j-cluster.component.d.ts +1 -1
  125. package/dist/lint/rules/__fixtures__/comp/comp004/pass/neo4j-cluster.component.d.ts.map +1 -1
  126. package/dist/lint/rules/__fixtures__/comp/comp004/pass/orders-table.component.d.ts +1 -1
  127. package/dist/lint/rules/__fixtures__/comp/comp004/pass/orders-table.component.d.ts.map +1 -1
  128. package/dist/lint/rules/comp/comp004-gate-needs-durable-runtime.d.ts +44 -0
  129. package/dist/lint/rules/comp/comp004-gate-needs-durable-runtime.d.ts.map +1 -0
  130. package/dist/lint/rules/comp/index.d.ts +1 -1
  131. package/dist/lint/rules/comp/index.d.ts.map +1 -1
  132. package/dist/lint/rules/op/index.d.ts +16 -0
  133. package/dist/lint/rules/op/index.d.ts.map +1 -0
  134. package/dist/lint/rules/op/ops012-activity-contract.d.ts +27 -0
  135. package/dist/lint/rules/op/ops012-activity-contract.d.ts.map +1 -0
  136. package/dist/lint/rules/op/ops013-step-output-ref.d.ts +61 -0
  137. package/dist/lint/rules/op/ops013-step-output-ref.d.ts.map +1 -0
  138. package/dist/lint/rules/op/ops014-converge-rule-refusals.d.ts +55 -0
  139. package/dist/lint/rules/op/ops014-converge-rule-refusals.d.ts.map +1 -0
  140. package/dist/lint/rules/op/support.d.ts +12 -0
  141. package/dist/lint/rules/op/support.d.ts.map +1 -0
  142. package/dist/lint/suppressions.d.ts +108 -0
  143. package/dist/lint/suppressions.d.ts.map +1 -0
  144. package/dist/live-endpoint.d.ts +1 -1
  145. package/dist/op/activities/activity-contracts.d.ts +54 -0
  146. package/dist/op/activities/activity-contracts.d.ts.map +1 -0
  147. package/dist/op/activities/apply.d.ts +262 -0
  148. package/dist/op/activities/apply.d.ts.map +1 -0
  149. package/dist/op/activities/build.d.ts +13 -0
  150. package/dist/op/activities/build.d.ts.map +1 -0
  151. package/dist/op/activities/converge.d.ts +110 -0
  152. package/dist/op/activities/converge.d.ts.map +1 -0
  153. package/dist/op/activities/env-teardown.d.ts +76 -0
  154. package/dist/op/activities/env-teardown.d.ts.map +1 -0
  155. package/dist/op/activities/guard-validate.d.ts +64 -0
  156. package/dist/op/activities/guard-validate.d.ts.map +1 -0
  157. package/dist/op/activities/http-check.d.ts +34 -0
  158. package/dist/op/activities/http-check.d.ts.map +1 -0
  159. package/dist/op/activities/index.d.ts +31 -0
  160. package/dist/op/activities/index.d.ts.map +1 -0
  161. package/dist/op/activities/lexicon-upgrade.d.ts +182 -0
  162. package/dist/op/activities/lexicon-upgrade.d.ts.map +1 -0
  163. package/dist/op/activities/lifecycle.d.ts +42 -0
  164. package/dist/op/activities/lifecycle.d.ts.map +1 -0
  165. package/dist/op/activities/pipeline-audit.d.ts +84 -0
  166. package/dist/op/activities/pipeline-audit.d.ts.map +1 -0
  167. package/dist/op/activities/policy.d.ts +17 -0
  168. package/dist/op/activities/policy.d.ts.map +1 -0
  169. package/dist/op/activities/reconcile.d.ts +89 -0
  170. package/dist/op/activities/reconcile.d.ts.map +1 -0
  171. package/dist/op/activities/shell.d.ts +13 -0
  172. package/dist/op/activities/shell.d.ts.map +1 -0
  173. package/dist/op/activities/teardown.d.ts +10 -0
  174. package/dist/op/activities/teardown.d.ts.map +1 -0
  175. package/dist/op/activities/util.d.ts +7 -0
  176. package/dist/op/activities/util.d.ts.map +1 -0
  177. package/dist/op/activities/wait.d.ts +28 -0
  178. package/dist/op/activities/wait.d.ts.map +1 -0
  179. package/dist/op/activities/workflow-audit.d.ts +92 -0
  180. package/dist/op/activities/workflow-audit.d.ts.map +1 -0
  181. package/dist/op/activity-contract-registry.d.ts +76 -0
  182. package/dist/op/activity-contract-registry.d.ts.map +1 -0
  183. package/dist/op/activity-contract.d.ts +25 -18
  184. package/dist/op/activity-contract.d.ts.map +1 -1
  185. package/dist/op/activity-failure.d.ts +28 -0
  186. package/dist/op/activity-failure.d.ts.map +1 -0
  187. package/dist/op/activity-profiles.d.ts +118 -0
  188. package/dist/op/activity-profiles.d.ts.map +1 -0
  189. package/dist/op/activity-registry.d.ts +22 -38
  190. package/dist/op/activity-registry.d.ts.map +1 -1
  191. package/dist/op/activity-runtime.d.ts +3 -14
  192. package/dist/op/activity-runtime.d.ts.map +1 -1
  193. package/dist/op/builders.d.ts +84 -29
  194. package/dist/op/builders.d.ts.map +1 -1
  195. package/dist/op/composites/apply-op.d.ts +113 -0
  196. package/dist/op/composites/apply-op.d.ts.map +1 -0
  197. package/dist/op/composites/converge-op.d.ts +120 -0
  198. package/dist/op/composites/converge-op.d.ts.map +1 -0
  199. package/dist/op/composites/index.d.ts +26 -0
  200. package/dist/op/composites/index.d.ts.map +1 -0
  201. package/dist/op/composites/lexicon-upgrade-op.d.ts +78 -0
  202. package/dist/op/composites/lexicon-upgrade-op.d.ts.map +1 -0
  203. package/dist/op/composites/pipeline-audit-op.d.ts +50 -0
  204. package/dist/op/composites/pipeline-audit-op.d.ts.map +1 -0
  205. package/dist/op/composites/reconcile-op.d.ts +57 -0
  206. package/dist/op/composites/reconcile-op.d.ts.map +1 -0
  207. package/dist/op/composites/watch-op.d.ts +60 -0
  208. package/dist/op/composites/watch-op.d.ts.map +1 -0
  209. package/dist/op/composites/workflow-audit-op.d.ts +57 -0
  210. package/dist/op/composites/workflow-audit-op.d.ts.map +1 -0
  211. package/dist/op/converge-rule.d.ts +12 -10
  212. package/dist/op/converge-rule.d.ts.map +1 -1
  213. package/dist/op/cron.d.ts +51 -0
  214. package/dist/op/cron.d.ts.map +1 -0
  215. package/dist/op/discover.d.ts +9 -0
  216. package/dist/op/discover.d.ts.map +1 -1
  217. package/dist/op/duration.d.ts +11 -0
  218. package/dist/op/duration.d.ts.map +1 -0
  219. package/dist/op/emulator-lifecycle.d.ts.map +1 -1
  220. package/dist/op/gate.d.ts +90 -0
  221. package/dist/op/gate.d.ts.map +1 -0
  222. package/dist/op/generate-pipeline.d.ts +9 -0
  223. package/dist/op/generate-pipeline.d.ts.map +1 -1
  224. package/dist/op/index.d.ts +23 -7
  225. package/dist/op/index.d.ts.map +1 -1
  226. package/dist/op/local-executor.d.ts +114 -28
  227. package/dist/op/local-executor.d.ts.map +1 -1
  228. package/dist/op/local-output.d.ts +11 -1
  229. package/dist/op/local-output.d.ts.map +1 -1
  230. package/dist/op/op-ir.d.ts +184 -0
  231. package/dist/op/op-ir.d.ts.map +1 -0
  232. package/dist/op/op-verb-class.d.ts +10 -2
  233. package/dist/op/op-verb-class.d.ts.map +1 -1
  234. package/dist/op/operator.d.ts +30 -5
  235. package/dist/op/operator.d.ts.map +1 -1
  236. package/dist/op/receipt-store.d.ts +1 -1
  237. package/dist/op/receipt-store.d.ts.map +1 -1
  238. package/dist/op/resource.d.ts +15 -1
  239. package/dist/op/resource.d.ts.map +1 -1
  240. package/dist/op/runtime.d.ts +200 -0
  241. package/dist/op/runtime.d.ts.map +1 -0
  242. package/dist/op/runtimes/local.d.ts +29 -0
  243. package/dist/op/runtimes/local.d.ts.map +1 -0
  244. package/dist/op/step-output-ref.d.ts +15 -13
  245. package/dist/op/step-output-ref.d.ts.map +1 -1
  246. package/dist/op/types.d.ts +79 -27
  247. package/dist/op/types.d.ts.map +1 -1
  248. package/dist/ownership.d.ts +1 -1
  249. package/dist/provenance.d.ts +21 -0
  250. package/dist/provenance.d.ts.map +1 -1
  251. package/dist/serializer-walker.d.ts.map +1 -1
  252. package/dist/stack-output.d.ts +5 -0
  253. package/dist/stack-output.d.ts.map +1 -1
  254. package/package.json +6 -1
  255. package/src/audit/catalog.test.ts +79 -3
  256. package/src/audit/catalog.ts +111 -13
  257. package/src/audit/core.test.ts +26 -0
  258. package/src/audit/core.ts +61 -17
  259. package/src/audit/discover.test.ts +39 -0
  260. package/src/audit/discover.ts +91 -15
  261. package/src/audit/lineage.ts +100 -0
  262. package/src/audit/prior-art.ts +153 -0
  263. package/src/audit/proof.test.ts +67 -0
  264. package/src/audit/proof.ts +70 -0
  265. package/src/audit/report-html.ts +9 -4
  266. package/src/audit/report-model.ts +16 -2
  267. package/src/audit/report.test.ts +33 -0
  268. package/src/audit/report.ts +5 -3
  269. package/src/audit/rules-doc.test.ts +34 -2
  270. package/src/audit/rules-doc.ts +82 -9
  271. package/src/audit/secrets.ts +15 -3
  272. package/src/audit/terraform-state.test.ts +144 -0
  273. package/src/audit/terraform-state.ts +131 -0
  274. package/src/build.ts +31 -1
  275. package/src/claimed-fields.test.ts +145 -0
  276. package/src/claimed-fields.ts +208 -0
  277. package/src/cli/build-params-cli.ts +2 -2
  278. package/src/cli/command-group.ts +14 -0
  279. package/src/cli/commands/__fixtures__/audit-terraform/infra/.terraform/providers/lock.txt +1 -0
  280. package/src/cli/commands/__fixtures__/audit-terraform/infra/main.tf +1 -0
  281. package/src/cli/commands/__fixtures__/audit-terraform/infra/modules/foo/main.tf +1 -0
  282. package/src/cli/commands/__fixtures__/audit-terraform/infra/variables.tf +3 -0
  283. package/src/cli/commands/__fixtures__/init-lexicon-output/package.json +13 -4
  284. package/src/cli/commands/__fixtures__/init-lexicon-output/src/codegen/docs-cli.ts +4 -0
  285. package/src/cli/commands/__fixtures__/init-lexicon-output/src/codegen/package.ts +1 -0
  286. package/src/cli/commands/__fixtures__/init-lexicon-output/src/package-cli.ts +21 -0
  287. package/src/cli/commands/__snapshots__/init-lexicon.test.ts.snap +1 -0
  288. package/src/cli/commands/audit.test.ts +189 -11
  289. package/src/cli/commands/audit.ts +73 -27
  290. package/src/cli/commands/build.test.ts +292 -9
  291. package/src/cli/commands/build.ts +156 -24
  292. package/src/cli/commands/check-lexicon-docs.ts +1 -1
  293. package/src/cli/commands/check-lexicon-examples.test.ts +29 -1
  294. package/src/cli/commands/check-lexicon-examples.ts +42 -10
  295. package/src/cli/commands/import.ts +6 -0
  296. package/src/cli/commands/init-lexicon/templates/codegen.ts +9 -0
  297. package/src/cli/commands/init-lexicon/templates/project.ts +44 -4
  298. package/src/cli/commands/init-lexicon.test.ts +97 -2
  299. package/src/cli/commands/init-lexicon.ts +4 -2
  300. package/src/cli/commands/init.test.ts +35 -1
  301. package/src/cli/commands/init.ts +14 -3
  302. package/src/cli/commands/lint.test.ts +48 -0
  303. package/src/cli/commands/lint.ts +123 -0
  304. package/src/cli/commands/onboard.test.ts +75 -0
  305. package/src/cli/commands/onboard.ts +54 -23
  306. package/src/cli/handlers/build.test.ts +81 -1
  307. package/src/cli/handlers/build.ts +15 -1
  308. package/src/cli/handlers/graph.test.ts +3 -3
  309. package/src/cli/handlers/lifecycle.test.ts +195 -12
  310. package/src/cli/handlers/lifecycle.ts +123 -6
  311. package/src/cli/handlers/misc.ts +1 -1
  312. package/src/cli/handlers/operator.test.ts +183 -11
  313. package/src/cli/handlers/operator.ts +260 -55
  314. package/src/cli/handlers/run.test.ts +431 -1287
  315. package/src/cli/handlers/run.ts +365 -915
  316. package/src/cli/main.test.ts +23 -15
  317. package/src/cli/main.ts +88 -33
  318. package/src/cli/mcp/op-tools.ts +147 -88
  319. package/src/cli/mcp/resource-handlers.ts +10 -37
  320. package/src/cli/mcp/resources/context.ts +11 -12
  321. package/src/cli/mcp/server.test.ts +25 -27
  322. package/src/cli/mcp/server.ts +2 -2
  323. package/src/cli/registry.ts +26 -9
  324. package/src/codegen/docs.ts +1 -1
  325. package/src/codegen/generate-post-synth-barrel.test.ts +1 -1
  326. package/src/codegen/generate-post-synth-barrel.ts +1 -1
  327. package/src/components/README.md +3 -2
  328. package/src/components/auto-release.test.ts +4 -20
  329. package/src/components/auto-release.ts +17 -42
  330. package/src/components/capability.ts +4 -4
  331. package/src/components/cli-support.test.ts +10 -5
  332. package/src/components/cli-support.ts +52 -44
  333. package/src/components/component.schema.json +2 -2
  334. package/src/components/component.ts +3 -3
  335. package/src/components/driver-output.ts +14 -2
  336. package/src/components/driver.test.ts +92 -22
  337. package/src/components/driver.ts +193 -83
  338. package/src/components/index.ts +1 -6
  339. package/src/components/manifest-persistence.test.ts +2 -20
  340. package/src/components/manifest-persistence.ts +4 -24
  341. package/src/components/pilots/pilots-lint-clean.test.ts +11 -10
  342. package/src/components/run-progress.ts +6 -6
  343. package/src/components/verbs/run-agent.ts +11 -17
  344. package/src/components/verbs/verify.ts +2 -2
  345. package/src/deep-observation.test.ts +75 -0
  346. package/src/deep-observation.ts +48 -0
  347. package/src/discovery/collect.test.ts +3 -3
  348. package/src/discovery/files.ts +4 -4
  349. package/src/discovery/fold-import.ts +105 -2
  350. package/src/discovery/param-deps.test.ts +112 -1
  351. package/src/discovery/param-deps.ts +193 -0
  352. package/src/discovery/sandbox/bundle.ts +15 -1
  353. package/src/discovery/sandbox/config-wire.test.ts +2 -2
  354. package/src/discovery/sandbox/config-wire.ts +2 -2
  355. package/src/fold/fold.test.ts +56 -0
  356. package/src/fold/foldable-helpers.ts +81 -1
  357. package/src/fold-provenance-exclusion.test.ts +186 -0
  358. package/src/fold-provenance.test.ts +495 -0
  359. package/src/fold-provenance.ts +441 -0
  360. package/src/graph-ops.test.ts +2 -2
  361. package/src/graph-ops.ts +1 -1
  362. package/src/held-elsewhere.test.ts +102 -0
  363. package/src/held-elsewhere.ts +114 -0
  364. package/src/import/ir-utils.ts +11 -1
  365. package/src/import/parser.ts +35 -0
  366. package/src/index.ts +3 -0
  367. package/src/kubectl-context.ts +3 -3
  368. package/src/lexicon-config.test.ts +5 -5
  369. package/src/lexicon-config.ts +4 -4
  370. package/src/lexicon-plugin-helpers.ts +1 -1
  371. package/src/lexicon.ts +120 -14
  372. package/src/lifecycle/change-set.test.ts +49 -0
  373. package/src/lifecycle/change-set.ts +67 -0
  374. package/src/lifecycle/converge-ledger.ts +1 -1
  375. package/src/lifecycle/deep-diff.test.ts +267 -14
  376. package/src/lifecycle/deep-diff.ts +217 -14
  377. package/src/lifecycle/deep-observe.test.ts +3 -1
  378. package/src/lifecycle/deep-observe.ts +16 -6
  379. package/src/lifecycle/gate-ledger.test.ts +74 -2
  380. package/src/lifecycle/gate-ledger.ts +143 -36
  381. package/src/lifecycle/index.ts +1 -0
  382. package/src/lifecycle/release-ledger.ts +2 -2
  383. package/src/lifecycle/run-ledger.test.ts +268 -0
  384. package/src/lifecycle/run-ledger.ts +201 -0
  385. package/src/lifecycle/symptoms.ts +3 -3
  386. package/src/lint/config.test.ts +82 -1
  387. package/src/lint/config.ts +112 -3
  388. package/src/lint/policy.test.ts +1 -1
  389. package/src/lint/post-synth.test.ts +31 -0
  390. package/src/lint/post-synth.ts +54 -1
  391. package/src/lint/rules/__fixtures__/comp/comp004/pass/neo4j-cluster.component.ts +2 -2
  392. package/src/lint/rules/__fixtures__/comp/comp004/pass/orders-table.component.ts +1 -1
  393. package/src/lint/rules/comp/comp.test.ts +1 -1
  394. package/src/lint/rules/comp/comp004-gate-needs-durable-runtime.ts +74 -0
  395. package/src/lint/rules/comp/index.ts +3 -3
  396. package/src/lint/rules/op/index.ts +23 -0
  397. package/src/lint/rules/op/ops012-activity-contract.test.ts +161 -0
  398. package/src/lint/rules/op/ops012-activity-contract.ts +64 -0
  399. package/src/lint/rules/op/ops013-step-output-ref.test.ts +237 -0
  400. package/src/lint/rules/op/ops013-step-output-ref.ts +98 -0
  401. package/src/lint/rules/op/ops014-converge-rule-refusals.test.ts +263 -0
  402. package/src/lint/rules/op/ops014-converge-rule-refusals.ts +179 -0
  403. package/src/lint/rules/op/support.ts +11 -0
  404. package/src/lint/suppressions.test.ts +163 -0
  405. package/src/lint/suppressions.ts +204 -0
  406. package/src/live-endpoint.ts +1 -1
  407. package/src/op/activities/activity-contracts.ts +66 -0
  408. package/src/op/activities/apply.test.ts +567 -0
  409. package/src/op/activities/apply.ts +662 -0
  410. package/src/op/activities/build.ts +26 -0
  411. package/src/op/activities/converge.test.ts +330 -0
  412. package/src/op/activities/converge.ts +449 -0
  413. package/src/op/activities/env-teardown.test.ts +187 -0
  414. package/src/op/activities/env-teardown.ts +171 -0
  415. package/src/op/activities/guard-validate.test.ts +139 -0
  416. package/src/op/activities/guard-validate.ts +223 -0
  417. package/src/op/activities/http-check.test.ts +48 -0
  418. package/src/op/activities/http-check.ts +55 -0
  419. package/src/op/activities/index.ts +119 -0
  420. package/src/op/activities/lexicon-upgrade.test.ts +492 -0
  421. package/src/op/activities/lexicon-upgrade.ts +673 -0
  422. package/src/op/activities/lifecycle.ts +85 -0
  423. package/src/op/activities/pipeline-audit.test.ts +67 -0
  424. package/src/op/activities/pipeline-audit.ts +201 -0
  425. package/src/op/activities/policy.test.ts +19 -0
  426. package/src/op/activities/policy.ts +37 -0
  427. package/src/op/activities/reconcile.test.ts +81 -0
  428. package/src/op/activities/reconcile.ts +187 -0
  429. package/src/op/activities/shell.ts +26 -0
  430. package/src/op/activities/teardown.ts +22 -0
  431. package/src/op/activities/util.ts +6 -0
  432. package/src/op/activities/wait.ts +69 -0
  433. package/src/op/activities/workflow-audit.test.ts +90 -0
  434. package/src/op/activities/workflow-audit.ts +203 -0
  435. package/src/op/activity-contract-registry.test.ts +98 -0
  436. package/src/op/activity-contract-registry.ts +114 -0
  437. package/src/op/activity-contract.test.ts +1 -1
  438. package/src/op/activity-contract.ts +37 -33
  439. package/src/op/activity-failure.ts +34 -0
  440. package/src/op/activity-profiles.test.ts +56 -0
  441. package/src/op/activity-profiles.ts +116 -0
  442. package/src/op/activity-registry.test.ts +37 -28
  443. package/src/op/activity-registry.ts +27 -56
  444. package/src/op/activity-runtime.ts +3 -47
  445. package/src/op/builders-exports.test.ts +0 -49
  446. package/src/op/builders-typed.test.ts +164 -0
  447. package/src/op/builders.ts +123 -39
  448. package/src/op/composites/apply-op.ts +208 -0
  449. package/src/op/composites/composites.test.ts +366 -0
  450. package/src/op/composites/converge-op.test.ts +168 -0
  451. package/src/op/composites/converge-op.ts +214 -0
  452. package/src/op/composites/index.ts +26 -0
  453. package/src/op/composites/lexicon-upgrade-op.test.ts +68 -0
  454. package/src/op/composites/lexicon-upgrade-op.ts +134 -0
  455. package/src/op/composites/pipeline-audit-op.test.ts +25 -0
  456. package/src/op/composites/pipeline-audit-op.ts +78 -0
  457. package/src/op/composites/reconcile-op.test.ts +51 -0
  458. package/src/op/composites/reconcile-op.ts +107 -0
  459. package/src/op/composites/watch-op.ts +108 -0
  460. package/src/op/composites/workflow-audit-op.test.ts +25 -0
  461. package/src/op/composites/workflow-audit-op.ts +88 -0
  462. package/src/op/converge-rule.ts +12 -10
  463. package/src/op/cron.test.ts +128 -0
  464. package/src/op/cron.ts +149 -0
  465. package/src/op/discover-root.test.ts +105 -1
  466. package/src/op/discover.test.ts +6 -2
  467. package/src/op/discover.ts +84 -16
  468. package/src/op/duration.ts +21 -0
  469. package/src/op/effect-step.test.ts +26 -11
  470. package/src/op/emulator-lifecycle.ts +1 -2
  471. package/src/op/gate.ts +155 -0
  472. package/src/op/generate-pipeline.test.ts +65 -3
  473. package/src/op/generate-pipeline.ts +19 -2
  474. package/src/op/index.ts +39 -6
  475. package/src/op/local-executor.test.ts +163 -34
  476. package/src/op/local-executor.ts +370 -102
  477. package/src/op/local-output.test.ts +95 -11
  478. package/src/op/local-output.ts +33 -4
  479. package/src/op/op-ir.test.ts +281 -0
  480. package/src/op/op-ir.ts +417 -0
  481. package/src/op/op-verb-class.ts +19 -5
  482. package/src/op/op.test.ts +26 -5
  483. package/src/op/operator.test.ts +175 -6
  484. package/src/op/operator.ts +63 -16
  485. package/src/op/receipt-store.ts +1 -1
  486. package/src/op/resource.ts +20 -2
  487. package/src/op/runtime.ts +203 -0
  488. package/src/op/runtimes/local.test.ts +181 -0
  489. package/src/op/runtimes/local.ts +212 -0
  490. package/src/op/step-output-ref.ts +15 -13
  491. package/src/op/types.ts +84 -24
  492. package/src/ownership.ts +1 -1
  493. package/src/provenance.test.ts +6 -0
  494. package/src/provenance.ts +21 -1
  495. package/src/serializer-walker.test.ts +42 -0
  496. package/src/serializer-walker.ts +21 -2
  497. package/src/stack-output.ts +6 -1
  498. package/dist/cli/handlers/op-progress.d.ts +0 -90
  499. package/dist/cli/handlers/op-progress.d.ts.map +0 -1
  500. package/dist/cli/handlers/run-client.d.ts +0 -127
  501. package/dist/cli/handlers/run-client.d.ts.map +0 -1
  502. package/dist/cli/handlers/run-report.d.ts +0 -5
  503. package/dist/cli/handlers/run-report.d.ts.map +0 -1
  504. package/dist/components/temporal-codegen-loader.d.ts +0 -27
  505. package/dist/components/temporal-codegen-loader.d.ts.map +0 -1
  506. package/dist/lint/rules/comp/comp004-gate-needs-temporal.d.ts +0 -37
  507. package/dist/lint/rules/comp/comp004-gate-needs-temporal.d.ts.map +0 -1
  508. package/src/cli/handlers/op-progress.test.ts +0 -268
  509. package/src/cli/handlers/op-progress.ts +0 -248
  510. package/src/cli/handlers/run-client.test.ts +0 -82
  511. package/src/cli/handlers/run-client.ts +0 -219
  512. package/src/cli/handlers/run-report.test.ts +0 -62
  513. package/src/cli/handlers/run-report.ts +0 -122
  514. package/src/components/temporal-codegen-loader.ts +0 -41
  515. package/src/lint/rules/comp/comp004-gate-needs-temporal.ts +0 -66
@@ -1,26 +1,19 @@
1
1
  import { describe, test, expect, vi, beforeEach } from "vitest";
2
- import { createMockTemporalClient } from "@intentius/chant-test-utils";
3
2
  import type { ParsedArgs } from "../registry";
4
- import { EventEmitter } from "node:events";
5
3
 
6
4
  const discoverOpsMock = vi.fn();
7
5
  const loadChantConfigMock = vi.fn();
8
- const loadTemporalClientMock = vi.fn();
9
- const resolveProfileMock = vi.fn();
10
- const existsSyncMock = vi.fn();
11
6
  const writeFileSyncMock = vi.fn();
12
7
  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
8
  const runComponentsMock = vi.fn();
18
- const resolveComponentTargetsMock = vi.fn();
19
- const findComponentGateMock = vi.fn();
20
- const loadComponentTemporalCodegenMock = vi.fn();
9
+ const listComponentsMock = vi.fn();
21
10
  const maybeRecordAutoReleaseMock = vi.fn();
22
11
  const maybePersistBuildManifestMock = vi.fn();
23
- const listComponentsMock = vi.fn();
12
+ const loadPluginsMock = vi.fn();
13
+ const recordGateApprovalMock = vi.fn();
14
+
15
+ const { memoryGateLedgerPort } = await vi.importActual<typeof import("../../op/gate")>("../../op/gate");
16
+ let gateLedger = memoryGateLedgerPort();
24
17
 
25
18
  vi.mock("../../op/discover", () => ({ discoverOps: () => discoverOpsMock() }));
26
19
  vi.mock("../../config", async () => {
@@ -29,87 +22,67 @@ vi.mock("../../config", async () => {
29
22
  });
30
23
  vi.mock("../../components/auto-release", () => ({
31
24
  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
25
  }));
39
26
  vi.mock("../../components/manifest-persistence", () => ({
40
27
  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
28
  }));
57
29
  vi.mock("node:fs", async () => {
58
30
  const actual = await vi.importActual<typeof import("node:fs")>("node:fs");
59
31
  return {
60
32
  ...actual,
61
- existsSync: (p: string) => existsSyncMock(p),
62
33
  writeFileSync: (...args: unknown[]) => writeFileSyncMock(...args),
63
34
  mkdirSync: (...args: unknown[]) => mkdirSyncMock(...args),
64
35
  };
65
36
  });
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
37
  vi.mock("../../components/cli-support", () => ({
75
38
  runComponents: (...args: unknown[]) => runComponentsMock(...args),
76
- resolveComponentTargets: (...args: unknown[]) => resolveComponentTargetsMock(...args),
77
- findComponentGate: (...args: unknown[]) => findComponentGateMock(...args),
78
39
  listComponents: (...args: unknown[]) => listComponentsMock(...args),
79
40
  }));
80
- vi.mock("../../components/temporal-codegen-loader", () => ({
81
- loadComponentTemporalCodegen: () => loadComponentTemporalCodegenMock(),
41
+ vi.mock("../plugins", () => ({
42
+ loadPlugins: (...args: unknown[]) => loadPluginsMock(...args),
82
43
  }));
44
+ vi.mock("./operator", () => ({
45
+ recordGateApproval: (...args: unknown[]) => recordGateApprovalMock(...args),
46
+ }));
47
+ // The gate ledger a local run consults (#2119) is the `chant/lifecycle` orphan
48
+ // branch by default. Point it at memory here: these tests exercise the CLI's
49
+ // gate handling, and a unit test must never write a fact to the real repo.
50
+ vi.mock("../../op/gate", async () => {
51
+ const actual = await vi.importActual<typeof import("../../op/gate")>("../../op/gate");
52
+ return { ...actual, gitGateLedgerPort: () => gateLedger };
53
+ });
54
+ // The local runtime appends each run's record to that same branch (#2118).
55
+ // Real behavior in a project, and exactly what this suite must not do in
56
+ // chant's own checkout, since the append is against `process.cwd()`. Only the
57
+ // write and the read-back are stubbed — `buildRunRecord` stays real, so
58
+ // `--json` is still asserted against the record the executor actually built.
59
+ vi.mock("../../lifecycle/run-ledger", async (importOriginal) => {
60
+ const actual = await importOriginal<typeof import("../../lifecycle/run-ledger")>();
61
+ return {
62
+ ...actual,
63
+ appendRunRecord: async (input: Parameters<typeof actual.appendRunRecord>[0]) => ({
64
+ commit: "0".repeat(40),
65
+ record: { version: 1 as const, ...input, id: input.id ?? "test-run-id" },
66
+ }),
67
+ readRunLedger: async () => ({ records: [], malformed: 0 }),
68
+ };
69
+ });
83
70
 
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");
71
+ const { runOpList, runOpStatus, runOpLog, runOpSignalRenamed, runOpApprove, runOpCancel, runOp, runOpComponents } =
72
+ await import("./run");
88
73
 
89
74
  function makeArgs(overrides: Partial<ParsedArgs> = {}): ParsedArgs {
90
75
  return {
91
76
  command: "run", path: ".",
92
77
  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
78
  ...overrides,
97
79
  };
98
80
  }
99
81
 
100
- function makeOp(name: string, depends: string[] = []): [string, { config: { name: string; phases: unknown[]; taskQueue?: string; depends?: string[]; overview: string } }] {
82
+ function makeOp(name: string, depends: string[] = []): [string, { config: { name: string; phases: unknown[]; depends?: string[]; overview: string } }] {
101
83
  return [name, { config: { name, phases: [], depends, overview: `${name} overview` } }];
102
84
  }
103
85
 
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
86
  function makeStdoutSpy() {
114
87
  const buf: string[] = [];
115
88
  vi.spyOn(console, "log").mockImplementation((s: string) => { buf.push(s); });
@@ -122,495 +95,35 @@ function makeStderrSpy() {
122
95
  return buf;
123
96
  }
124
97
 
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" });
98
+ describe("runOpSignalRenamed", () => {
99
+ test("`run signal` points at `run approve` and never sends anything", async () => {
380
100
  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);
570
- const stderr = makeStderrSpy();
571
- const exit = await runOpSignal({
101
+ const exit = await runOpSignalRenamed({
572
102
  args: makeArgs({ extraPositional: "alb-deploy", extraPositional2: "gate-dns" }),
573
103
  plugins: [], serializers: [],
574
104
  });
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");
105
+ expect(exit).toBe(1);
106
+ const out = stderr.join("\n");
107
+ expect(out).toContain("`chant run signal` is now `chant run approve`");
108
+ expect(out).toContain("chant run approve alb-deploy gate-dns");
596
109
  });
597
110
  });
598
111
 
599
112
  describe("runOpCancel", () => {
600
113
  beforeEach(() => {
601
- loadTemporalClientMock.mockReset();
602
- loadChantConfigMock.mockReset();
603
- resolveProfileMock.mockReset();
114
+ discoverOpsMock.mockReset();
115
+ loadChantConfigMock.mockReset().mockResolvedValue({ config: {} });
116
+ loadPluginsMock.mockReset().mockResolvedValue([]);
604
117
  });
605
118
 
606
- test("missing op name → exit 1", async () => {
119
+ test("missing op name -> exit 1", async () => {
607
120
  const stderr = makeStderrSpy();
608
121
  const exit = await runOpCancel({ args: makeArgs({ extraPositional: undefined }), plugins: [], serializers: [] });
609
122
  expect(exit).toBe(1);
610
123
  expect(stderr.join("\n")).toContain("Op name is required");
611
124
  });
612
125
 
613
- test("requires --force → exit 1 without it", async () => {
126
+ test("requires --force -> exit 1 without it", async () => {
614
127
  const stderr = makeStderrSpy();
615
128
  const exit = await runOpCancel({
616
129
  args: makeArgs({ extraPositional: "alb-deploy", force: false }),
@@ -619,52 +132,15 @@ describe("runOpCancel", () => {
619
132
  expect(exit).toBe(1);
620
133
  expect(stderr.join("\n")).toContain("--force");
621
134
  });
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
135
  });
649
136
 
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
- }
137
+ // ── runOp (the main `chant run <name>` command) ───────────────────────
656
138
 
657
139
  describe("runOp", () => {
658
140
  beforeEach(() => {
659
141
  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();
142
+ loadChantConfigMock.mockReset().mockResolvedValue({ config: {} });
143
+ loadPluginsMock.mockReset().mockResolvedValue([]);
668
144
  });
669
145
 
670
146
  test("path defaults to '.' → exit 1 with hint", async () => {
@@ -693,220 +169,6 @@ describe("runOp", () => {
693
169
  expect(stderr.join("\n")).toContain("No *.op.ts files found");
694
170
  });
695
171
 
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
- });
705
-
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
- });
717
-
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);
732
-
733
- const exit = await runOp({ args: makeArgs({ path: "alb-deploy", report: true }), plugins: [], serializers: [] });
734
-
735
- expect(exit).toBe(0);
736
- expect(generateReportMock).toHaveBeenCalledTimes(1);
737
- expect(stdoutSpy).toHaveBeenCalledWith("# Report\nDeploy completed.");
738
- stdoutSpy.mockRestore();
739
- });
740
-
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
- }
783
- });
784
-
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
- }
876
- });
877
-
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);
902
-
903
- const exit = await promise;
904
- expect(exit).toBe(1);
905
- expect(proc.kill).toHaveBeenCalled();
906
- } finally {
907
- vi.useRealTimers();
908
- }
909
- });
910
172
  });
911
173
 
912
174
  // ── local mode dispatcher + guards ──────────────────────────────────────────
@@ -918,71 +180,88 @@ function localOp(name: string, steps: unknown[]) {
918
180
  describe("runOp dispatcher", () => {
919
181
  beforeEach(() => {
920
182
  discoverOpsMock.mockReset();
921
- loadTemporalClientMock.mockReset();
922
- loadChantConfigMock.mockReset();
923
- resolveProfileMock.mockReset();
924
- existsSyncMock.mockReset();
925
- spawnChildMock.mockReset();
183
+ loadChantConfigMock.mockReset().mockResolvedValue({ config: {} });
184
+ loadPluginsMock.mockReset().mockResolvedValue([]);
926
185
  });
927
186
 
928
- test("no flag → local executor (no Temporal client or worker spawned)", async () => {
187
+ test("no --on → the built-in local runtime runs the Op in this process", async () => {
929
188
  discoverOpsMock.mockResolvedValue({
930
189
  ops: new Map([localOp("hello", [{ kind: "activity", fn: "shellCmd", args: { cmd: "true" } }])]),
931
190
  errors: [],
932
191
  });
933
192
  const stderrWrite = vi.spyOn(process.stderr, "write").mockImplementation(() => true);
934
- const exit = await runOp({ args: makeArgs({ path: "hello", temporal: false }), plugins: [], serializers: [] });
193
+ const exit = await runOp({ args: makeArgs({ path: "hello" }), plugins: [], serializers: [] });
935
194
  expect(exit).toBe(0);
936
- expect(loadTemporalClientMock).not.toHaveBeenCalled();
937
- expect(spawnChildMock).not.toHaveBeenCalled();
195
+ expect(loadPluginsMock).not.toHaveBeenCalled();
938
196
  stderrWrite.mockRestore();
939
197
  });
940
198
 
941
- test("--temporal → Temporal path (missing worker.ts → exit 1)", async () => {
199
+ test("--report → exit 1 naming the removal, nothing run (#2116)", async () => {
942
200
  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
201
  const stderr = makeStderrSpy();
947
- const exit = await runOp({ args: makeArgs({ path: "hello", temporal: true }), plugins: [], serializers: [] });
202
+ const exit = await runOp({ args: makeArgs({ path: "hello", report: true }), plugins: [], serializers: [] });
948
203
  expect(exit).toBe(1);
949
- expect(stderr.join("\n")).toContain("worker.ts not found");
204
+ expect(stderr.join("\n")).toContain("#2116 removed");
205
+ expect(discoverOpsMock).not.toHaveBeenCalled();
950
206
  });
951
207
 
952
- test("gate in local mode → fast-fail before execution, suggests --temporal", async () => {
208
+ test("gate in local mode → exit 3 and the approve line (#2119)", async () => {
209
+ gateLedger = memoryGateLedgerPort();
953
210
  discoverOpsMock.mockResolvedValue({
954
211
  ops: new Map([localOp("gated", [{ kind: "gate", signalName: "approve-prod" }])]),
955
212
  errors: [],
956
213
  });
957
- const stderr = makeStderrSpy();
958
- const exit = await runOp({ args: makeArgs({ path: "gated", temporal: false }), plugins: [], serializers: [] });
959
- expect(exit).toBe(1);
960
- expect(stderr.join("\n")).toContain("--temporal");
961
- expect(loadTemporalClientMock).not.toHaveBeenCalled();
214
+ // `renderHuman` writes straight to process.stderr, not through console.error.
215
+ const stderrWrite = vi.spyOn(process.stderr, "write").mockImplementation(() => true);
216
+ const exit = await runOp({ args: makeArgs({ path: "gated" }), plugins: [], serializers: [] });
217
+ expect(exit).toBe(3);
218
+ const out = stderrWrite.mock.calls.map((c) => String(c[0])).join("");
219
+ stderrWrite.mockRestore();
220
+ expect(out).toContain('is gated on "approve-prod"');
221
+ expect(out).toContain("chant approve gated approve-prod");
222
+ expect(gateLedger.appended).toHaveLength(1);
962
223
  });
963
224
 
964
- test("--local and --temporal together → exit 1 before any work", async () => {
965
- const stderr = makeStderrSpy();
966
- const exit = await runOp({ args: makeArgs({ path: "hello", local: true, temporal: true }), plugins: [], serializers: [] });
967
- expect(exit).toBe(1);
968
- expect(stderr.join("\n")).toContain("mutually exclusive");
969
- expect(discoverOpsMock).not.toHaveBeenCalled();
970
- expect(loadTemporalClientMock).not.toHaveBeenCalled();
225
+ test("an approved gate lets the run through and exits 0", async () => {
226
+ gateLedger = memoryGateLedgerPort({
227
+ pending: [{
228
+ version: 1, kind: "pending", op: "gated", gate: "approve-prod",
229
+ timestamp: "2026-09-05T10:00:00.000Z", expiresAt: "2099-01-01T00:00:00.000Z",
230
+ }],
231
+ resolutions: [{
232
+ version: 1, op: "gated", gate: "approve-prod",
233
+ resolvedBy: "alex", timestamp: "2026-09-05T11:00:00.000Z",
234
+ }],
235
+ });
236
+ discoverOpsMock.mockResolvedValue({
237
+ ops: new Map([localOp("gated", [
238
+ { kind: "gate", signalName: "approve-prod" },
239
+ { kind: "activity", fn: "shellCmd", args: { cmd: "true" } },
240
+ ])]),
241
+ errors: [],
242
+ });
243
+ const stderrWrite = vi.spyOn(process.stderr, "write").mockImplementation(() => true);
244
+ const exit = await runOp({ args: makeArgs({ path: "gated" }), plugins: [], serializers: [] });
245
+ expect(exit).toBe(0);
246
+ expect(stderrWrite.mock.calls.map((c) => String(c[0])).join("")).toContain("[approved] alex");
247
+ stderrWrite.mockRestore();
971
248
  });
972
249
 
973
- test("--json → structured result on stdout", async () => {
250
+ test("--json → the run's ledger record on stdout (#2118)", async () => {
974
251
  discoverOpsMock.mockResolvedValue({
975
252
  ops: new Map([localOp("hello", [{ kind: "activity", fn: "shellCmd", args: { cmd: "true" } }])]),
976
253
  errors: [],
977
254
  });
978
255
  const stdoutWrite = vi.spyOn(process.stdout, "write").mockImplementation(() => true);
979
256
  vi.spyOn(process.stderr, "write").mockImplementation(() => true);
980
- const exit = await runOp({ args: makeArgs({ path: "hello", temporal: false, json: true }), plugins: [], serializers: [] });
257
+ const exit = await runOp({ args: makeArgs({ path: "hello", json: true }), plugins: [], serializers: [] });
981
258
  expect(exit).toBe(0);
982
259
  const printed = stdoutWrite.mock.calls.map((c) => String(c[0])).join("");
983
260
  const parsed = JSON.parse(printed.trim());
984
261
  expect(parsed.op).toBe("hello");
985
- expect(parsed.ok).toBe(true);
262
+ expect(parsed.version).toBe(1);
263
+ expect(parsed.status).toBe("ok");
264
+ expect(parsed.phases[0].steps[0]).toMatchObject({ fn: "shellCmd", status: "ok" });
986
265
  vi.restoreAllMocks();
987
266
  });
988
267
  });
@@ -1001,18 +280,15 @@ describe("runOp: --sandbox with a policyGate step (chant #2003)", () => {
1001
280
 
1002
281
  beforeEach(() => {
1003
282
  discoverOpsMock.mockReset();
1004
- loadTemporalClientMock.mockReset();
1005
- loadChantConfigMock.mockReset();
1006
- resolveProfileMock.mockReset();
1007
- existsSyncMock.mockReset();
1008
- spawnChildMock.mockReset();
283
+ loadChantConfigMock.mockReset().mockResolvedValue({ config: { lexicons: ["stub"] } });
284
+ loadPluginsMock.mockReset().mockResolvedValue([]);
1009
285
  });
1010
286
 
1011
- test("local mode → refuses before any phase runs, naming the combination", async () => {
287
+ test("refuses before any phase runs, naming the combination", async () => {
1012
288
  discoverOpsMock.mockResolvedValue({ ops: new Map([policyGateOp("gate")]), errors: [] });
1013
289
  const stderr = makeStderrSpy();
1014
290
 
1015
- const exit = await runOp({ args: makeArgs({ path: "gate", temporal: false, sandbox: true }), plugins: [], serializers: [] });
291
+ const exit = await runOp({ args: makeArgs({ path: "gate", sandbox: true }), plugins: [], serializers: [] });
1016
292
 
1017
293
  expect(exit).toBe(1);
1018
294
  const out = stderr.join("\n");
@@ -1022,19 +298,6 @@ describe("runOp: --sandbox with a policyGate step (chant #2003)", () => {
1022
298
  expect(out).not.toContain("This is a chant bug");
1023
299
  });
1024
300
 
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
301
  test("a policyGate nested in an effect step is found too", async () => {
1039
302
  discoverOpsMock.mockResolvedValue({
1040
303
  ops: new Map([
@@ -1046,7 +309,7 @@ describe("runOp: --sandbox with a policyGate step (chant #2003)", () => {
1046
309
  });
1047
310
  const stderr = makeStderrSpy();
1048
311
 
1049
- const exit = await runOp({ args: makeArgs({ path: "gate", temporal: false, sandbox: true }), plugins: [], serializers: [] });
312
+ const exit = await runOp({ args: makeArgs({ path: "gate", sandbox: true }), plugins: [], serializers: [] });
1050
313
 
1051
314
  expect(exit).toBe(1);
1052
315
  expect(stderr.join("\n")).toContain("policyGate");
@@ -1054,19 +317,68 @@ describe("runOp: --sandbox with a policyGate step (chant #2003)", () => {
1054
317
 
1055
318
  test("without --sandbox the same Op is not refused", async () => {
1056
319
  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);
320
+ const start = vi.fn(async () => ({
321
+ op: "gate",
322
+ runId: "stub-1",
323
+ result: async () => ({ op: "gate", runId: "stub-1", state: "completed", startedAt: "2026-01-01T00:00:00.000Z" }),
324
+ }));
325
+ makeStdoutSpy();
1060
326
  const stderr = makeStderrSpy();
1061
327
 
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: [] });
328
+ // On a stub runtime, so reaching `start` proves the pre-flight let it
329
+ // through without the policyGate itself having to run here.
330
+ const exit = await runOp({
331
+ args: makeArgs({ path: "gate", on: "stub" }),
332
+ plugins: [{ name: "stub", opRuntime: { name: "stub", start } } as never], serializers: [],
333
+ });
1066
334
 
1067
- expect(exit).toBe(1);
1068
- expect(stderr.join("\n")).toContain("worker.ts not found");
1069
- expect(loadChantConfigMock).toHaveBeenCalled();
335
+ expect(exit).toBe(0);
336
+ expect(start).toHaveBeenCalledTimes(1);
337
+ expect(stderr.join("\n")).not.toContain("policyGate");
338
+ });
339
+
340
+ // #2192 — `--profile` is core's flag and the runtime's meaning. The handler
341
+ // only has to hand it over; fountain's own tests cover what it does with it.
342
+ test("--profile reaches the hosting runtime's start options", async () => {
343
+ discoverOpsMock.mockResolvedValue({ ops: new Map([makeOp("prod-apply")]), errors: [] });
344
+ const start = vi.fn(async () => ({
345
+ op: "prod-apply",
346
+ runId: "stub-1",
347
+ result: async () => ({ op: "prod-apply", runId: "stub-1", state: "completed", startedAt: "2026-01-01T00:00:00.000Z" }),
348
+ }));
349
+ makeStdoutSpy();
350
+ makeStderrSpy();
351
+
352
+ const exit = await runOp({
353
+ args: makeArgs({ path: "prod-apply", on: "stub", profile: "staging" }),
354
+ plugins: [{ name: "stub", opRuntime: { name: "stub", start } } as never], serializers: [],
355
+ });
356
+
357
+ expect(exit).toBe(0);
358
+ expect(start).toHaveBeenCalledWith(expect.anything(), expect.objectContaining({ profile: "staging" }));
359
+ });
360
+
361
+ test("without --profile the runtime is left to its own default", async () => {
362
+ discoverOpsMock.mockResolvedValue({ ops: new Map([makeOp("prod-apply")]), errors: [] });
363
+ let seen: Record<string, unknown> | undefined;
364
+ const start = vi.fn(async (_op: unknown, opts: Record<string, unknown>) => {
365
+ seen = opts;
366
+ return {
367
+ op: "prod-apply",
368
+ runId: "stub-1",
369
+ result: async () => ({ op: "prod-apply", runId: "stub-1", state: "completed", startedAt: "2026-01-01T00:00:00.000Z" }),
370
+ };
371
+ });
372
+ makeStdoutSpy();
373
+ makeStderrSpy();
374
+
375
+ await runOp({
376
+ args: makeArgs({ path: "prod-apply", on: "stub" }),
377
+ plugins: [{ name: "stub", opRuntime: { name: "stub", start } } as never], serializers: [],
378
+ });
379
+
380
+ expect(seen).toBeDefined();
381
+ expect(seen).not.toHaveProperty("profile");
1070
382
  });
1071
383
 
1072
384
  test("--sandbox on an Op with no policyGate step is untouched", async () => {
@@ -1076,27 +388,220 @@ describe("runOp: --sandbox with a policyGate step (chant #2003)", () => {
1076
388
  });
1077
389
  const stderrWrite = vi.spyOn(process.stderr, "write").mockImplementation(() => true);
1078
390
 
1079
- const exit = await runOp({ args: makeArgs({ path: "hello", temporal: false, sandbox: true }), plugins: [], serializers: [] });
391
+ const exit = await runOp({ args: makeArgs({ path: "hello", sandbox: true }), plugins: [], serializers: [] });
1080
392
 
1081
393
  expect(exit).toBe(0);
1082
394
  stderrWrite.mockRestore();
1083
395
  });
1084
396
  });
1085
397
 
1086
- describe("Temporal-only subcommand guards", () => {
398
+ /**
399
+ * #2116 — `run list/status/log/cancel --components` reported a component's
400
+ * durable run state. Nothing keeps one any more, so each refuses with a line
401
+ * that says so and points at the command that still answers. The Op-path
402
+ * counterparts run on the resolved runtime, covered below.
403
+ */
404
+ describe("run <sub> --components refuses (#2116)", () => {
1087
405
  const cases: Array<[string, (ctx: { args: ParsedArgs; plugins: never[]; serializers: never[] }) => Promise<number>]> = [
1088
406
  ["list", runOpList],
1089
407
  ["status", runOpStatus],
1090
408
  ["log", runOpLog],
1091
- ["signal", runOpSignal],
1092
409
  ["cancel", runOpCancel],
1093
410
  ];
1094
411
 
1095
- test.each(cases)("run %s without --temporal → exit 1 + actionable message", async (_name, handler) => {
412
+ test.each(cases)("run %s --components -> exit 1 naming the removal", async (_name, handler) => {
413
+ const stderr = makeStderrSpy();
414
+ const exit = await handler({
415
+ args: makeArgs({ components: true, force: true, extraPositional: "x", extraPositional2: "y" }),
416
+ plugins: [], serializers: [],
417
+ });
418
+ expect(exit).toBe(1);
419
+ expect(stderr.join("\n")).toContain("durable run state, which #2116 removed");
420
+ });
421
+
422
+ test("run list --components points at `chant list --components`", async () => {
423
+ const stderr = makeStderrSpy();
424
+ const exit = await runOpList({ args: makeArgs({ components: true }), plugins: [], serializers: [] });
425
+ expect(exit).toBe(1);
426
+ expect(stderr.join("\n")).toContain("chant list --components");
427
+ expect(listComponentsMock).not.toHaveBeenCalled();
428
+ });
429
+ });
430
+
431
+ // ── the runtime seam (#2121) ────────────────────────────────────────────────
432
+
433
+ describe("run subcommands on the resolved runtime", () => {
434
+ function makeStubRuntime(overrides: Record<string, unknown> = {}) {
435
+ return {
436
+ name: "stub",
437
+ start: vi.fn(async (op: { name: string }) => ({
438
+ op: op.name,
439
+ runId: "stub-1",
440
+ result: async () => ({
441
+ op: op.name, runId: "stub-1", state: "completed", startedAt: "2026-01-01T00:00:00.000Z",
442
+ endedAt: "2026-01-01T00:00:05.000Z",
443
+ }),
444
+ })),
445
+ status: vi.fn(async (op: string) => ({
446
+ op, runId: "stub-1", state: "running", startedAt: "2026-01-01T00:00:00.000Z",
447
+ })),
448
+ // A provider's `log` returns run-ledger records (#2118), newest first.
449
+ log: vi.fn(async (op: string) => [
450
+ {
451
+ version: 1, id: "stub-1", op, env: "staging", status: "ok",
452
+ started: "2026-01-01T00:00:00.000Z", ended: "2026-01-01T00:00:05.000Z",
453
+ labels: {}, outcomes: {}, phases: [],
454
+ },
455
+ ]),
456
+ list: vi.fn(async (ops: Array<{ name: string }>) =>
457
+ new Map(ops.map((o) => [o.name, { op: o.name, runId: "stub-1", state: "completed", startedAt: "2026-01-01T00:00:00.000Z" }]))),
458
+ cancel: vi.fn(async () => undefined),
459
+ ...overrides,
460
+ };
461
+ }
462
+
463
+ function stubPlugin(runtime: unknown) {
464
+ return { name: "stub", opRuntime: runtime } as never;
465
+ }
466
+
467
+ beforeEach(() => {
468
+ discoverOpsMock.mockReset();
469
+ loadChantConfigMock.mockReset().mockResolvedValue({ config: { lexicons: ["stub"] } });
470
+ loadPluginsMock.mockReset().mockResolvedValue([]);
471
+ });
472
+
473
+ test("--on stub reaches the stub lexicon's start", async () => {
474
+ const runtime = makeStubRuntime();
475
+ discoverOpsMock.mockResolvedValue({ ops: new Map([makeOp("hello")]), errors: [] });
476
+ makeStdoutSpy();
477
+ const exit = await runOp({
478
+ args: makeArgs({ path: "hello", on: "stub" }),
479
+ plugins: [stubPlugin(runtime)], serializers: [],
480
+ });
481
+ expect(exit).toBe(0);
482
+ expect(runtime.start).toHaveBeenCalledTimes(1);
483
+ expect((runtime.start.mock.calls[0][0] as { name: string }).name).toBe("hello");
484
+ });
485
+
486
+ test("run status/log/list/cancel --on stub reach their methods", async () => {
487
+ const runtime = makeStubRuntime();
488
+ discoverOpsMock.mockResolvedValue({ ops: new Map([makeOp("hello")]), errors: [] });
489
+ makeStdoutSpy();
490
+ makeStderrSpy();
491
+ const ctx = (over: Partial<ParsedArgs>) => ({
492
+ args: makeArgs({ on: "stub", ...over }),
493
+ plugins: [stubPlugin(runtime)], serializers: [],
494
+ });
495
+
496
+ expect(await runOpStatus(ctx({ extraPositional: "hello" }))).toBe(0);
497
+ expect(await runOpLog(ctx({ extraPositional: "hello" }))).toBe(0);
498
+ expect(await runOpList(ctx({}))).toBe(0);
499
+ expect(await runOpCancel(ctx({ extraPositional: "hello", force: true }))).toBe(0);
500
+
501
+ expect(runtime.status).toHaveBeenCalledWith("hello");
502
+ expect(runtime.log).toHaveBeenCalledWith("hello", undefined);
503
+ expect(runtime.list).toHaveBeenCalledTimes(1);
504
+ expect(runtime.cancel).toHaveBeenCalledWith("hello", { force: true });
505
+ });
506
+
507
+ test("--on nope → exit 1 naming the configured lexicons", async () => {
508
+ discoverOpsMock.mockResolvedValue({ ops: new Map([makeOp("hello")]), errors: [] });
509
+ loadChantConfigMock.mockResolvedValue({ config: { lexicons: ["aws", "k8s"] } });
510
+ const stderr = makeStderrSpy();
511
+ const exit = await runOp({
512
+ args: makeArgs({ path: "hello", on: "nope" }),
513
+ plugins: [], serializers: [],
514
+ });
515
+ expect(exit).toBe(1);
516
+ expect(stderr.join("\n")).toContain("is not a configured lexicon");
517
+ expect(stderr.join("\n")).toContain("aws, k8s");
518
+ });
519
+
520
+ test("--on a lexicon with no opRuntime → exit 1 naming the lexicon", async () => {
521
+ discoverOpsMock.mockResolvedValue({ ops: new Map([makeOp("hello")]), errors: [] });
522
+ const stderr = makeStderrSpy();
523
+ const exit = await runOp({
524
+ args: makeArgs({ path: "hello", on: "plain" }),
525
+ plugins: [{ name: "plain" } as never], serializers: [],
526
+ });
527
+ expect(exit).toBe(1);
528
+ expect(stderr.join("\n")).toContain('lexicon "plain" does not host Op runs');
529
+ });
530
+
531
+ test("--components on a runtime that cannot host them → one line, exit 1", async () => {
532
+ const runtime = makeStubRuntime();
533
+ const stderr = makeStderrSpy();
534
+ const exit = await runOpComponents({
535
+ args: makeArgs({ path: "search-service", components: true, on: "stub" }),
536
+ plugins: [stubPlugin(runtime)], serializers: [],
537
+ });
538
+ expect(exit).toBe(1);
539
+ expect(stderr.join("\n")).toContain('--components is not supported on the "stub" runtime');
540
+ expect(runComponentsMock).not.toHaveBeenCalled();
541
+ });
542
+ });
543
+
544
+ describe("runOpApprove", () => {
545
+ beforeEach(() => {
546
+ recordGateApprovalMock.mockReset();
547
+ loadChantConfigMock.mockReset().mockResolvedValue({ config: { lexicons: ["stub"] } });
548
+ loadPluginsMock.mockReset().mockResolvedValue([]);
549
+ });
550
+
551
+ test("missing op or gate name → exit 1, nothing recorded", async () => {
552
+ const stderr = makeStderrSpy();
553
+ const exit = await runOpApprove({
554
+ args: makeArgs({ extraPositional: "alb-deploy" }),
555
+ plugins: [], serializers: [],
556
+ });
557
+ expect(exit).toBe(1);
558
+ expect(stderr.join("\n")).toContain("chant run approve <op> <gate>");
559
+ expect(recordGateApprovalMock).not.toHaveBeenCalled();
560
+ });
561
+
562
+ test("writes the resolution, then calls the provider's resolveGate", async () => {
563
+ const record = { version: 1, op: "alb-deploy", gate: "release", resolvedBy: "alex", timestamp: "2026-01-01T00:00:00.000Z" };
564
+ recordGateApprovalMock.mockResolvedValue({ ok: true, record });
565
+ const resolveGate = vi.fn(async () => undefined);
566
+ const stderr = makeStderrSpy();
567
+
568
+ const exit = await runOpApprove({
569
+ args: makeArgs({ on: "stub", extraPositional: "alb-deploy", extraPositional2: "release", approver: "alex" }),
570
+ plugins: [{ name: "stub", opRuntime: { name: "stub", resolveGate } } as never], serializers: [],
571
+ });
572
+
573
+ expect(exit).toBe(0);
574
+ expect(recordGateApprovalMock).toHaveBeenCalledWith("alb-deploy", "release", {
575
+ actor: "alex", note: undefined, url: undefined,
576
+ });
577
+ expect(resolveGate).toHaveBeenCalledWith("alb-deploy", "release", record);
578
+ expect(stderr.join("\n")).toContain('Runtime "stub" was notified');
579
+ });
580
+
581
+ test("a provider with no resolveGate still records the fact and exits 0", async () => {
582
+ const record = { version: 1, op: "alb-deploy", gate: "release", resolvedBy: "ci", timestamp: "2026-01-01T00:00:00.000Z" };
583
+ recordGateApprovalMock.mockResolvedValue({ ok: true, record });
1096
584
  const stderr = makeStderrSpy();
1097
- const exit = await handler({ args: makeArgs({ temporal: false, extraPositional: "x", extraPositional2: "y" }), plugins: [], serializers: [] });
585
+
586
+ const exit = await runOpApprove({
587
+ args: makeArgs({ extraPositional: "alb-deploy", extraPositional2: "release" }),
588
+ plugins: [], serializers: [],
589
+ });
590
+
591
+ expect(exit).toBe(0);
592
+ expect(recordGateApprovalMock).toHaveBeenCalledTimes(1);
593
+ expect(stderr.join("\n")).toContain("when the op next runs");
594
+ });
595
+
596
+ test("a refused ledger write → exit 1 without waking the runtime", async () => {
597
+ recordGateApprovalMock.mockResolvedValue({ ok: false });
598
+ const resolveGate = vi.fn(async () => undefined);
599
+ const exit = await runOpApprove({
600
+ args: makeArgs({ on: "stub", extraPositional: "alb-deploy", extraPositional2: "release" }),
601
+ plugins: [{ name: "stub", opRuntime: { name: "stub", resolveGate } } as never], serializers: [],
602
+ });
1098
603
  expect(exit).toBe(1);
1099
- expect(stderr.join("\n")).toContain("not available in local mode");
604
+ expect(resolveGate).not.toHaveBeenCalled();
1100
605
  });
1101
606
  });
1102
607
 
@@ -1112,10 +617,10 @@ describe("runOp dispatcher: --components routes to runOpComponents", () => {
1112
617
 
1113
618
  test("runOp with --components dispatches to runComponents, not Op discovery", async () => {
1114
619
  discoverOpsMock.mockReset();
1115
- runComponentsMock.mockResolvedValue({ success: true, selected: ["svc"], run: { order: ["svc"], waves: [["svc"]], results: [{ component: "svc", ok: true, records: [] }], ok: true } });
620
+ runComponentsMock.mockResolvedValue({ success: true, selected: ["svc"], run: { order: ["svc"], waves: [["svc"]], results: [{ component: "svc", ok: true, status: "ok", records: [] }], ok: true, status: "ok" } });
1116
621
  vi.spyOn(process.stderr, "write").mockImplementation(() => true);
1117
622
 
1118
- const exit = await runOp({ args: makeArgs({ path: "svc", components: true, temporal: false }), plugins: [], serializers: [] });
623
+ const exit = await runOp({ args: makeArgs({ path: "svc", components: true }), plugins: [], serializers: [] });
1119
624
 
1120
625
  expect(exit).toBe(0);
1121
626
  expect(runComponentsMock).toHaveBeenCalledWith(expect.any(String), "svc", { env: undefined, componentOutputs: {}, buildParams: [] });
@@ -1123,15 +628,14 @@ describe("runOp dispatcher: --components routes to runOpComponents", () => {
1123
628
  vi.restoreAllMocks();
1124
629
  });
1125
630
 
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.
631
+ // chant #1116 — the component driver never checked --report, so it was
632
+ // silently ignored and the command fell through to a real dispatch.
633
+ // Hard-error instead, before runComponents is ever reached.
1130
634
  test("--report combined with --components → exit 1 before any dispatch, no fall-through (#1116)", async () => {
1131
635
  discoverOpsMock.mockReset();
1132
636
  const stderr = makeStderrSpy();
1133
637
 
1134
- const exit = await runOp({ args: makeArgs({ path: "svc", components: true, report: true, temporal: false }), plugins: [], serializers: [] });
638
+ const exit = await runOp({ args: makeArgs({ path: "svc", components: true, report: true }), plugins: [], serializers: [] });
1135
639
 
1136
640
  expect(exit).toBe(1);
1137
641
  expect(stderr.join("\n")).toContain("not supported with --components");
@@ -1144,10 +648,10 @@ describe("runOp dispatcher: --components routes to runOpComponents", () => {
1144
648
  // real dispatch through runComponents — mocked here, never a real cloud call.
1145
649
  test("plain --components (no --report) still dispatches to runComponents (#1116 regression guard)", async () => {
1146
650
  discoverOpsMock.mockReset();
1147
- runComponentsMock.mockResolvedValue({ success: true, selected: ["svc"], run: { order: ["svc"], waves: [["svc"]], results: [{ component: "svc", ok: true, records: [] }], ok: true } });
651
+ runComponentsMock.mockResolvedValue({ success: true, selected: ["svc"], run: { order: ["svc"], waves: [["svc"]], results: [{ component: "svc", ok: true, status: "ok", records: [] }], ok: true, status: "ok" } });
1148
652
  vi.spyOn(process.stderr, "write").mockImplementation(() => true);
1149
653
 
1150
- const exit = await runOp({ args: makeArgs({ path: "svc", components: true, report: false, temporal: false }), plugins: [], serializers: [] });
654
+ const exit = await runOp({ args: makeArgs({ path: "svc", components: true, report: false }), plugins: [], serializers: [] });
1151
655
 
1152
656
  expect(exit).toBe(0);
1153
657
  expect(runComponentsMock).toHaveBeenCalled();
@@ -1165,7 +669,7 @@ describe("runOpComponents", () => {
1165
669
 
1166
670
  test("no component name → exit 1 with hint", async () => {
1167
671
  const stderr = makeStderrSpy();
1168
- const exit = await runOpComponents({ args: makeArgs({ path: ".", temporal: false }), plugins: [], serializers: [] });
672
+ const exit = await runOpComponents({ args: makeArgs({ path: "." }), plugins: [], serializers: [] });
1169
673
  expect(exit).toBe(1);
1170
674
  expect(stderr.join("\n")).toContain("Component name is required");
1171
675
  expect(runComponentsMock).not.toHaveBeenCalled();
@@ -1181,11 +685,11 @@ describe("runOpComponents", () => {
1181
685
  runComponentsMock.mockResolvedValue({
1182
686
  success: true,
1183
687
  selected: ["svc"],
1184
- run: { order: ["svc"], waves: [["svc"]], results: [{ component: "svc", ok: true, records: [] }], ok: true },
688
+ run: { order: ["svc"], waves: [["svc"]], results: [{ component: "svc", ok: true, status: "ok", records: [] }], ok: true, status: "ok" },
1185
689
  });
1186
690
  const stderr = makeStderrSpy();
1187
691
 
1188
- const exit = await runOpComponents({ args: makeArgs({ path: "svc", temporal: false }), plugins: [], serializers: [] });
692
+ const exit = await runOpComponents({ args: makeArgs({ path: "svc" }), plugins: [], serializers: [] });
1189
693
 
1190
694
  expect(exit).toBe(0);
1191
695
  expect(runComponentsMock).toHaveBeenCalledWith(expect.any(String), "svc", expect.objectContaining({
@@ -1203,13 +707,13 @@ describe("runOpComponents", () => {
1203
707
  runComponentsMock.mockResolvedValue({
1204
708
  success: true,
1205
709
  selected: ["svc"],
1206
- run: { order: ["svc"], waves: [["svc"]], results: [{ component: "svc", ok: true, records: [] }], ok: true },
710
+ run: { order: ["svc"], waves: [["svc"]], results: [{ component: "svc", ok: true, status: "ok", records: [] }], ok: true, status: "ok" },
1207
711
  });
1208
712
  vi.spyOn(process.stderr, "write").mockImplementation(() => true);
1209
713
  const stderr = makeStderrSpy();
1210
714
 
1211
715
  const exit = await runOpComponents({
1212
- args: makeArgs({ path: "svc", temporal: false, param: ["tier=production"] }),
716
+ args: makeArgs({ path: "svc", param: ["tier=production"] }),
1213
717
  plugins: [],
1214
718
  serializers: [],
1215
719
  });
@@ -1228,7 +732,7 @@ describe("runOpComponents", () => {
1228
732
  });
1229
733
  const stderr = makeStderrSpy();
1230
734
 
1231
- const exit = await runOpComponents({ args: makeArgs({ path: "svc", temporal: false }), plugins: [], serializers: [] });
735
+ const exit = await runOpComponents({ args: makeArgs({ path: "svc" }), plugins: [], serializers: [] });
1232
736
 
1233
737
  expect(exit).toBe(1);
1234
738
  expect(stderr.join("\n")).toMatch(/"tier"/);
@@ -1242,7 +746,7 @@ describe("runOpComponents", () => {
1242
746
  const stderr = makeStderrSpy();
1243
747
 
1244
748
  const exit = await runOpComponents({
1245
- args: makeArgs({ path: "svc", temporal: false, param: ["tier=bogus"] }),
749
+ args: makeArgs({ path: "svc", param: ["tier=bogus"] }),
1246
750
  plugins: [],
1247
751
  serializers: [],
1248
752
  });
@@ -1261,13 +765,14 @@ describe("runOpComponents", () => {
1261
765
  run: {
1262
766
  order: ["svc"],
1263
767
  waves: [["svc"]],
1264
- results: [{ component: "svc", ok: true, records: [{ component: "svc", phase: "Apply", kind: "cfn-deploy", status: "ok", durationMs: 5 }] }],
768
+ results: [{ component: "svc", ok: true, status: "ok", records: [{ component: "svc", phase: "Apply", kind: "cfn-deploy", status: "ok", durationMs: 5 }] }],
1265
769
  ok: true,
770
+ status: "ok",
1266
771
  },
1267
772
  });
1268
773
  const stderrWrite = vi.spyOn(process.stderr, "write").mockImplementation(() => true);
1269
774
 
1270
- const exit = await runOpComponents({ args: makeArgs({ path: "svc", temporal: false }), plugins: [], serializers: [] });
775
+ const exit = await runOpComponents({ args: makeArgs({ path: "svc" }), plugins: [], serializers: [] });
1271
776
 
1272
777
  expect(exit).toBe(0);
1273
778
  expect(runComponentsMock).toHaveBeenCalledWith(expect.any(String), "svc", { env: undefined, componentOutputs: {}, buildParams: [] });
@@ -1277,21 +782,21 @@ describe("runOpComponents", () => {
1277
782
  });
1278
783
 
1279
784
  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 } });
785
+ runComponentsMock.mockResolvedValue({ success: true, selected: ["svc"], run: { order: ["svc"], waves: [["svc"]], results: [{ component: "svc", ok: true, status: "ok", records: [] }], ok: true, status: "ok" } });
1281
786
  vi.spyOn(process.stderr, "write").mockImplementation(() => true);
1282
787
 
1283
- await runOpComponents({ args: makeArgs({ path: "svc", env: "staging", temporal: false }), plugins: [], serializers: [] });
788
+ await runOpComponents({ args: makeArgs({ path: "svc", env: "staging" }), plugins: [], serializers: [] });
1284
789
 
1285
790
  expect(runComponentsMock).toHaveBeenCalledWith(expect.any(String), "svc", { env: "staging", componentOutputs: {}, buildParams: [] });
1286
791
  vi.restoreAllMocks();
1287
792
  });
1288
793
 
1289
794
  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 };
795
+ const run = { order: ["svc"], waves: [["svc"]], results: [{ component: "svc", ok: true, status: "ok", records: [] }], ok: true, status: "ok" };
1291
796
  runComponentsMock.mockResolvedValue({ success: true, selected: ["svc"], run });
1292
797
  const stdoutWrite = vi.spyOn(process.stdout, "write").mockImplementation(() => true);
1293
798
 
1294
- const exit = await runOpComponents({ args: makeArgs({ path: "svc", json: true, temporal: false }), plugins: [], serializers: [] });
799
+ const exit = await runOpComponents({ args: makeArgs({ path: "svc", json: true }), plugins: [], serializers: [] });
1295
800
 
1296
801
  expect(exit).toBe(0);
1297
802
  const printed = stdoutWrite.mock.calls.map((c) => String(c[0])).join("");
@@ -1309,7 +814,7 @@ describe("runOpComponents", () => {
1309
814
  return {
1310
815
  success: true,
1311
816
  selected: ["svc"],
1312
- 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" },
1313
818
  };
1314
819
  },
1315
820
  );
@@ -1317,7 +822,7 @@ describe("runOpComponents", () => {
1317
822
  vi.spyOn(process.stderr, "write").mockImplementation(() => true);
1318
823
 
1319
824
  const exit = await runOpComponents({
1320
- args: makeArgs({ path: "svc", progressJson: true, temporal: false }),
825
+ args: makeArgs({ path: "svc", progressJson: true }),
1321
826
  plugins: [],
1322
827
  serializers: [],
1323
828
  });
@@ -1337,12 +842,12 @@ describe("runOpComponents", () => {
1337
842
  runComponentsMock.mockResolvedValue({
1338
843
  success: true,
1339
844
  selected: ["svc"],
1340
- run: { order: ["svc"], waves: [["svc"]], results: [{ component: "svc", ok: true, records: [] }], ok: true },
845
+ run: { order: ["svc"], waves: [["svc"]], results: [{ component: "svc", ok: true, status: "ok", records: [] }], ok: true, status: "ok" },
1341
846
  });
1342
847
  const stdoutWrite = vi.spyOn(process.stdout, "write").mockImplementation(() => true);
1343
848
  vi.spyOn(process.stderr, "write").mockImplementation(() => true);
1344
849
 
1345
- await runOpComponents({ args: makeArgs({ path: "svc", temporal: false }), plugins: [], serializers: [] });
850
+ await runOpComponents({ args: makeArgs({ path: "svc" }), plugins: [], serializers: [] });
1346
851
 
1347
852
  expect(runComponentsMock).toHaveBeenCalledWith(expect.any(String), "svc", {
1348
853
  env: undefined,
@@ -1362,15 +867,16 @@ describe("runOpComponents", () => {
1362
867
  order: ["shared-alb", "search-service"],
1363
868
  waves: [["shared-alb"], ["search-service"]],
1364
869
  results: [
1365
- { component: "shared-alb", ok: true, records: [] },
1366
- { component: "search-service", ok: true, records: [] },
870
+ { component: "shared-alb", ok: true, status: "ok", records: [] },
871
+ { component: "search-service", ok: true, status: "ok", records: [] },
1367
872
  ],
1368
873
  ok: true,
874
+ status: "ok",
1369
875
  },
1370
876
  });
1371
877
  const stderrWrite = vi.spyOn(process.stderr, "write").mockImplementation(() => true);
1372
878
 
1373
- const exit = await runOpComponents({ args: makeArgs({ path: "all", temporal: false }), plugins: [], serializers: [] });
879
+ const exit = await runOpComponents({ args: makeArgs({ path: "all" }), plugins: [], serializers: [] });
1374
880
 
1375
881
  expect(exit).toBe(0);
1376
882
  expect(runComponentsMock).toHaveBeenCalledWith(expect.any(String), "all", { env: undefined, componentOutputs: {}, buildParams: [] });
@@ -1387,14 +893,15 @@ describe("runOpComponents", () => {
1387
893
  run: {
1388
894
  order: ["svc"],
1389
895
  waves: [["svc"]],
1390
- results: [{ component: "svc", ok: false, records: [{ component: "svc", phase: "Apply", kind: "cfn-deploy", status: "fail", durationMs: 5, error: "boom" }] }],
896
+ results: [{ component: "svc", ok: false, status: "fail", records: [{ component: "svc", phase: "Apply", kind: "cfn-deploy", status: "fail", durationMs: 5, error: "boom" }] }],
1391
897
  ok: false,
898
+ status: "fail",
1392
899
  failedComponent: "svc",
1393
900
  },
1394
901
  });
1395
902
  const stderrWrite = vi.spyOn(process.stderr, "write").mockImplementation(() => true);
1396
903
 
1397
- const exit = await runOpComponents({ args: makeArgs({ path: "svc", temporal: false }), plugins: [], serializers: [] });
904
+ const exit = await runOpComponents({ args: makeArgs({ path: "svc" }), plugins: [], serializers: [] });
1398
905
 
1399
906
  expect(exit).toBe(1);
1400
907
  const printed = stderrWrite.mock.calls.map((c) => String(c[0])).join("");
@@ -1406,26 +913,33 @@ describe("runOpComponents", () => {
1406
913
  runComponentsMock.mockResolvedValue({ success: false, selected: [], error: 'Component "missing" not found. Known components: svc' });
1407
914
  const stderr = makeStderrSpy();
1408
915
 
1409
- const exit = await runOpComponents({ args: makeArgs({ path: "missing", temporal: false }), plugins: [], serializers: [] });
916
+ const exit = await runOpComponents({ args: makeArgs({ path: "missing" }), plugins: [], serializers: [] });
1410
917
 
1411
918
  expect(exit).toBe(1);
1412
919
  expect(stderr.join("\n")).toContain('Component "missing" not found');
1413
920
  });
1414
921
 
1415
- test("a gate in the component (local mode) → exit 1 with an actionable, Temporal-pointing message", async () => {
922
+ test("a gate in the component → exit 3 and the approve line (#2119)", async () => {
1416
923
  runComponentsMock.mockResolvedValue({
1417
924
  success: false,
1418
925
  selected: ["svc"],
1419
- gateUnsupported: { component: "svc", signalName: "release-approval" },
926
+ gated: {
927
+ component: "svc",
928
+ gate: {
929
+ version: 1, kind: "pending", op: "svc", gate: "release-approval",
930
+ timestamp: "2026-09-05T12:00:00.000Z", expiresAt: "2026-09-07T12:00:00.000Z",
931
+ },
932
+ },
1420
933
  });
1421
934
  const stderr = makeStderrSpy();
1422
935
 
1423
- const exit = await runOpComponents({ args: makeArgs({ path: "svc", temporal: false }), plugins: [], serializers: [] });
936
+ const exit = await runOpComponents({ args: makeArgs({ path: "svc" }), plugins: [], serializers: [] });
1424
937
 
1425
- expect(exit).toBe(1);
938
+ expect(exit).toBe(3);
1426
939
  const out = stderr.join("\n");
1427
- expect(out).toContain('gate "release-approval"');
1428
- expect(out).toContain("--temporal");
940
+ expect(out).toContain('gated on "release-approval"');
941
+ expect(out).toContain("chant approve svc release-approval");
942
+ expect(out).toContain("expires : 2026-09-07T12:00:00.000Z");
1429
943
  });
1430
944
 
1431
945
  // ── auto-release recording post-run (#597) ────────────────────────────────
@@ -1438,14 +952,15 @@ describe("runOpComponents", () => {
1438
952
  run: {
1439
953
  order: ["svc"],
1440
954
  waves: [["svc"]],
1441
- results: [{ component: "svc", ok: true, records: [{ component: "svc", phase: "Publish", kind: "publish-image", status: "ok", durationMs: 5, output: { digest: "sha256:abc" } }] }],
955
+ results: [{ component: "svc", ok: true, status: "ok", records: [{ component: "svc", phase: "Publish", kind: "publish-image", status: "ok", durationMs: 5, output: { digest: "sha256:abc" } }] }],
1442
956
  ok: true,
957
+ status: "ok",
1443
958
  },
1444
959
  });
1445
960
  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
961
  vi.spyOn(process.stderr, "write").mockImplementation(() => true);
1447
962
 
1448
- const exit = await runOpComponents({ args: makeArgs({ path: "svc", env: "staging", temporal: false }), plugins: [], serializers: [] });
963
+ const exit = await runOpComponents({ args: makeArgs({ path: "svc", env: "staging" }), plugins: [], serializers: [] });
1449
964
 
1450
965
  expect(exit).toBe(0);
1451
966
  expect(maybeRecordAutoReleaseMock).toHaveBeenCalledTimes(1);
@@ -1463,14 +978,15 @@ describe("runOpComponents", () => {
1463
978
  run: {
1464
979
  order: ["svc"],
1465
980
  waves: [["svc"]],
1466
- results: [{ component: "svc", ok: false, records: [{ component: "svc", phase: "Apply", kind: "cfn-deploy", status: "fail", durationMs: 5, error: "boom" }] }],
981
+ results: [{ component: "svc", ok: false, status: "fail", records: [{ component: "svc", phase: "Apply", kind: "cfn-deploy", status: "fail", durationMs: 5, error: "boom" }] }],
1467
982
  ok: false,
983
+ status: "fail",
1468
984
  failedComponent: "svc",
1469
985
  },
1470
986
  });
1471
987
  vi.spyOn(process.stderr, "write").mockImplementation(() => true);
1472
988
 
1473
- const exit = await runOpComponents({ args: makeArgs({ path: "svc", temporal: false }), plugins: [], serializers: [] });
989
+ const exit = await runOpComponents({ args: makeArgs({ path: "svc" }), plugins: [], serializers: [] });
1474
990
 
1475
991
  expect(exit).toBe(1);
1476
992
  expect(maybeRecordAutoReleaseMock).not.toHaveBeenCalled();
@@ -1481,12 +997,12 @@ describe("runOpComponents", () => {
1481
997
  runComponentsMock.mockResolvedValue({
1482
998
  success: true,
1483
999
  selected: ["svc"],
1484
- run: { order: ["svc"], waves: [["svc"]], results: [{ component: "svc", ok: true, records: [] }], ok: true },
1000
+ run: { order: ["svc"], waves: [["svc"]], results: [{ component: "svc", ok: true, status: "ok", records: [] }], ok: true, status: "ok" },
1485
1001
  });
1486
1002
  maybeRecordAutoReleaseMock.mockResolvedValue({ recorded: false, reason: "opted-out" });
1487
1003
  vi.spyOn(process.stderr, "write").mockImplementation(() => true);
1488
1004
 
1489
- const exit = await runOpComponents({ args: makeArgs({ path: "svc", temporal: false, noReleaseRecord: true }), plugins: [], serializers: [] });
1005
+ const exit = await runOpComponents({ args: makeArgs({ path: "svc", noReleaseRecord: true }), plugins: [], serializers: [] });
1490
1006
 
1491
1007
  expect(exit).toBe(0);
1492
1008
  expect(maybeRecordAutoReleaseMock).toHaveBeenCalledTimes(1);
@@ -1499,13 +1015,13 @@ describe("runOpComponents", () => {
1499
1015
  runComponentsMock.mockResolvedValue({
1500
1016
  success: true,
1501
1017
  selected: ["svc"],
1502
- run: { order: ["svc"], waves: [["svc"]], results: [{ component: "svc", ok: true, records: [] }], ok: true },
1018
+ run: { order: ["svc"], waves: [["svc"]], results: [{ component: "svc", ok: true, status: "ok", records: [] }], ok: true, status: "ok" },
1503
1019
  });
1504
1020
  loadChantConfigMock.mockResolvedValue({ config: { release: { autoRecord: false } } });
1505
1021
  maybeRecordAutoReleaseMock.mockResolvedValue({ recorded: false, reason: "opted-out" });
1506
1022
  vi.spyOn(process.stderr, "write").mockImplementation(() => true);
1507
1023
 
1508
- await runOpComponents({ args: makeArgs({ path: "svc", temporal: false }), plugins: [], serializers: [] });
1024
+ await runOpComponents({ args: makeArgs({ path: "svc" }), plugins: [], serializers: [] });
1509
1025
 
1510
1026
  const [, options] = maybeRecordAutoReleaseMock.mock.calls[0];
1511
1027
  expect(options).toMatchObject({ disabled: true });
@@ -1516,13 +1032,13 @@ describe("runOpComponents", () => {
1516
1032
  runComponentsMock.mockResolvedValue({
1517
1033
  success: true,
1518
1034
  selected: ["svc"],
1519
- run: { order: ["svc"], waves: [["svc"]], results: [{ component: "svc", ok: true, records: [] }], ok: true },
1035
+ run: { order: ["svc"], waves: [["svc"]], results: [{ component: "svc", ok: true, status: "ok", records: [] }], ok: true, status: "ok" },
1520
1036
  });
1521
1037
  maybeRecordAutoReleaseMock.mockResolvedValue({ recorded: false, reason: "error", error: "ledger push failed" });
1522
1038
  const stderr = makeStderrSpy();
1523
1039
  vi.spyOn(process.stderr, "write").mockImplementation(() => true);
1524
1040
 
1525
- const exit = await runOpComponents({ args: makeArgs({ path: "svc", temporal: false }), plugins: [], serializers: [] });
1041
+ const exit = await runOpComponents({ args: makeArgs({ path: "svc" }), plugins: [], serializers: [] });
1526
1042
 
1527
1043
  expect(exit).toBe(0);
1528
1044
  expect(stderr.join("\n")).toContain("ledger push failed");
@@ -1543,12 +1059,13 @@ describe("runOpComponents", () => {
1543
1059
  waves: [["svc"]],
1544
1060
  results: [{ component: "svc", ok: true, records: [{ component: "svc", phase: "Build", kind: "docker-build", status: "ok", durationMs: 5, output: buildOutput }] }],
1545
1061
  ok: true,
1062
+ status: "ok",
1546
1063
  },
1547
1064
  });
1548
1065
  maybePersistBuildManifestMock.mockResolvedValue({ persisted: true, commit: "a".repeat(40), manifestDigest: "sha256:manifestabc" });
1549
1066
  vi.spyOn(process.stderr, "write").mockImplementation(() => true);
1550
1067
 
1551
- const exit = await runOpComponents({ args: makeArgs({ path: "svc", env: "staging", temporal: false }), plugins: [], serializers: [] });
1068
+ const exit = await runOpComponents({ args: makeArgs({ path: "svc", env: "staging" }), plugins: [], serializers: [] });
1552
1069
 
1553
1070
  expect(exit).toBe(0);
1554
1071
  expect(maybePersistBuildManifestMock).toHaveBeenCalledTimes(1);
@@ -1566,14 +1083,15 @@ describe("runOpComponents", () => {
1566
1083
  run: {
1567
1084
  order: ["svc"],
1568
1085
  waves: [["svc"]],
1569
- results: [{ component: "svc", ok: false, records: [{ component: "svc", phase: "Apply", kind: "cfn-deploy", status: "fail", durationMs: 5, error: "boom" }] }],
1086
+ results: [{ component: "svc", ok: false, status: "fail", records: [{ component: "svc", phase: "Apply", kind: "cfn-deploy", status: "fail", durationMs: 5, error: "boom" }] }],
1570
1087
  ok: false,
1088
+ status: "fail",
1571
1089
  failedComponent: "svc",
1572
1090
  },
1573
1091
  });
1574
1092
  vi.spyOn(process.stderr, "write").mockImplementation(() => true);
1575
1093
 
1576
- const exit = await runOpComponents({ args: makeArgs({ path: "svc", temporal: false }), plugins: [], serializers: [] });
1094
+ const exit = await runOpComponents({ args: makeArgs({ path: "svc" }), plugins: [], serializers: [] });
1577
1095
 
1578
1096
  expect(exit).toBe(1);
1579
1097
  expect(maybePersistBuildManifestMock).not.toHaveBeenCalled();
@@ -1584,12 +1102,12 @@ describe("runOpComponents", () => {
1584
1102
  runComponentsMock.mockResolvedValue({
1585
1103
  success: true,
1586
1104
  selected: ["svc"],
1587
- run: { order: ["svc"], waves: [["svc"]], results: [{ component: "svc", ok: true, records: [] }], ok: true },
1105
+ run: { order: ["svc"], waves: [["svc"]], results: [{ component: "svc", ok: true, status: "ok", records: [] }], ok: true, status: "ok" },
1588
1106
  });
1589
1107
  maybePersistBuildManifestMock.mockResolvedValue({ persisted: false, reason: "opted-out" });
1590
1108
  vi.spyOn(process.stderr, "write").mockImplementation(() => true);
1591
1109
 
1592
- const exit = await runOpComponents({ args: makeArgs({ path: "svc", temporal: false, noReleaseRecord: true }), plugins: [], serializers: [] });
1110
+ const exit = await runOpComponents({ args: makeArgs({ path: "svc", noReleaseRecord: true }), plugins: [], serializers: [] });
1593
1111
 
1594
1112
  expect(exit).toBe(0);
1595
1113
  expect(maybePersistBuildManifestMock).toHaveBeenCalledTimes(1);
@@ -1602,13 +1120,13 @@ describe("runOpComponents", () => {
1602
1120
  runComponentsMock.mockResolvedValue({
1603
1121
  success: true,
1604
1122
  selected: ["svc"],
1605
- run: { order: ["svc"], waves: [["svc"]], results: [{ component: "svc", ok: true, records: [] }], ok: true },
1123
+ run: { order: ["svc"], waves: [["svc"]], results: [{ component: "svc", ok: true, status: "ok", records: [] }], ok: true, status: "ok" },
1606
1124
  });
1607
1125
  maybePersistBuildManifestMock.mockResolvedValue({ persisted: false, reason: "error", error: "manifest push failed" });
1608
1126
  const stderr = makeStderrSpy();
1609
1127
  vi.spyOn(process.stderr, "write").mockImplementation(() => true);
1610
1128
 
1611
- const exit = await runOpComponents({ args: makeArgs({ path: "svc", temporal: false }), plugins: [], serializers: [] });
1129
+ const exit = await runOpComponents({ args: makeArgs({ path: "svc" }), plugins: [], serializers: [] });
1612
1130
 
1613
1131
  expect(exit).toBe(0);
1614
1132
  expect(stderr.join("\n")).toContain("manifest push failed");
@@ -1616,377 +1134,3 @@ describe("runOpComponents", () => {
1616
1134
  });
1617
1135
  });
1618
1136
  });
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
- });