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