From 9234c781f7cba758c26a8c242810f051098d7574 Mon Sep 17 00:00:00 2001 From: Steve Manicdote Date: Wed, 22 Jul 2026 17:47:43 -0700 Subject: [PATCH] test: stop hardcoding the version literal in tests; enforce lockstep (#29) The per-plugin smoke tests and three stacks tests hardcoded the version string (`assert x.__version__ == "0.7.0"`), so every release turned the suite red until all nine were hand-edited -- a check that fails on every legitimate bump and passes on every wrong-but-consistent one. They now assert __version__ is well-formed semver, naming no value. The three redundant stacks version tests are deleted (smoke covers the property). tests/test_marketplace.py gains test_all_plugins_report_the_same_version, enforcing lockstep -- the guarantee the nine literals were reaching for -- without naming a version. Verified: bumping all twelve genuine carriers to a fake version keeps the suite green with zero test edits; desyncing one plugin turns the lockstep and agreement tests red. Co-Authored-By: Claude Opus 4.8 --- CHANGELOG.md | 10 +++ ...07-22-version-literal-delitteralization.md | 64 +++++++++++++++++++ plugins/ballast/tests/test_smoke.py | 6 +- plugins/ballast/tests/test_stacks.py | 5 -- plugins/bosun/tests/test_smoke.py | 6 +- plugins/hull/tests/test_smoke.py | 6 +- plugins/keel/tests/test_smoke.py | 7 +- plugins/rigging/tests/test_smoke.py | 6 +- plugins/rigging/tests/test_stacks.py | 5 -- plugins/stow/tests/test_smoke.py | 6 +- plugins/stow/tests/test_stacks.py | 6 -- tests/test_marketplace.py | 20 ++++++ 12 files changed, 118 insertions(+), 29 deletions(-) create mode 100644 docs/superpowers/plans/2026-07-22-version-literal-delitteralization.md diff --git a/CHANGELOG.md b/CHANGELOG.md index b6b0b58..fbb4f7f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,16 @@ and could stop an installed copy from updating. ## [Unreleased] +### Changed + +- Bumping the suite version no longer breaks the test suite. The per-plugin + smoke and stacks tests no longer hardcode the version string — they assert it + is well-formed semver — and a new `test_marketplace.py` check enforces + lockstep (all six plugins report the same version) without naming a value. + A release now edits only the twelve files that genuinely carry the version; + the fifteen meaningless release-time failures are gone, and lockstep — a + stated property of the suite — is enforced for the first time (#29). + ## [0.7.0] - 2026-07-22 ### Added diff --git a/docs/superpowers/plans/2026-07-22-version-literal-delitteralization.md b/docs/superpowers/plans/2026-07-22-version-literal-delitteralization.md new file mode 100644 index 0000000..4645c9a --- /dev/null +++ b/docs/superpowers/plans/2026-07-22-version-literal-delitteralization.md @@ -0,0 +1,64 @@ +# Kill the version-literal release tax (#29) — Implementation Plan + +> Small, mechanical, fully specified by issue #29. Executed inline (not subagent-driven). + +**Goal:** A version bump edits only the 12 files that genuinely carry the version (6 `plugin.json` + 6 `__init__.py`). No test file names the version; the suite verifies the version *property* (exists, semver, all six agree) instead of restating the value. + +**Principle:** `assert bosun.__version__ == "0.7.0"` cannot catch anything — it fails on every legitimate release and passes on every wrong-but-consistent one. Replace value-restatement with property checks, and add the one real guarantee nothing enforces today: lockstep (all six equal). + +## Global Constraints +- No test file contains a version literal after this change. +- Self-contained per plugin: each smoke test keeps its own small semver check (no cross-plugin import); the cross-plugin lockstep check lives in `tests/test_marketplace.py`, which already owns repo-level guards. +- Semver shape for this suite is `MAJOR.MINOR.PATCH` → `^\d+\.\d+\.\d+$`. +- Behaviour-only refactor of tests; no engine/config/rendered-artifact change. + +## Changes + +### 1. Five per-plugin `test_smoke.py` (hull, bosun, stow, rigging, ballast) +Each has, in `test_package_imports`, `assert .__version__ == "0.7.0"`, and in `test_plugin_json_parses_and_names_`, `assert plugin["version"] == "0.7.0"`. Replace both with property checks. Add `import re` where absent (rigging's smoke has no `re` import). + +- `test_package_imports`: + ```python + import + assert isinstance(.__version__, str) + assert re.fullmatch(r"\d+\.\d+\.\d+", .__version__), .__version__ + ``` +- plugin.json test: replace `assert plugin["version"] == "0.7.0"` with + ```python + assert re.fullmatch(r"\d+\.\d+\.\d+", plugin["version"]), plugin["version"] + ``` + (Agreement between plugin.json and `__init__.py` is already owned by + `test_marketplace.py::test_plugin_json_version_matches_the_package_version`, so the + local check only needs to assert the value is a well-formed version.) + +### 2. keel `test_smoke.py` +Remove the `VERSION = "0.7.0"` module constant. Change the two `== VERSION` assertions (in `test_package_imports` and `test_plugin_json_parses_and_names_keel`) to the same property checks as above. keel's smoke already imports nothing for regex — add `import re`. + +### 3. Three `test_stacks.py` version tests (rigging, ballast, stow) +`test_rigging_version` / `test_ballast_version` / `test_stow_version` each assert `x.__version__ == "0.7.0"` — an exact duplicate of the smoke check. **Delete these three functions.** (The property is covered once, in each plugin's smoke test.) + +### 4. `tests/test_marketplace.py` — the one new guarantee +Add a lockstep test that names no version: +```python +def test_all_plugins_report_the_same_version(): + """Lockstep is a stated property of this suite (see the changelog preamble) + and nothing enforced it. Reads every plugin's __init__ version and asserts + they are all equal, naming no literal -- so it passes at every consistent + release and fails only on a genuine desync.""" + import re + + versions = {} + for name in PLUGIN_DIRS: + init = (REPO / "plugins" / name / name / "__init__.py").read_text() + match = re.search(r'__version__\s*=\s*["\']([^"\']+)["\']', init) + assert match, f"{name}: no __version__ in {name}/__init__.py" + versions[name] = match.group(1) + distinct = set(versions.values()) + assert len(distinct) == 1, f"plugins are not in lockstep: {versions}" +``` + +## Verification +1. Full suite green after the edits. +2. `grep -rn '\d\.\d\.\d' plugins/*/tests/ tests/` returns no version literal in any test. +3. **Teeth check (not committed):** temporarily change one plugin's `__init__.py` version to a different value → confirm `test_all_plugins_report_the_same_version` AND `test_plugin_json_version_matches_the_package_version` go red → revert. +4. **Release-proof check (not committed):** temporarily bump ALL 12 carriers consistently to a fake version (e.g. 9.9.9) → confirm the whole suite stays green with zero test edits → revert. This is the property the change exists to deliver. diff --git a/plugins/ballast/tests/test_smoke.py b/plugins/ballast/tests/test_smoke.py index c1c394c..60281ef 100644 --- a/plugins/ballast/tests/test_smoke.py +++ b/plugins/ballast/tests/test_smoke.py @@ -4,6 +4,7 @@ from __future__ import annotations import json +import re from pathlib import Path REPO = Path(__file__).resolve().parents[3] @@ -13,13 +14,14 @@ def test_package_imports(): import ballast - assert ballast.__version__ == "0.7.0" + assert isinstance(ballast.__version__, str) + assert re.fullmatch(r"\d+\.\d+\.\d+", ballast.__version__), ballast.__version__ def test_plugin_json_parses_and_names_ballast(): plugin = json.loads((PLUGIN_ROOT / ".claude-plugin" / "plugin.json").read_text()) assert plugin["name"] == "ballast" - assert plugin["version"] == "0.7.0" + assert re.fullmatch(r"\d+\.\d+\.\d+", plugin["version"]), plugin["version"] def test_marketplace_lists_ballast(): diff --git a/plugins/ballast/tests/test_stacks.py b/plugins/ballast/tests/test_stacks.py index c6564ad..cd1f195 100644 --- a/plugins/ballast/tests/test_stacks.py +++ b/plugins/ballast/tests/test_stacks.py @@ -4,11 +4,6 @@ from ballast.stacks import REGISTRY, STACK_IDS, StackSpec -def test_ballast_version(): - import ballast - assert ballast.__version__ == "0.7.0" - - def test_registry_keys(): assert tuple(REGISTRY) == ("python",) diff --git a/plugins/bosun/tests/test_smoke.py b/plugins/bosun/tests/test_smoke.py index 5bdeedf..12e2a11 100644 --- a/plugins/bosun/tests/test_smoke.py +++ b/plugins/bosun/tests/test_smoke.py @@ -4,6 +4,7 @@ from __future__ import annotations import json +import re from pathlib import Path REPO = Path(__file__).resolve().parents[3] @@ -13,13 +14,14 @@ def test_package_imports(): import bosun - assert bosun.__version__ == "0.7.0" + assert isinstance(bosun.__version__, str) + assert re.fullmatch(r"\d+\.\d+\.\d+", bosun.__version__), bosun.__version__ def test_plugin_json_parses_and_names_bosun(): plugin = json.loads((PLUGIN_ROOT / ".claude-plugin" / "plugin.json").read_text()) assert plugin["name"] == "bosun" - assert plugin["version"] == "0.7.0" + assert re.fullmatch(r"\d+\.\d+\.\d+", plugin["version"]), plugin["version"] def test_marketplace_lists_bosun(): diff --git a/plugins/hull/tests/test_smoke.py b/plugins/hull/tests/test_smoke.py index 4786cd1..a95e722 100644 --- a/plugins/hull/tests/test_smoke.py +++ b/plugins/hull/tests/test_smoke.py @@ -4,6 +4,7 @@ from __future__ import annotations import json +import re from pathlib import Path REPO = Path(__file__).resolve().parents[3] @@ -13,13 +14,14 @@ def test_package_imports(): import hull - assert hull.__version__ == "0.7.0" + assert isinstance(hull.__version__, str) + assert re.fullmatch(r"\d+\.\d+\.\d+", hull.__version__), hull.__version__ def test_plugin_json_parses_and_names_hull(): plugin = json.loads((PLUGIN_ROOT / ".claude-plugin" / "plugin.json").read_text()) assert plugin["name"] == "hull" - assert plugin["version"] == "0.7.0" + assert re.fullmatch(r"\d+\.\d+\.\d+", plugin["version"]), plugin["version"] def test_marketplace_lists_hull(): diff --git a/plugins/keel/tests/test_smoke.py b/plugins/keel/tests/test_smoke.py index e66c7d2..557fef9 100644 --- a/plugins/keel/tests/test_smoke.py +++ b/plugins/keel/tests/test_smoke.py @@ -13,12 +13,12 @@ import importlib import importlib.util import json +import re from pathlib import Path REPO = Path(__file__).resolve().parents[3] PLUGIN_ROOT = Path(__file__).resolve().parents[1] -VERSION = "0.7.0" #: Every skill keel ships. Listed explicitly rather than globbed so that #: deleting a skill fails here instead of silently shrinking what's @@ -33,13 +33,14 @@ def test_package_imports(): import keel - assert keel.__version__ == VERSION + assert isinstance(keel.__version__, str) + assert re.fullmatch(r"\d+\.\d+\.\d+", keel.__version__), keel.__version__ def test_plugin_json_parses_and_names_keel(): plugin = json.loads((PLUGIN_ROOT / ".claude-plugin" / "plugin.json").read_text()) assert plugin["name"] == "keel" - assert plugin["version"] == VERSION + assert re.fullmatch(r"\d+\.\d+\.\d+", plugin["version"]), plugin["version"] def test_marketplace_lists_keel(): diff --git a/plugins/rigging/tests/test_smoke.py b/plugins/rigging/tests/test_smoke.py index 174492f..136814e 100644 --- a/plugins/rigging/tests/test_smoke.py +++ b/plugins/rigging/tests/test_smoke.py @@ -4,6 +4,7 @@ from __future__ import annotations import json +import re from pathlib import Path REPO = Path(__file__).resolve().parents[3] @@ -13,13 +14,14 @@ def test_package_imports(): import rigging - assert rigging.__version__ == "0.7.0" + assert isinstance(rigging.__version__, str) + assert re.fullmatch(r"\d+\.\d+\.\d+", rigging.__version__), rigging.__version__ def test_plugin_json_parses_and_names_rigging(): plugin = json.loads((PLUGIN_ROOT / ".claude-plugin" / "plugin.json").read_text()) assert plugin["name"] == "rigging" - assert plugin["version"] == "0.7.0" + assert re.fullmatch(r"\d+\.\d+\.\d+", plugin["version"]), plugin["version"] def test_marketplace_lists_rigging(): diff --git a/plugins/rigging/tests/test_stacks.py b/plugins/rigging/tests/test_stacks.py index cf6f782..f8c6a6c 100644 --- a/plugins/rigging/tests/test_stacks.py +++ b/plugins/rigging/tests/test_stacks.py @@ -5,11 +5,6 @@ from rigging.stacks import REGISTRY, STACK_IDS, Step, StackSpec -def test_rigging_version(): - import rigging - assert rigging.__version__ == "0.7.0" - - def test_registry_keys(): assert tuple(REGISTRY) == ("python", "node") diff --git a/plugins/stow/tests/test_smoke.py b/plugins/stow/tests/test_smoke.py index 0828828..32993f9 100644 --- a/plugins/stow/tests/test_smoke.py +++ b/plugins/stow/tests/test_smoke.py @@ -4,6 +4,7 @@ from __future__ import annotations import json +import re from pathlib import Path REPO = Path(__file__).resolve().parents[3] @@ -13,13 +14,14 @@ def test_package_imports(): import stow - assert stow.__version__ == "0.7.0" + assert isinstance(stow.__version__, str) + assert re.fullmatch(r"\d+\.\d+\.\d+", stow.__version__), stow.__version__ def test_plugin_json_parses_and_names_stow(): plugin = json.loads((PLUGIN_ROOT / ".claude-plugin" / "plugin.json").read_text()) assert plugin["name"] == "stow" - assert plugin["version"] == "0.7.0" + assert re.fullmatch(r"\d+\.\d+\.\d+", plugin["version"]), plugin["version"] def test_marketplace_lists_stow(): diff --git a/plugins/stow/tests/test_stacks.py b/plugins/stow/tests/test_stacks.py index bcbc136..ddc8902 100644 --- a/plugins/stow/tests/test_stacks.py +++ b/plugins/stow/tests/test_stacks.py @@ -6,12 +6,6 @@ from stow.stacks import BASE, REGISTRY, STACK_IDS, StackSpec -def test_stow_version(): - import stow - - assert stow.__version__ == "0.7.0" - - def test_registry_keys(): assert tuple(REGISTRY) == ("python", "node") diff --git a/tests/test_marketplace.py b/tests/test_marketplace.py index 8561fd2..d70a098 100644 --- a/tests/test_marketplace.py +++ b/tests/test_marketplace.py @@ -56,6 +56,26 @@ def test_plugin_json_version_matches_the_package_version(): ) +def test_all_plugins_report_the_same_version(): + """Lockstep is a stated property of this suite (see the changelog preamble) + and nothing enforced it -- the nine hardcoded version literals the smoke + tests used to carry were reaching for this check without ever making it. + Reads every plugin's __init__ version and asserts they are all equal, + naming no literal, so it passes at every consistent release and fails only + on a genuine desync. + """ + import re + + versions = {} + for name in PLUGIN_DIRS: + init = (REPO / "plugins" / name / name / "__init__.py").read_text() + match = re.search(r'__version__\s*=\s*["\']([^"\']+)["\']', init) + assert match, f"{name}: no __version__ in {name}/__init__.py" + versions[name] = match.group(1) + distinct = set(versions.values()) + assert len(distinct) == 1, f"plugins are not in lockstep: {versions}" + + def test_every_plugin_ships_at_least_one_skill(): for name in PLUGIN_DIRS: skills = sorted((REPO / "plugins" / name / "skills").glob("*/SKILL.md"))