From fa858410056bd691ec07345ecf54fb138b5560a1 Mon Sep 17 00:00:00 2001 From: Martin Gaughran Date: Fri, 26 Jun 2026 17:04:22 +0000 Subject: [PATCH 01/12] Add behavioural tests for validate command This covers cases that aren't easily tested with golden master tests. --- .../example-module-shell/1.0.yaml | 19 ++++++++ .../all-excluded-defaults/settings.yaml | 3 ++ .../example-module-shell/1.0.yaml | 17 +++++++ .../invalid/missing-default/settings.yaml | 4 ++ .../example-module-shell/1.0.yaml | 17 +++++++ .../modified-no-allow-updates/settings.yaml | 3 ++ .../removed-no-deprecation/settings.yaml | 3 ++ .../example-module-base/1.0.yaml | 8 ++++ .../example-module-dependent/1.0.yaml | 12 +++++ .../invalid/unknown-dependency/settings.yaml | 3 ++ .../minimal/example-module-shell/1.0.yaml | 17 +++++++ tests/configs/minimal/settings.yaml | 3 ++ tests/test_validate.py | 47 +++++++++++++++++++ 13 files changed, 156 insertions(+) create mode 100644 tests/configs/invalid/all-excluded-defaults/example-module-shell/1.0.yaml create mode 100644 tests/configs/invalid/all-excluded-defaults/settings.yaml create mode 100644 tests/configs/invalid/missing-default/example-module-shell/1.0.yaml create mode 100644 tests/configs/invalid/missing-default/settings.yaml create mode 100644 tests/configs/invalid/modified-no-allow-updates/example-module-shell/1.0.yaml create mode 100644 tests/configs/invalid/modified-no-allow-updates/settings.yaml create mode 100644 tests/configs/invalid/removed-no-deprecation/settings.yaml create mode 100644 tests/configs/invalid/unknown-dependency/example-module-base/1.0.yaml create mode 100644 tests/configs/invalid/unknown-dependency/example-module-dependent/1.0.yaml create mode 100644 tests/configs/invalid/unknown-dependency/settings.yaml create mode 100644 tests/configs/minimal/example-module-shell/1.0.yaml create mode 100644 tests/configs/minimal/settings.yaml create mode 100644 tests/test_validate.py diff --git a/tests/configs/invalid/all-excluded-defaults/example-module-shell/1.0.yaml b/tests/configs/invalid/all-excluded-defaults/example-module-shell/1.0.yaml new file mode 100644 index 00000000..92da05df --- /dev/null +++ b/tests/configs/invalid/all-excluded-defaults/example-module-shell/1.0.yaml @@ -0,0 +1,19 @@ +# yaml-language-server: $schema=/workspaces/deploy-tools/src/deploy_tools/models/schemas/release.json + +module: + name: example-module-shell + version: "1.0" + description: Shell module excluded from defaults with no eligible fallback + + env_vars: + - name: SHELL_VALUE + value: hello + + applications: + - app_type: shell + + name: test-shell-echo + script: + - echo $SHELL_VALUE + + exclude_from_defaults: true diff --git a/tests/configs/invalid/all-excluded-defaults/settings.yaml b/tests/configs/invalid/all-excluded-defaults/settings.yaml new file mode 100644 index 00000000..70f60017 --- /dev/null +++ b/tests/configs/invalid/all-excluded-defaults/settings.yaml @@ -0,0 +1,3 @@ +# yaml-language-server: $schema=/workspaces/deploy-tools/src/deploy_tools/models/schemas/deployment-settings.json + +default_versions: {} diff --git a/tests/configs/invalid/missing-default/example-module-shell/1.0.yaml b/tests/configs/invalid/missing-default/example-module-shell/1.0.yaml new file mode 100644 index 00000000..e8f3c4a7 --- /dev/null +++ b/tests/configs/invalid/missing-default/example-module-shell/1.0.yaml @@ -0,0 +1,17 @@ +# yaml-language-server: $schema=/workspaces/deploy-tools/src/deploy_tools/models/schemas/release.json + +module: + name: example-module-shell + version: "1.0" + description: Minimal shell-only module used by the validate tests + + env_vars: + - name: SHELL_VALUE + value: hello + + applications: + - app_type: shell + + name: test-shell-echo + script: + - echo $SHELL_VALUE diff --git a/tests/configs/invalid/missing-default/settings.yaml b/tests/configs/invalid/missing-default/settings.yaml new file mode 100644 index 00000000..33cecd08 --- /dev/null +++ b/tests/configs/invalid/missing-default/settings.yaml @@ -0,0 +1,4 @@ +# yaml-language-server: $schema=/workspaces/deploy-tools/src/deploy_tools/models/schemas/deployment-settings.json + +default_versions: + example-module-shell: "2.0" diff --git a/tests/configs/invalid/modified-no-allow-updates/example-module-shell/1.0.yaml b/tests/configs/invalid/modified-no-allow-updates/example-module-shell/1.0.yaml new file mode 100644 index 00000000..69c07817 --- /dev/null +++ b/tests/configs/invalid/modified-no-allow-updates/example-module-shell/1.0.yaml @@ -0,0 +1,17 @@ +# yaml-language-server: $schema=/workspaces/deploy-tools/src/deploy_tools/models/schemas/release.json + +module: + name: example-module-shell + version: "1.0" + description: Minimal shell-only module used by the validate tests + + env_vars: + - name: SHELL_VALUE + value: changed + + applications: + - app_type: shell + + name: test-shell-echo + script: + - echo $SHELL_VALUE diff --git a/tests/configs/invalid/modified-no-allow-updates/settings.yaml b/tests/configs/invalid/modified-no-allow-updates/settings.yaml new file mode 100644 index 00000000..70f60017 --- /dev/null +++ b/tests/configs/invalid/modified-no-allow-updates/settings.yaml @@ -0,0 +1,3 @@ +# yaml-language-server: $schema=/workspaces/deploy-tools/src/deploy_tools/models/schemas/deployment-settings.json + +default_versions: {} diff --git a/tests/configs/invalid/removed-no-deprecation/settings.yaml b/tests/configs/invalid/removed-no-deprecation/settings.yaml new file mode 100644 index 00000000..70f60017 --- /dev/null +++ b/tests/configs/invalid/removed-no-deprecation/settings.yaml @@ -0,0 +1,3 @@ +# yaml-language-server: $schema=/workspaces/deploy-tools/src/deploy_tools/models/schemas/deployment-settings.json + +default_versions: {} diff --git a/tests/configs/invalid/unknown-dependency/example-module-base/1.0.yaml b/tests/configs/invalid/unknown-dependency/example-module-base/1.0.yaml new file mode 100644 index 00000000..7e8c3115 --- /dev/null +++ b/tests/configs/invalid/unknown-dependency/example-module-base/1.0.yaml @@ -0,0 +1,8 @@ +# yaml-language-server: $schema=/workspaces/deploy-tools/src/deploy_tools/models/schemas/release.json + +module: + name: example-module-base + version: "1.0" + description: Base module that the dependent module references + + applications: [] diff --git a/tests/configs/invalid/unknown-dependency/example-module-dependent/1.0.yaml b/tests/configs/invalid/unknown-dependency/example-module-dependent/1.0.yaml new file mode 100644 index 00000000..dc38a528 --- /dev/null +++ b/tests/configs/invalid/unknown-dependency/example-module-dependent/1.0.yaml @@ -0,0 +1,12 @@ +# yaml-language-server: $schema=/workspaces/deploy-tools/src/deploy_tools/models/schemas/release.json + +module: + name: example-module-dependent + version: "1.0" + description: References a version of example-module-base that does not exist + + dependencies: + - name: example-module-base + version: "9.9" + + applications: [] diff --git a/tests/configs/invalid/unknown-dependency/settings.yaml b/tests/configs/invalid/unknown-dependency/settings.yaml new file mode 100644 index 00000000..70f60017 --- /dev/null +++ b/tests/configs/invalid/unknown-dependency/settings.yaml @@ -0,0 +1,3 @@ +# yaml-language-server: $schema=/workspaces/deploy-tools/src/deploy_tools/models/schemas/deployment-settings.json + +default_versions: {} diff --git a/tests/configs/minimal/example-module-shell/1.0.yaml b/tests/configs/minimal/example-module-shell/1.0.yaml new file mode 100644 index 00000000..e8f3c4a7 --- /dev/null +++ b/tests/configs/minimal/example-module-shell/1.0.yaml @@ -0,0 +1,17 @@ +# yaml-language-server: $schema=/workspaces/deploy-tools/src/deploy_tools/models/schemas/release.json + +module: + name: example-module-shell + version: "1.0" + description: Minimal shell-only module used by the validate tests + + env_vars: + - name: SHELL_VALUE + value: hello + + applications: + - app_type: shell + + name: test-shell-echo + script: + - echo $SHELL_VALUE diff --git a/tests/configs/minimal/settings.yaml b/tests/configs/minimal/settings.yaml new file mode 100644 index 00000000..70f60017 --- /dev/null +++ b/tests/configs/minimal/settings.yaml @@ -0,0 +1,3 @@ +# yaml-language-server: $schema=/workspaces/deploy-tools/src/deploy_tools/models/schemas/deployment-settings.json + +default_versions: {} diff --git a/tests/test_validate.py b/tests/test_validate.py new file mode 100644 index 00000000..d0ef0b15 --- /dev/null +++ b/tests/test_validate.py @@ -0,0 +1,47 @@ +from pathlib import Path + +import pytest + +from conftest import run_cli +from deploy_tools.validate import ValidationError + +# Configurations that 'validate' should reject when deployed from scratch into an empty +# area. Each maps a config folder under configs/invalid to a substring expected in the +# resulting ValidationError. +INVALID_INITIAL_CONFIGS = [ + ("unknown-dependency", "unknown module dependency"), + ("missing-default", "Unable to configure"), + ("all-excluded-defaults", "exclude_from_defaults"), +] + + +@pytest.mark.parametrize("config_name, message", INVALID_INITIAL_CONFIGS) +def test_validate_rejects_invalid_initial_config( + tmp_path: Path, configs: Path, config_name: str, message: str +): + with pytest.raises(ValidationError, match=message): + run_cli( + "validate", "--from-scratch", tmp_path, configs / "invalid" / config_name + ) + + +def test_validate_rejects_update_without_allow_updates(tmp_path: Path, configs: Path): + # Deploy a baseline whose module did not opt in to allow_updates, then validate a + # config that changes that module in place: this must be rejected. + run_cli("sync", "--from-scratch", tmp_path, configs / "minimal") + with pytest.raises(ValidationError, match="modified without updating version"): + run_cli("validate", tmp_path, configs / "invalid" / "modified-no-allow-updates") + + +def test_validate_rejects_removal_without_deprecation(tmp_path: Path, configs: Path): + # Deploy a baseline module, then validate a config that drops it without first + # deprecating it (and without --allow-all): this must be rejected. + run_cli("sync", "--from-scratch", tmp_path, configs / "minimal") + with pytest.raises(ValidationError, match="removed without prior deprecation"): + run_cli("validate", tmp_path, configs / "invalid" / "removed-no-deprecation") + + +def test_validate_test_build(tmp_path: Path, configs: Path): + # --test-build actually builds the modules into a temporary area and syntax-checks + # the generated shell entrypoints, so exercise it on a valid shell-only config. + run_cli("validate", "--test-build", "--from-scratch", tmp_path, configs / "minimal") From b536836ca9755cc3f0f42d60b08fec9745ee30e2 Mon Sep 17 00:00:00 2001 From: Martin Gaughran Date: Tue, 7 Jul 2026 15:40:32 +0000 Subject: [PATCH 02/12] Add validation checks for handling initially-deprecated modules --- .../example-module-shell/1.0.yaml | 17 ++++++++++ .../example-module-shell/2.0.yaml | 21 ++++++++++++ .../invalid/added-deprecated/settings.yaml | 3 ++ tests/test_validate.py | 34 ++++++++++++++++--- 4 files changed, 71 insertions(+), 4 deletions(-) create mode 100644 tests/configs/invalid/added-deprecated/example-module-shell/1.0.yaml create mode 100644 tests/configs/invalid/added-deprecated/example-module-shell/2.0.yaml create mode 100644 tests/configs/invalid/added-deprecated/settings.yaml diff --git a/tests/configs/invalid/added-deprecated/example-module-shell/1.0.yaml b/tests/configs/invalid/added-deprecated/example-module-shell/1.0.yaml new file mode 100644 index 00000000..e8f3c4a7 --- /dev/null +++ b/tests/configs/invalid/added-deprecated/example-module-shell/1.0.yaml @@ -0,0 +1,17 @@ +# yaml-language-server: $schema=/workspaces/deploy-tools/src/deploy_tools/models/schemas/release.json + +module: + name: example-module-shell + version: "1.0" + description: Minimal shell-only module used by the validate tests + + env_vars: + - name: SHELL_VALUE + value: hello + + applications: + - app_type: shell + + name: test-shell-echo + script: + - echo $SHELL_VALUE diff --git a/tests/configs/invalid/added-deprecated/example-module-shell/2.0.yaml b/tests/configs/invalid/added-deprecated/example-module-shell/2.0.yaml new file mode 100644 index 00000000..c857eea9 --- /dev/null +++ b/tests/configs/invalid/added-deprecated/example-module-shell/2.0.yaml @@ -0,0 +1,21 @@ +# yaml-language-server: $schema=/workspaces/deploy-tools/src/deploy_tools/models/schemas/release.json + +module: + name: example-module-shell + version: "2.0" + description: Minimal shell-only module used by the validate tests + + env_vars: + - name: SHELL_VALUE + value: hello + + applications: + - app_type: shell + + name: test-shell-echo + script: + - echo $SHELL_VALUE + +# A brand-new release introduced directly in a deprecated state. This is only permitted +# with --allow-all (or a from-scratch deploy); a normal deploy must reject it. +deprecated: true diff --git a/tests/configs/invalid/added-deprecated/settings.yaml b/tests/configs/invalid/added-deprecated/settings.yaml new file mode 100644 index 00000000..70f60017 --- /dev/null +++ b/tests/configs/invalid/added-deprecated/settings.yaml @@ -0,0 +1,3 @@ +# yaml-language-server: $schema=/workspaces/deploy-tools/src/deploy_tools/models/schemas/deployment-settings.json + +default_versions: {} diff --git a/tests/test_validate.py b/tests/test_validate.py index d0ef0b15..51b945a7 100644 --- a/tests/test_validate.py +++ b/tests/test_validate.py @@ -18,14 +18,16 @@ @pytest.mark.parametrize("config_name, message", INVALID_INITIAL_CONFIGS) def test_validate_rejects_invalid_initial_config( tmp_path: Path, configs: Path, config_name: str, message: str -): +) -> None: with pytest.raises(ValidationError, match=message): run_cli( "validate", "--from-scratch", tmp_path, configs / "invalid" / config_name ) -def test_validate_rejects_update_without_allow_updates(tmp_path: Path, configs: Path): +def test_validate_rejects_update_without_allow_updates( + tmp_path: Path, configs: Path +) -> None: # Deploy a baseline whose module did not opt in to allow_updates, then validate a # config that changes that module in place: this must be rejected. run_cli("sync", "--from-scratch", tmp_path, configs / "minimal") @@ -33,7 +35,9 @@ def test_validate_rejects_update_without_allow_updates(tmp_path: Path, configs: run_cli("validate", tmp_path, configs / "invalid" / "modified-no-allow-updates") -def test_validate_rejects_removal_without_deprecation(tmp_path: Path, configs: Path): +def test_validate_rejects_removal_without_deprecation( + tmp_path: Path, configs: Path +) -> None: # Deploy a baseline module, then validate a config that drops it without first # deprecating it (and without --allow-all): this must be rejected. run_cli("sync", "--from-scratch", tmp_path, configs / "minimal") @@ -41,7 +45,29 @@ def test_validate_rejects_removal_without_deprecation(tmp_path: Path, configs: P run_cli("validate", tmp_path, configs / "invalid" / "removed-no-deprecation") -def test_validate_test_build(tmp_path: Path, configs: Path): +def test_validate_rejects_added_deprecated_module( + tmp_path: Path, configs: Path +) -> None: + # Deploy a baseline module, then validate a config that introduces a brand-new + # release already in a deprecated state. Deprecating a module on initial creation + # is only allowed with --allow-all, so a normal validate must reject it. + run_cli("sync", "--from-scratch", tmp_path, configs / "minimal") + with pytest.raises(ValidationError, match="cannot have deprecated status"): + run_cli("validate", tmp_path, configs / "invalid" / "added-deprecated") + + +def test_validate_allows_added_deprecated_module_with_allow_all( + tmp_path: Path, configs: Path +) -> None: + # The --allow-all flag deliberately permits introducing an already-deprecated + # release on initial creation. + run_cli("sync", "--from-scratch", tmp_path, configs / "minimal") + run_cli( + "validate", "--allow-all", tmp_path, configs / "invalid" / "added-deprecated" + ) + + +def test_validate_test_build(tmp_path: Path, configs: Path) -> None: # --test-build actually builds the modules into a temporary area and syntax-checks # the generated shell entrypoints, so exercise it on a valid shell-only config. run_cli("validate", "--test-build", "--from-scratch", tmp_path, configs / "minimal") From 9579ac31e6332d77eeb6bc73de8fc3a115510153 Mon Sep 17 00:00:00 2001 From: Martin Gaughran Date: Fri, 19 Jun 2026 12:50:57 +0000 Subject: [PATCH 03/12] Add dedicated unit tests for default version logic --- tests/test_default_versions.py | 126 +++++++++++++++++++++++++++++++++ 1 file changed, 126 insertions(+) create mode 100644 tests/test_default_versions.py diff --git a/tests/test_default_versions.py b/tests/test_default_versions.py new file mode 100644 index 00000000..2025ce6e --- /dev/null +++ b/tests/test_default_versions.py @@ -0,0 +1,126 @@ +"""Unit tests for default-version selection. + +These call the function directly rather than through the CLI. Here that is to isolate +pure, non-trivial logic (e.g. natsort ordering of non-SemVer versions): a targeted unit +test states the outcome precisely and pins any regression to a line, rather than +surfacing it as an opaque golden-master diff. +""" + +from collections import defaultdict + +import pytest + +from deploy_tools.models.deployment import ( + Deployment, + DeploymentSettings, + ReleasesByNameAndVersion, +) +from deploy_tools.models.module import Module, Release +from deploy_tools.validate import ValidationError, validate_default_versions + + +def _release( + name: str, version: str, *, deprecated: bool = False, excluded: bool = False +) -> Release: + """Build a minimal Release with no applications, for selection tests.""" + return Release( + module=Module( + name=name, + version=version, + applications=[], + exclude_from_defaults=excluded, + ), + deprecated=deprecated, + ) + + +def _deployment( + *releases: Release, default_versions: dict[str, str] | None = None +) -> Deployment: + """Assemble a Deployment from loose Releases and optional explicit defaults.""" + by_name: ReleasesByNameAndVersion = defaultdict(dict) + for release in releases: + by_name[release.module.name][release.module.version] = release + + return Deployment( + settings=DeploymentSettings(default_versions=default_versions or {}), + releases=by_name, + ) + + +def test_auto_selects_latest_version_by_natsort() -> None: + # natsort, not string ordering: "10.0" must beat "2.0" (which sorts last lexically). + deployment = _deployment( + _release("mod", "1.0"), + _release("mod", "2.0"), + _release("mod", "10.0"), + ) + assert validate_default_versions(deployment) == {"mod": "10.0"} + + +def test_prerelease_sorts_before_final_release() -> None: + # The documented non-SemVer case: "1.2rc1" must come before "1.2", so the final + # release is chosen as default over its own release candidate. + deployment = _deployment( + _release("mod", "1.2rc1"), + _release("mod", "1.2"), + ) + assert validate_default_versions(deployment) == {"mod": "1.2"} + + +def test_excluded_versions_are_not_auto_selected() -> None: + # The highest version opts out of defaults, so the next-highest is chosen instead. + deployment = _deployment( + _release("mod", "1.0"), + _release("mod", "2.0", excluded=True), + ) + assert validate_default_versions(deployment) == {"mod": "1.0"} + + +def test_deprecated_versions_are_not_auto_selected() -> None: + # Deprecated releases are excluded from the deployed set, so a higher deprecated + # version must not become the default over a lower active one. + deployment = _deployment( + _release("mod", "1.0"), + _release("mod", "2.0", deprecated=True), + ) + assert validate_default_versions(deployment) == {"mod": "1.0"} + + +def test_explicit_default_is_preserved_over_auto_selection() -> None: + # An explicit default wins even when a higher version exists. + deployment = _deployment( + _release("mod", "1.0"), + _release("mod", "2.0"), + default_versions={"mod": "1.0"}, + ) + assert validate_default_versions(deployment) == {"mod": "1.0"} + + +def test_explicit_default_for_nonexistent_version_raises() -> None: + # Pointing the default at a version that will not be deployed is rejected. + deployment = _deployment( + _release("mod", "1.0"), + default_versions={"mod": "9.9"}, + ) + with pytest.raises(ValidationError, match="Unable to configure"): + validate_default_versions(deployment) + + +def test_explicit_default_for_deprecated_version_raises() -> None: + # A deprecated version is not in the deployed set, so it cannot be an explicit + # default either. + deployment = _deployment( + _release("mod", "1.0", deprecated=True), + default_versions={"mod": "1.0"}, + ) + with pytest.raises(ValidationError, match="Unable to configure"): + validate_default_versions(deployment) + + +def test_all_versions_excluded_raises() -> None: + # With every version opting out and no explicit default, there is no eligible + # version to make default. + deployment = _deployment(_release("mod", "1.0", excluded=True)) + with pytest.raises(ValidationError, match="exclude_from_defaults"): + validate_default_versions(deployment) From 6f972917463ff91611aaf349da3f2c083719582c Mon Sep 17 00:00:00 2001 From: Martin Gaughran Date: Wed, 8 Jul 2026 16:50:07 +0000 Subject: [PATCH 04/12] Move minimal test config to location consistent with others --- .../minimal/example-module-shell/1.0.yaml | 0 tests/configs/{ => valid}/minimal/settings.yaml | 0 tests/test_validate.py | 16 +++++++++++----- 3 files changed, 11 insertions(+), 5 deletions(-) rename tests/configs/{ => valid}/minimal/example-module-shell/1.0.yaml (100%) rename tests/configs/{ => valid}/minimal/settings.yaml (100%) diff --git a/tests/configs/minimal/example-module-shell/1.0.yaml b/tests/configs/valid/minimal/example-module-shell/1.0.yaml similarity index 100% rename from tests/configs/minimal/example-module-shell/1.0.yaml rename to tests/configs/valid/minimal/example-module-shell/1.0.yaml diff --git a/tests/configs/minimal/settings.yaml b/tests/configs/valid/minimal/settings.yaml similarity index 100% rename from tests/configs/minimal/settings.yaml rename to tests/configs/valid/minimal/settings.yaml diff --git a/tests/test_validate.py b/tests/test_validate.py index 51b945a7..8d1d3528 100644 --- a/tests/test_validate.py +++ b/tests/test_validate.py @@ -30,7 +30,7 @@ def test_validate_rejects_update_without_allow_updates( ) -> None: # Deploy a baseline whose module did not opt in to allow_updates, then validate a # config that changes that module in place: this must be rejected. - run_cli("sync", "--from-scratch", tmp_path, configs / "minimal") + run_cli("sync", "--from-scratch", tmp_path, configs / "valid" / "minimal") with pytest.raises(ValidationError, match="modified without updating version"): run_cli("validate", tmp_path, configs / "invalid" / "modified-no-allow-updates") @@ -40,7 +40,7 @@ def test_validate_rejects_removal_without_deprecation( ) -> None: # Deploy a baseline module, then validate a config that drops it without first # deprecating it (and without --allow-all): this must be rejected. - run_cli("sync", "--from-scratch", tmp_path, configs / "minimal") + run_cli("sync", "--from-scratch", tmp_path, configs / "valid" / "minimal") with pytest.raises(ValidationError, match="removed without prior deprecation"): run_cli("validate", tmp_path, configs / "invalid" / "removed-no-deprecation") @@ -51,7 +51,7 @@ def test_validate_rejects_added_deprecated_module( # Deploy a baseline module, then validate a config that introduces a brand-new # release already in a deprecated state. Deprecating a module on initial creation # is only allowed with --allow-all, so a normal validate must reject it. - run_cli("sync", "--from-scratch", tmp_path, configs / "minimal") + run_cli("sync", "--from-scratch", tmp_path, configs / "valid" / "minimal") with pytest.raises(ValidationError, match="cannot have deprecated status"): run_cli("validate", tmp_path, configs / "invalid" / "added-deprecated") @@ -61,7 +61,7 @@ def test_validate_allows_added_deprecated_module_with_allow_all( ) -> None: # The --allow-all flag deliberately permits introducing an already-deprecated # release on initial creation. - run_cli("sync", "--from-scratch", tmp_path, configs / "minimal") + run_cli("sync", "--from-scratch", tmp_path, configs / "valid" / "minimal") run_cli( "validate", "--allow-all", tmp_path, configs / "invalid" / "added-deprecated" ) @@ -70,4 +70,10 @@ def test_validate_allows_added_deprecated_module_with_allow_all( def test_validate_test_build(tmp_path: Path, configs: Path) -> None: # --test-build actually builds the modules into a temporary area and syntax-checks # the generated shell entrypoints, so exercise it on a valid shell-only config. - run_cli("validate", "--test-build", "--from-scratch", tmp_path, configs / "minimal") + run_cli( + "validate", + "--test-build", + "--from-scratch", + tmp_path, + configs / "valid" / "minimal", + ) From 819bbf7a67f4735ffea5f1200e7ca2f33b51dee3 Mon Sep 17 00:00:00 2001 From: Martin Gaughran Date: Wed, 8 Jul 2026 16:52:06 +0000 Subject: [PATCH 05/12] Improve names of configuration folders --- .../example-module-shell/1.0.yaml | 0 .../settings.yaml | 0 .../example-module-shell/1.0.yaml | 0 .../example-module-shell/2.0.yaml | 0 .../settings.yaml | 0 .../example-module-shell/1.0.yaml | 0 .../settings.yaml | 0 .../example-module-shell/1.0.yaml | 0 .../settings.yaml | 0 .../settings.yaml | 0 tests/test_validate.py | 19 +++++++++++++------ 11 files changed, 13 insertions(+), 6 deletions(-) rename tests/configs/invalid/{added-deprecated => default-version-not-deployed}/example-module-shell/1.0.yaml (100%) rename tests/configs/invalid/{missing-default => default-version-not-deployed}/settings.yaml (100%) rename tests/configs/invalid/{missing-default => deprecated-on-creation}/example-module-shell/1.0.yaml (100%) rename tests/configs/invalid/{added-deprecated => deprecated-on-creation}/example-module-shell/2.0.yaml (100%) rename tests/configs/invalid/{added-deprecated => deprecated-on-creation}/settings.yaml (100%) rename tests/configs/invalid/{modified-no-allow-updates => modified-without-allow-updates}/example-module-shell/1.0.yaml (100%) rename tests/configs/invalid/{all-excluded-defaults => modified-without-allow-updates}/settings.yaml (100%) rename tests/configs/invalid/{all-excluded-defaults => no-eligible-default}/example-module-shell/1.0.yaml (100%) rename tests/configs/invalid/{modified-no-allow-updates => no-eligible-default}/settings.yaml (100%) rename tests/configs/invalid/{removed-no-deprecation => removed-without-deprecation}/settings.yaml (100%) diff --git a/tests/configs/invalid/added-deprecated/example-module-shell/1.0.yaml b/tests/configs/invalid/default-version-not-deployed/example-module-shell/1.0.yaml similarity index 100% rename from tests/configs/invalid/added-deprecated/example-module-shell/1.0.yaml rename to tests/configs/invalid/default-version-not-deployed/example-module-shell/1.0.yaml diff --git a/tests/configs/invalid/missing-default/settings.yaml b/tests/configs/invalid/default-version-not-deployed/settings.yaml similarity index 100% rename from tests/configs/invalid/missing-default/settings.yaml rename to tests/configs/invalid/default-version-not-deployed/settings.yaml diff --git a/tests/configs/invalid/missing-default/example-module-shell/1.0.yaml b/tests/configs/invalid/deprecated-on-creation/example-module-shell/1.0.yaml similarity index 100% rename from tests/configs/invalid/missing-default/example-module-shell/1.0.yaml rename to tests/configs/invalid/deprecated-on-creation/example-module-shell/1.0.yaml diff --git a/tests/configs/invalid/added-deprecated/example-module-shell/2.0.yaml b/tests/configs/invalid/deprecated-on-creation/example-module-shell/2.0.yaml similarity index 100% rename from tests/configs/invalid/added-deprecated/example-module-shell/2.0.yaml rename to tests/configs/invalid/deprecated-on-creation/example-module-shell/2.0.yaml diff --git a/tests/configs/invalid/added-deprecated/settings.yaml b/tests/configs/invalid/deprecated-on-creation/settings.yaml similarity index 100% rename from tests/configs/invalid/added-deprecated/settings.yaml rename to tests/configs/invalid/deprecated-on-creation/settings.yaml diff --git a/tests/configs/invalid/modified-no-allow-updates/example-module-shell/1.0.yaml b/tests/configs/invalid/modified-without-allow-updates/example-module-shell/1.0.yaml similarity index 100% rename from tests/configs/invalid/modified-no-allow-updates/example-module-shell/1.0.yaml rename to tests/configs/invalid/modified-without-allow-updates/example-module-shell/1.0.yaml diff --git a/tests/configs/invalid/all-excluded-defaults/settings.yaml b/tests/configs/invalid/modified-without-allow-updates/settings.yaml similarity index 100% rename from tests/configs/invalid/all-excluded-defaults/settings.yaml rename to tests/configs/invalid/modified-without-allow-updates/settings.yaml diff --git a/tests/configs/invalid/all-excluded-defaults/example-module-shell/1.0.yaml b/tests/configs/invalid/no-eligible-default/example-module-shell/1.0.yaml similarity index 100% rename from tests/configs/invalid/all-excluded-defaults/example-module-shell/1.0.yaml rename to tests/configs/invalid/no-eligible-default/example-module-shell/1.0.yaml diff --git a/tests/configs/invalid/modified-no-allow-updates/settings.yaml b/tests/configs/invalid/no-eligible-default/settings.yaml similarity index 100% rename from tests/configs/invalid/modified-no-allow-updates/settings.yaml rename to tests/configs/invalid/no-eligible-default/settings.yaml diff --git a/tests/configs/invalid/removed-no-deprecation/settings.yaml b/tests/configs/invalid/removed-without-deprecation/settings.yaml similarity index 100% rename from tests/configs/invalid/removed-no-deprecation/settings.yaml rename to tests/configs/invalid/removed-without-deprecation/settings.yaml diff --git a/tests/test_validate.py b/tests/test_validate.py index 8d1d3528..6ddba61f 100644 --- a/tests/test_validate.py +++ b/tests/test_validate.py @@ -10,8 +10,8 @@ # resulting ValidationError. INVALID_INITIAL_CONFIGS = [ ("unknown-dependency", "unknown module dependency"), - ("missing-default", "Unable to configure"), - ("all-excluded-defaults", "exclude_from_defaults"), + ("default-version-not-deployed", "Unable to configure"), + ("no-eligible-default", "exclude_from_defaults"), ] @@ -32,7 +32,9 @@ def test_validate_rejects_update_without_allow_updates( # config that changes that module in place: this must be rejected. run_cli("sync", "--from-scratch", tmp_path, configs / "valid" / "minimal") with pytest.raises(ValidationError, match="modified without updating version"): - run_cli("validate", tmp_path, configs / "invalid" / "modified-no-allow-updates") + run_cli( + "validate", tmp_path, configs / "invalid" / "modified-without-allow-updates" + ) def test_validate_rejects_removal_without_deprecation( @@ -42,7 +44,9 @@ def test_validate_rejects_removal_without_deprecation( # deprecating it (and without --allow-all): this must be rejected. run_cli("sync", "--from-scratch", tmp_path, configs / "valid" / "minimal") with pytest.raises(ValidationError, match="removed without prior deprecation"): - run_cli("validate", tmp_path, configs / "invalid" / "removed-no-deprecation") + run_cli( + "validate", tmp_path, configs / "invalid" / "removed-without-deprecation" + ) def test_validate_rejects_added_deprecated_module( @@ -53,7 +57,7 @@ def test_validate_rejects_added_deprecated_module( # is only allowed with --allow-all, so a normal validate must reject it. run_cli("sync", "--from-scratch", tmp_path, configs / "valid" / "minimal") with pytest.raises(ValidationError, match="cannot have deprecated status"): - run_cli("validate", tmp_path, configs / "invalid" / "added-deprecated") + run_cli("validate", tmp_path, configs / "invalid" / "deprecated-on-creation") def test_validate_allows_added_deprecated_module_with_allow_all( @@ -63,7 +67,10 @@ def test_validate_allows_added_deprecated_module_with_allow_all( # release on initial creation. run_cli("sync", "--from-scratch", tmp_path, configs / "valid" / "minimal") run_cli( - "validate", "--allow-all", tmp_path, configs / "invalid" / "added-deprecated" + "validate", + "--allow-all", + tmp_path, + configs / "invalid" / "deprecated-on-creation", ) From 270ab73964ef4bb1decdf9d5f81bff12cdc2277b Mon Sep 17 00:00:00 2001 From: Martin Gaughran Date: Fri, 19 Jun 2026 17:44:53 +0000 Subject: [PATCH 06/12] Reduce irrelevant parameters in configuration --- .../example-module-shell/1.0.yaml | 6 +----- .../deprecated-on-creation/example-module-shell/1.0.yaml | 6 +----- .../deprecated-on-creation/example-module-shell/2.0.yaml | 8 +------- .../example-module-shell/1.0.yaml | 8 +++----- .../no-eligible-default/example-module-shell/1.0.yaml | 6 +----- tests/configs/valid/minimal/example-module-shell/1.0.yaml | 6 +----- 6 files changed, 8 insertions(+), 32 deletions(-) diff --git a/tests/configs/invalid/default-version-not-deployed/example-module-shell/1.0.yaml b/tests/configs/invalid/default-version-not-deployed/example-module-shell/1.0.yaml index e8f3c4a7..deda7448 100644 --- a/tests/configs/invalid/default-version-not-deployed/example-module-shell/1.0.yaml +++ b/tests/configs/invalid/default-version-not-deployed/example-module-shell/1.0.yaml @@ -5,13 +5,9 @@ module: version: "1.0" description: Minimal shell-only module used by the validate tests - env_vars: - - name: SHELL_VALUE - value: hello - applications: - app_type: shell name: test-shell-echo script: - - echo $SHELL_VALUE + - echo hello diff --git a/tests/configs/invalid/deprecated-on-creation/example-module-shell/1.0.yaml b/tests/configs/invalid/deprecated-on-creation/example-module-shell/1.0.yaml index e8f3c4a7..deda7448 100644 --- a/tests/configs/invalid/deprecated-on-creation/example-module-shell/1.0.yaml +++ b/tests/configs/invalid/deprecated-on-creation/example-module-shell/1.0.yaml @@ -5,13 +5,9 @@ module: version: "1.0" description: Minimal shell-only module used by the validate tests - env_vars: - - name: SHELL_VALUE - value: hello - applications: - app_type: shell name: test-shell-echo script: - - echo $SHELL_VALUE + - echo hello diff --git a/tests/configs/invalid/deprecated-on-creation/example-module-shell/2.0.yaml b/tests/configs/invalid/deprecated-on-creation/example-module-shell/2.0.yaml index c857eea9..ec135cff 100644 --- a/tests/configs/invalid/deprecated-on-creation/example-module-shell/2.0.yaml +++ b/tests/configs/invalid/deprecated-on-creation/example-module-shell/2.0.yaml @@ -5,17 +5,11 @@ module: version: "2.0" description: Minimal shell-only module used by the validate tests - env_vars: - - name: SHELL_VALUE - value: hello - applications: - app_type: shell name: test-shell-echo script: - - echo $SHELL_VALUE + - echo hello -# A brand-new release introduced directly in a deprecated state. This is only permitted -# with --allow-all (or a from-scratch deploy); a normal deploy must reject it. deprecated: true diff --git a/tests/configs/invalid/modified-without-allow-updates/example-module-shell/1.0.yaml b/tests/configs/invalid/modified-without-allow-updates/example-module-shell/1.0.yaml index 69c07817..7f3194e6 100644 --- a/tests/configs/invalid/modified-without-allow-updates/example-module-shell/1.0.yaml +++ b/tests/configs/invalid/modified-without-allow-updates/example-module-shell/1.0.yaml @@ -5,13 +5,11 @@ module: version: "1.0" description: Minimal shell-only module used by the validate tests - env_vars: - - name: SHELL_VALUE - value: changed - applications: - app_type: shell name: test-shell-echo + # Differs from the valid/minimal baseline (echo hello) so that deploying this over + # it is an in-place modification, rejected without allow_updates / a version bump. script: - - echo $SHELL_VALUE + - echo modified diff --git a/tests/configs/invalid/no-eligible-default/example-module-shell/1.0.yaml b/tests/configs/invalid/no-eligible-default/example-module-shell/1.0.yaml index 92da05df..77d40f52 100644 --- a/tests/configs/invalid/no-eligible-default/example-module-shell/1.0.yaml +++ b/tests/configs/invalid/no-eligible-default/example-module-shell/1.0.yaml @@ -5,15 +5,11 @@ module: version: "1.0" description: Shell module excluded from defaults with no eligible fallback - env_vars: - - name: SHELL_VALUE - value: hello - applications: - app_type: shell name: test-shell-echo script: - - echo $SHELL_VALUE + - echo hello exclude_from_defaults: true diff --git a/tests/configs/valid/minimal/example-module-shell/1.0.yaml b/tests/configs/valid/minimal/example-module-shell/1.0.yaml index e8f3c4a7..deda7448 100644 --- a/tests/configs/valid/minimal/example-module-shell/1.0.yaml +++ b/tests/configs/valid/minimal/example-module-shell/1.0.yaml @@ -5,13 +5,9 @@ module: version: "1.0" description: Minimal shell-only module used by the validate tests - env_vars: - - name: SHELL_VALUE - value: hello - applications: - app_type: shell name: test-shell-echo script: - - echo $SHELL_VALUE + - echo hello From dd2f5aaa5c3ec4d48505e2a1dfc0f0df016103e6 Mon Sep 17 00:00:00 2001 From: Martin Gaughran Date: Mon, 22 Jun 2026 10:40:38 +0000 Subject: [PATCH 07/12] Add tests for validation edge cases --- .../example-module-shell/1.0.yaml | 15 ++++++++++++ .../invalid-shell-script/settings.yaml | 3 +++ tests/test_validate.py | 24 +++++++++++++++++++ 3 files changed, 42 insertions(+) create mode 100644 tests/configs/invalid/invalid-shell-script/example-module-shell/1.0.yaml create mode 100644 tests/configs/invalid/invalid-shell-script/settings.yaml diff --git a/tests/configs/invalid/invalid-shell-script/example-module-shell/1.0.yaml b/tests/configs/invalid/invalid-shell-script/example-module-shell/1.0.yaml new file mode 100644 index 00000000..8b749cd2 --- /dev/null +++ b/tests/configs/invalid/invalid-shell-script/example-module-shell/1.0.yaml @@ -0,0 +1,15 @@ +# yaml-language-server: $schema=/workspaces/deploy-tools/src/deploy_tools/models/schemas/release.json + +module: + name: example-module-shell + version: "1.0" + description: Module whose shell script is not valid bash + + applications: + - app_type: shell + + name: test-shell-echo + # 'fi' with no matching 'if' is invalid bash, so `validate --test-build` must + # reject the generated entrypoint when it runs `bash -n` over it. + script: + - fi diff --git a/tests/configs/invalid/invalid-shell-script/settings.yaml b/tests/configs/invalid/invalid-shell-script/settings.yaml new file mode 100644 index 00000000..70f60017 --- /dev/null +++ b/tests/configs/invalid/invalid-shell-script/settings.yaml @@ -0,0 +1,3 @@ +# yaml-language-server: $schema=/workspaces/deploy-tools/src/deploy_tools/models/schemas/deployment-settings.json + +default_versions: {} diff --git a/tests/test_validate.py b/tests/test_validate.py index 6ddba61f..63df526d 100644 --- a/tests/test_validate.py +++ b/tests/test_validate.py @@ -84,3 +84,27 @@ def test_validate_test_build(tmp_path: Path, configs: Path) -> None: tmp_path, configs / "valid" / "minimal", ) + + +def test_validate_test_build_rejects_invalid_script( + tmp_path: Path, configs: Path +) -> None: + # --test-build runs `bash -n` over each generated shell entrypoint, so a script that + # is not valid bash must be rejected. + with pytest.raises(ValidationError, match="is invalid with errors"): + run_cli( + "validate", + "--test-build", + "--from-scratch", + tmp_path, + configs / "invalid" / "invalid-shell-script", + ) + + +def test_validate_reports_no_actions_when_unchanged( + tmp_path: Path, configs: Path +) -> None: + # Validating a config that is already fully deployed previews no changes. + run_cli("sync", "--from-scratch", tmp_path, configs / "valid" / "minimal") + output = run_cli("validate", tmp_path, configs / "valid" / "minimal") + assert "No release actions required" in output From 7f7380343f4cb6cb0eab546a07882fb1877324e7 Mon Sep 17 00:00:00 2001 From: Martin Gaughran Date: Mon, 6 Jul 2026 15:22:30 +0000 Subject: [PATCH 08/12] Use more specific pytest.raises matches for test_validate.py --- tests/test_validate.py | 35 ++++++++++++++++++++++++++++------- 1 file changed, 28 insertions(+), 7 deletions(-) diff --git a/tests/test_validate.py b/tests/test_validate.py index 63df526d..66715d9f 100644 --- a/tests/test_validate.py +++ b/tests/test_validate.py @@ -9,9 +9,19 @@ # area. Each maps a config folder under configs/invalid to a substring expected in the # resulting ValidationError. INVALID_INITIAL_CONFIGS = [ - ("unknown-dependency", "unknown module dependency"), - ("default-version-not-deployed", "Unable to configure"), - ("no-eligible-default", "exclude_from_defaults"), + ( + "unknown-dependency", + "Module example-module-dependent/1.0 has unknown module dependency " + "example-module-base/9.9", + ), + ( + "default-version-not-deployed", + "Unable to configure example-module-shell/2.0 as default", + ), + ( + "no-eligible-default", + "every version for name: example-module-shell has set exclude_from_defaults", + ), ] @@ -31,7 +41,10 @@ def test_validate_rejects_update_without_allow_updates( # Deploy a baseline whose module did not opt in to allow_updates, then validate a # config that changes that module in place: this must be rejected. run_cli("sync", "--from-scratch", tmp_path, configs / "valid" / "minimal") - with pytest.raises(ValidationError, match="modified without updating version"): + with pytest.raises( + ValidationError, + match="Module example-module-shell/1.0 modified without updating version", + ): run_cli( "validate", tmp_path, configs / "invalid" / "modified-without-allow-updates" ) @@ -43,7 +56,10 @@ def test_validate_rejects_removal_without_deprecation( # Deploy a baseline module, then validate a config that drops it without first # deprecating it (and without --allow-all): this must be rejected. run_cli("sync", "--from-scratch", tmp_path, configs / "valid" / "minimal") - with pytest.raises(ValidationError, match="removed without prior deprecation"): + with pytest.raises( + ValidationError, + match="Module example-module-shell/1.0 removed without prior deprecation", + ): run_cli( "validate", tmp_path, configs / "invalid" / "removed-without-deprecation" ) @@ -56,7 +72,10 @@ def test_validate_rejects_added_deprecated_module( # release already in a deprecated state. Deprecating a module on initial creation # is only allowed with --allow-all, so a normal validate must reject it. run_cli("sync", "--from-scratch", tmp_path, configs / "valid" / "minimal") - with pytest.raises(ValidationError, match="cannot have deprecated status"): + with pytest.raises( + ValidationError, + match="Module example-module-shell/2.0 cannot have deprecated status", + ): run_cli("validate", tmp_path, configs / "invalid" / "deprecated-on-creation") @@ -91,7 +110,9 @@ def test_validate_test_build_rejects_invalid_script( ) -> None: # --test-build runs `bash -n` over each generated shell entrypoint, so a script that # is not valid bash must be rejected. - with pytest.raises(ValidationError, match="is invalid with errors"): + with pytest.raises( + ValidationError, match="Output script .*/test-shell-echo is invalid with errors" + ): run_cli( "validate", "--test-build", From f67c14d21e5a3b6d1fb4387f5fb625bdedcf44ee Mon Sep 17 00:00:00 2001 From: Martin Gaughran Date: Wed, 8 Jul 2026 16:54:49 +0000 Subject: [PATCH 09/12] Reword test config descriptions to clarify use These configurations can now be shared between different tests where appropriate. --- .../default-version-not-deployed/example-module-shell/1.0.yaml | 2 +- .../deprecated-on-creation/example-module-shell/1.0.yaml | 2 +- .../deprecated-on-creation/example-module-shell/2.0.yaml | 2 +- .../example-module-shell/1.0.yaml | 2 +- tests/configs/valid/minimal/example-module-shell/1.0.yaml | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/configs/invalid/default-version-not-deployed/example-module-shell/1.0.yaml b/tests/configs/invalid/default-version-not-deployed/example-module-shell/1.0.yaml index deda7448..570bd2bf 100644 --- a/tests/configs/invalid/default-version-not-deployed/example-module-shell/1.0.yaml +++ b/tests/configs/invalid/default-version-not-deployed/example-module-shell/1.0.yaml @@ -3,7 +3,7 @@ module: name: example-module-shell version: "1.0" - description: Minimal shell-only module used by the validate tests + description: Minimal shell-only module with a single shell application applications: - app_type: shell diff --git a/tests/configs/invalid/deprecated-on-creation/example-module-shell/1.0.yaml b/tests/configs/invalid/deprecated-on-creation/example-module-shell/1.0.yaml index deda7448..570bd2bf 100644 --- a/tests/configs/invalid/deprecated-on-creation/example-module-shell/1.0.yaml +++ b/tests/configs/invalid/deprecated-on-creation/example-module-shell/1.0.yaml @@ -3,7 +3,7 @@ module: name: example-module-shell version: "1.0" - description: Minimal shell-only module used by the validate tests + description: Minimal shell-only module with a single shell application applications: - app_type: shell diff --git a/tests/configs/invalid/deprecated-on-creation/example-module-shell/2.0.yaml b/tests/configs/invalid/deprecated-on-creation/example-module-shell/2.0.yaml index ec135cff..ba509f0f 100644 --- a/tests/configs/invalid/deprecated-on-creation/example-module-shell/2.0.yaml +++ b/tests/configs/invalid/deprecated-on-creation/example-module-shell/2.0.yaml @@ -3,7 +3,7 @@ module: name: example-module-shell version: "2.0" - description: Minimal shell-only module used by the validate tests + description: Minimal shell-only module with a single shell application applications: - app_type: shell diff --git a/tests/configs/invalid/modified-without-allow-updates/example-module-shell/1.0.yaml b/tests/configs/invalid/modified-without-allow-updates/example-module-shell/1.0.yaml index 7f3194e6..c2771566 100644 --- a/tests/configs/invalid/modified-without-allow-updates/example-module-shell/1.0.yaml +++ b/tests/configs/invalid/modified-without-allow-updates/example-module-shell/1.0.yaml @@ -3,7 +3,7 @@ module: name: example-module-shell version: "1.0" - description: Minimal shell-only module used by the validate tests + description: Minimal shell-only module with a single shell application applications: - app_type: shell diff --git a/tests/configs/valid/minimal/example-module-shell/1.0.yaml b/tests/configs/valid/minimal/example-module-shell/1.0.yaml index deda7448..570bd2bf 100644 --- a/tests/configs/valid/minimal/example-module-shell/1.0.yaml +++ b/tests/configs/valid/minimal/example-module-shell/1.0.yaml @@ -3,7 +3,7 @@ module: name: example-module-shell version: "1.0" - description: Minimal shell-only module used by the validate tests + description: Minimal shell-only module with a single shell application applications: - app_type: shell From ec3034773d178799ea4d1e8ce7f7aba40b34cbd9 Mon Sep 17 00:00:00 2001 From: Martin Gaughran Date: Mon, 6 Jul 2026 15:26:16 +0000 Subject: [PATCH 10/12] Use more specific pytest.raises matches in test_default_versions.py --- tests/test_default_versions.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/tests/test_default_versions.py b/tests/test_default_versions.py index 2025ce6e..26850872 100644 --- a/tests/test_default_versions.py +++ b/tests/test_default_versions.py @@ -103,7 +103,7 @@ def test_explicit_default_for_nonexistent_version_raises() -> None: _release("mod", "1.0"), default_versions={"mod": "9.9"}, ) - with pytest.raises(ValidationError, match="Unable to configure"): + with pytest.raises(ValidationError, match="Unable to configure mod/9.9 as default"): validate_default_versions(deployment) @@ -114,7 +114,7 @@ def test_explicit_default_for_deprecated_version_raises() -> None: _release("mod", "1.0", deprecated=True), default_versions={"mod": "1.0"}, ) - with pytest.raises(ValidationError, match="Unable to configure"): + with pytest.raises(ValidationError, match="Unable to configure mod/1.0 as default"): validate_default_versions(deployment) @@ -122,5 +122,8 @@ def test_all_versions_excluded_raises() -> None: # With every version opting out and no explicit default, there is no eligible # version to make default. deployment = _deployment(_release("mod", "1.0", excluded=True)) - with pytest.raises(ValidationError, match="exclude_from_defaults"): + with pytest.raises( + ValidationError, + match="every version for name: mod has set exclude_from_defaults", + ): validate_default_versions(deployment) From 5fb0daeeb2de354801aa8f9a35638a63ec18f61d Mon Sep 17 00:00:00 2001 From: Martin Gaughran Date: Thu, 9 Jul 2026 11:15:24 +0000 Subject: [PATCH 11/12] Trim module docstring for test_default_versions.py --- tests/test_default_versions.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/test_default_versions.py b/tests/test_default_versions.py index 26850872..95dbbb79 100644 --- a/tests/test_default_versions.py +++ b/tests/test_default_versions.py @@ -1,9 +1,7 @@ """Unit tests for default-version selection. These call the function directly rather than through the CLI. Here that is to isolate -pure, non-trivial logic (e.g. natsort ordering of non-SemVer versions): a targeted unit -test states the outcome precisely and pins any regression to a line, rather than -surfacing it as an opaque golden-master diff. +pure, non-trivial logic (e.g. natsort ordering of non-SemVer versions). """ from collections import defaultdict From 05063502a6164b64ec9054b42e663af1d6d71170 Mon Sep 17 00:00:00 2001 From: Martin Gaughran Date: Thu, 9 Jul 2026 13:13:45 +0000 Subject: [PATCH 12/12] Make test build check explicit in test_validate.py The test build happens in a temporary directory, so it isn't easy to check the files exist directly. --- tests/test_validate.py | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/tests/test_validate.py b/tests/test_validate.py index 66715d9f..31566841 100644 --- a/tests/test_validate.py +++ b/tests/test_validate.py @@ -93,9 +93,10 @@ def test_validate_allows_added_deprecated_module_with_allow_all( ) -def test_validate_test_build(tmp_path: Path, configs: Path) -> None: - # --test-build actually builds the modules into a temporary area and syntax-checks - # the generated shell entrypoints, so exercise it on a valid shell-only config. +def test_validate_test_build_accepts_valid_config( + tmp_path: Path, configs: Path +) -> None: + # A valid shell-only config survives the build and `bash -n` check unchanged. run_cli( "validate", "--test-build", @@ -105,21 +106,20 @@ def test_validate_test_build(tmp_path: Path, configs: Path) -> None: ) -def test_validate_test_build_rejects_invalid_script( +def test_validate_test_build_catches_invalid_script( tmp_path: Path, configs: Path ) -> None: - # --test-build runs `bash -n` over each generated shell entrypoint, so a script that - # is not valid bash must be rejected. + # This config's entrypoint is not valid bash ('fi' with no 'if'). Only the build + # and `bash -n` check can catch it, so it passes without the --test-build flag and + # fails with it. This is proof the flag actually carries out the build. + config = configs / "invalid" / "invalid-shell-script" + + run_cli("validate", "--from-scratch", tmp_path, config) + with pytest.raises( ValidationError, match="Output script .*/test-shell-echo is invalid with errors" ): - run_cli( - "validate", - "--test-build", - "--from-scratch", - tmp_path, - configs / "invalid" / "invalid-shell-script", - ) + run_cli("validate", "--test-build", "--from-scratch", tmp_path, config) def test_validate_reports_no_actions_when_unchanged(