Files
autocad-mcp-pro/tests/test_release_consistency.py
T
Umutcan EdizsalanandCursor e04c814cb8 docs(release): v1.4.0 - changelog, README live-benchmark section, CLAUDE.md sync, consistency gates
- CHANGELOG 1.4.0 section (infra + features), Unreleased now lists the 1.5 openers
- README: v1.4 snapshot (131 tools / 6 resources / 5 prompts / 474 tests), CI + PyPI badges, live-run benchmark table + chart, mcp-name registry marker
- CLAUDE.md: sections 15 (layouts) + 16 (solids), tool profiles, ISO 286 fits; counts synced to the live surface
- tests/test_release_consistency.py: locks pyproject/version.py/CHANGELOG/README/server.json versions, README counts vs registrations, section-header counts, Dockerfile COPY vs wheel includes, mcp-name marker

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-23 15:16:32 +03:00

161 lines
6.3 KiB
Python

"""Release-consistency gates (v1.4.0 Faz 0).
Locks the version string, the README release snapshot, the server.py section
headers, and the Dockerfile file selection to the actual state of the tree so
none of them can drift again:
- pyproject version == CHANGELOG top release == README snapshot major.minor
- version.py fallback parser returns the pyproject version
- README snapshot tool/resource/prompt counts == live @mcp.* decorator counts
- every ``SECTION n: ... (N tools)`` header matches the decorators below it
- every wheel ``only-include`` entry is COPY'd into the Docker image
"""
from __future__ import annotations
import re
import tomllib
from pathlib import Path
import pytest
ROOT = Path(__file__).resolve().parents[1]
def _pyproject() -> dict:
with (ROOT / "pyproject.toml").open("rb") as stream:
return tomllib.load(stream)
def _pyproject_version() -> str:
return str(_pyproject()["project"]["version"])
def _server_source() -> str:
return (ROOT / "server.py").read_text(encoding="utf-8")
SEMVER_RE = re.compile(r"^\d+\.\d+\.\d+$")
def test_pyproject_version_is_semver() -> None:
assert SEMVER_RE.match(_pyproject_version()), (
f"pyproject version {_pyproject_version()!r} is not MAJOR.MINOR.PATCH"
)
def test_version_py_fallback_matches_pyproject(monkeypatch: pytest.MonkeyPatch) -> None:
"""The pyproject fallback in version.py must resolve the same version.
The installed-metadata short circuit is forced to miss so the test checks
our parser (the code path a source checkout without an install uses).
"""
import version as version_module
def _raise(_name: str) -> str:
raise version_module.PackageNotFoundError
monkeypatch.setattr(version_module, "version", _raise)
assert version_module.package_version() == _pyproject_version()
def test_changelog_top_release_matches_pyproject() -> None:
changelog = (ROOT / "CHANGELOG.md").read_text(encoding="utf-8")
match = re.search(r"^## \[(\d+\.\d+\.\d+)\]", changelog, flags=re.MULTILINE)
assert match, "CHANGELOG.md has no '## [X.Y.Z]' release heading"
assert match.group(1) == _pyproject_version(), (
f"CHANGELOG top release {match.group(1)} != pyproject {_pyproject_version()}"
)
def _readme_snapshot() -> tuple[str, int, int, int]:
readme = (ROOT / "README.md").read_text(encoding="utf-8")
# The snapshot may wrap across blockquote lines; join continuations first.
flat = re.sub(r"\n>\s*", " ", readme)
match = re.search(
r"\*\*v(\d+\.\d+) release snapshot:\*\*\s*(\d+) tools\D+?(\d+) resources"
r"\D+?(\d+) prompt templates",
flat,
)
assert match, "README.md release-snapshot line not found or malformed"
return match.group(1), int(match.group(2)), int(match.group(3)), int(match.group(4))
def test_readme_snapshot_matches_pyproject_minor() -> None:
snapshot_minor, _, _, _ = _readme_snapshot()
expected = ".".join(_pyproject_version().split(".")[:2])
assert snapshot_minor == expected, (
f"README snapshot v{snapshot_minor} != pyproject major.minor v{expected}"
)
def test_readme_snapshot_counts_match_registrations() -> None:
src = _server_source()
tools = len(re.findall(r"@mcp\.tool\(", src))
resources = len(re.findall(r"@mcp\.resource\(", src))
prompts = len(re.findall(r"@mcp\.prompt\(", src))
_, readme_tools, readme_resources, readme_prompts = _readme_snapshot()
assert (readme_tools, readme_resources, readme_prompts) == (tools, resources, prompts), (
f"README snapshot says {readme_tools}/{readme_resources}/{readme_prompts} "
f"(tools/resources/prompts) but server.py registers {tools}/{resources}/{prompts}"
)
def test_section_header_counts_match_decorators() -> None:
src = _server_source()
headers = [
(m.start(), m.group(1), m.group(2))
for m in re.finditer(r"#[^\n]*SECTION ([\w]+):[^\n]*?\((\d+) tools?\)", src)
]
assert headers, "no counted SECTION headers found in server.py"
# Segment boundaries: every SECTION header (counted or not) ends a segment.
boundaries = sorted(m.start() for m in re.finditer(r"#[^\n]*SECTION [\w]+:", src))
boundaries.append(len(src))
mismatches: list[str] = []
for start, section_id, declared in headers:
end = min(b for b in boundaries if b > start)
actual = len(re.findall(r"@mcp\.tool\(", src[start:end]))
if actual != int(declared):
mismatches.append(f"SECTION {section_id}: header says {declared}, actual {actual}")
assert not mismatches, "; ".join(mismatches)
def test_server_json_versions_match_pyproject() -> None:
"""MCP registry manifest must ship the same version as the package."""
import json
manifest = json.loads((ROOT / "server.json").read_text(encoding="utf-8"))
expected = _pyproject_version()
assert manifest["version"] == expected, (
f"server.json version {manifest['version']} != pyproject {expected}"
)
for package in manifest["packages"]:
assert package["version"] == expected, (
f"server.json package {package['identifier']} version "
f"{package['version']} != pyproject {expected}"
)
if package["registryType"] == "pypi":
assert package["identifier"] == _pyproject()["project"]["name"]
def test_readme_contains_mcp_name_marker() -> None:
"""PyPI ownership validation for the MCP registry needs this marker in the
README (which ships to PyPI as the long description)."""
import json
manifest = json.loads((ROOT / "server.json").read_text(encoding="utf-8"))
readme = (ROOT / "README.md").read_text(encoding="utf-8")
assert f"mcp-name: {manifest['name']}" in readme
def test_dockerfile_copies_every_wheel_include() -> None:
"""The Docker image must contain everything the wheel ships (GH bug: the
image previously missed engineering/ and version.py)."""
dockerfile = (ROOT / "Dockerfile").read_text(encoding="utf-8")
copy_lines = "\n".join(
line for line in dockerfile.splitlines() if line.strip().upper().startswith("COPY")
)
includes = _pyproject()["tool"]["hatch"]["build"]["targets"]["wheel"]["only-include"]
missing = [name for name in includes if name not in copy_lines]
assert not missing, f"Dockerfile COPY misses wheel-shipped paths: {missing}"