Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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 <pkg>.__version__ == "0.7.0"`, and in `test_plugin_json_parses_and_names_<pkg>`, `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 <pkg>
assert isinstance(<pkg>.__version__, str)
assert re.fullmatch(r"\d+\.\d+\.\d+", <pkg>.__version__), <pkg>.__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.
6 changes: 4 additions & 2 deletions plugins/ballast/tests/test_smoke.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from __future__ import annotations

import json
import re
from pathlib import Path

REPO = Path(__file__).resolve().parents[3]
Expand All @@ -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():
Expand Down
5 changes: 0 additions & 5 deletions plugins/ballast/tests/test_stacks.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",)

Expand Down
6 changes: 4 additions & 2 deletions plugins/bosun/tests/test_smoke.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from __future__ import annotations

import json
import re
from pathlib import Path

REPO = Path(__file__).resolve().parents[3]
Expand All @@ -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():
Expand Down
6 changes: 4 additions & 2 deletions plugins/hull/tests/test_smoke.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from __future__ import annotations

import json
import re
from pathlib import Path

REPO = Path(__file__).resolve().parents[3]
Expand All @@ -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():
Expand Down
7 changes: 4 additions & 3 deletions plugins/keel/tests/test_smoke.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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():
Expand Down
6 changes: 4 additions & 2 deletions plugins/rigging/tests/test_smoke.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from __future__ import annotations

import json
import re
from pathlib import Path

REPO = Path(__file__).resolve().parents[3]
Expand All @@ -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():
Expand Down
5 changes: 0 additions & 5 deletions plugins/rigging/tests/test_stacks.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")

Expand Down
6 changes: 4 additions & 2 deletions plugins/stow/tests/test_smoke.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from __future__ import annotations

import json
import re
from pathlib import Path

REPO = Path(__file__).resolve().parents[3]
Expand All @@ -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():
Expand Down
6 changes: 0 additions & 6 deletions plugins/stow/tests/test_stacks.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")

Expand Down
20 changes: 20 additions & 0 deletions tests/test_marketplace.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"))
Expand Down