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