Files
chemenu/tools/chemenu/tests/test_docs_verify.py
T
torben 31662dc3ff
CI / verify (push) Successful in 54s
Release / release (push) Successful in 37s
feat: Versionsstelle als Kompatibilitaetsfrage, Breaking-Change-Vermerk erzwungen (2.5.0)
Files changed:
- CHANGES.md
- INSTALL.md
- VERSION
- instructions/dev/stack-dev/SKILL.md
- instructions/dev/version-parts.md
- tools/CONTRACT.md
- tools/chemenu/commands/docs_verify.py
- tools/chemenu/commands/version_cmd.py
- tools/chemenu/tests/test_docs_verify.py
- tools/chemenu/tests/test_version_cmd.py
- tools/chemenu/version.py
2026-09-02 09:17:36 +02:00

299 lines
12 KiB
Python

import pytest
import typer
from chemenu.commands import docs_verify
def test_every_registered_command_is_documented():
"""Forward direction: a command added to the CLI without a README row is
exactly the drift this check exists to catch."""
assert docs_verify.check_cli_readme() == []
def test_registered_commands_include_groups_and_top_level():
commands = docs_verify.registered_commands()
assert "new" in commands
assert "touch" in commands
assert "xref add" in commands
assert "confidence init-base" in commands
assert "docs verify" in commands
def test_undocumented_command_is_reported(monkeypatch):
monkeypatch.setattr(
docs_verify, "registered_commands", lambda: {"new", "frobnicate"}
)
monkeypatch.setattr(docs_verify, "top_level_names", lambda: {"new", "frobnicate"})
issues = docs_verify.check_cli_readme()
assert any("frobnicate" in issue for issue in issues)
def test_documented_but_nonexistent_command_is_reported(monkeypatch):
monkeypatch.setattr(docs_verify, "registered_commands", lambda: set())
monkeypatch.setattr(docs_verify, "top_level_names", lambda: set())
issues = docs_verify.check_cli_readme()
assert any("is not a wikitool command" in issue for issue in issues)
def test_invented_subcommand_under_a_real_group_is_caught(tmp_path, monkeypatch):
"""Regression guard: checking only the first token (`xref`) let a typo'd
or invented subcommand sit undetected forever next to a real command
group. The reverse check must match the full registered path, not just
the top-level word."""
fake = tmp_path / "README.md"
fake.write_text("| `xref frobnicate --a X --b Y` | does not exist |\n", encoding="utf-8")
monkeypatch.setattr(docs_verify, "CLI_README", fake)
monkeypatch.setattr(docs_verify, "registered_commands", lambda: {"xref add", "xref remove"})
issues = docs_verify.check_cli_readme()
assert any("xref frobnicate" in issue for issue in issues)
def test_collection_contracts_exist():
assert docs_verify.check_collection_contracts() == []
def test_readmes_carry_no_command_table():
"""A derived copy is checked or absent: the command table is checked in
tools/CONTRACT.md, so no README may hold a second one."""
assert docs_verify.check_readmes_have_no_command_table() == []
def test_a_command_table_in_the_root_readme_is_reported(tmp_path, monkeypatch):
fake = tmp_path / "README.md"
fake.write_text("| Command | Purpose |\n| `lint` | does things |\n", encoding="utf-8")
monkeypatch.setattr(docs_verify, "ROOT_README", fake)
issues = docs_verify.check_readmes_have_no_command_table()
assert any("`lint`" in issue for issue in issues)
def test_non_command_tables_in_the_root_readme_are_allowed(tmp_path, monkeypatch):
fake = tmp_path / "README.md"
fake.write_text("| Skill | Purpose |\n| `wiki-ingest` | ingests |\n", encoding="utf-8")
monkeypatch.setattr(docs_verify, "ROOT_README", fake)
assert docs_verify.check_readmes_have_no_command_table() == []
def test_stage_readmes_are_checked_too(tmp_path, monkeypatch):
"""tools/README.md is the file the command table actually drifted in - a
stage README is allowed to exist, but not to hold a second copy."""
root = tmp_path
(root / "tools").mkdir()
(root / "tools" / "README.md").write_text(
"| Command | Purpose |\n| `publish` | pushes |\n", encoding="utf-8"
)
monkeypatch.setattr(docs_verify.config, "ROOT", root)
monkeypatch.setattr(docs_verify, "ROOT_README", root / "README.md")
issues = docs_verify.check_readmes_have_no_command_table()
assert any("`publish`" in issue for issue in issues)
def test_install_md_is_checked_too(tmp_path, monkeypatch):
"""INSTALL.md is human-facing prose about installing an instance - the
command reference lives exactly once, in tools/CONTRACT.md."""
root = tmp_path
(root / "INSTALL.md").write_text(
"| Command | Purpose |\n| `doctor` | checks things |\n", encoding="utf-8"
)
monkeypatch.setattr(docs_verify.config, "ROOT", root)
monkeypatch.setattr(docs_verify, "ROOT_README", root / "README.md") # doesn't exist here
issues = docs_verify.check_readmes_have_no_command_table()
assert any("`doctor`" in issue for issue in issues)
def test_legacy_type_blocks_are_absent():
assert docs_verify.check_legacy_type_blocks() == []
def test_legacy_type_regex_matches_pre_migration_form():
assert docs_verify.LEGACY_TYPE_RE.search("---\ntype: comparison\ntags: []\n---")
assert not docs_verify.LEGACY_TYPE_RE.search("---\ntype: types/comparison.md\n---")
def test_no_content_is_gitignored():
"""The regression guard for the 2026-08-13 `.gitignore` rewrite: patterns
like `*temp*` and `bin/` were silently excluding files under raw/, so the
wiki reported them as covered while `publish` never committed them."""
assert docs_verify.check_ignored_content() == []
def test_ignore_canaries_are_clear():
assert docs_verify.ignored_canaries() == []
def test_a_swallowed_canary_is_reported():
"""`tools/.wikitool_session/` is legitimately ignored, so it stands in for
a content path that a bad pattern would swallow."""
swallowed = docs_verify.ignored_canaries(("tools/.wikitool_session/budget.json",))
assert swallowed == ["tools/.wikitool_session/budget.json"]
def test_the_environment_note_is_ignored_but_its_template_is_not():
"""The pattern has to split a file from its own template. `ENVIRONMENT.md`
describes one checkout and must never be committed; `ENVIRONMENT.md.template`
is tracked machinery that `dist export` ships, and the careless pattern
(`ENVIRONMENT.md*`) would swallow both."""
assert docs_verify.ignored_canaries(("ENVIRONMENT.md",)) == ["ENVIRONMENT.md"]
assert docs_verify.ignored_canaries(("ENVIRONMENT.md.template",)) == []
def test_coverage_output_is_ignored():
"""`pytest --cov` writes into tools/, and `publish` runs `git add -A`."""
paths = ("tools/coverage.xml", "tools/htmlcov/index.html", "tools/.coverage")
assert docs_verify.ignored_canaries(paths) == list(paths)
def test_ignore_checks_degrade_when_git_is_unavailable(monkeypatch):
"""Without git the ignore rules are unknowable, not wrong - `docs verify`
must stay usable rather than reporting a false positive."""
monkeypatch.setattr(docs_verify, "_git", lambda *a, **k: None)
assert docs_verify.check_ignored_content() == []
def test_this_repos_version_and_changelog_agree():
assert docs_verify.check_version_changelog() == []
def _versioned_tree(tmp_path, monkeypatch, version: str, changes: str):
(tmp_path / "VERSION").write_text(version, encoding="utf-8")
(tmp_path / "CHANGES.md").write_text(changes, encoding="utf-8")
monkeypatch.setattr(docs_verify.config, "ROOT", tmp_path)
def test_a_bump_with_no_changelog_entry_is_reported(tmp_path, monkeypatch):
"""The check that gives `version bump` its teeth: a version raised with
nothing written about it would ship release notes describing the
previous release."""
_versioned_tree(tmp_path, monkeypatch, "0.2.0\n", "# Changelog\n\n## 0.1.0 - 2026-08-29 - Old\n")
issues = docs_verify.check_version_changelog()
assert any("0.2.0" in issue and "0.1.0" in issue for issue in issues)
def test_a_changelog_with_no_versioned_entry_is_accepted(tmp_path, monkeypatch):
"""A fresh distribution ships an empty changelog, and this repo's own
pre-versioning entries are dated rather than versioned. Neither claims to
describe the current version."""
_versioned_tree(
tmp_path, monkeypatch, "0.1.0\n", "# Changelog\n\n## 2026-08-01 - Before versioning\n"
)
assert docs_verify.check_version_changelog() == []
def test_a_missing_or_malformed_version_is_reported(tmp_path, monkeypatch):
monkeypatch.setattr(docs_verify.config, "ROOT", tmp_path)
assert any("VERSION" in issue for issue in docs_verify.check_version_changelog())
_versioned_tree(tmp_path, monkeypatch, "not-a-version\n", "# Changelog\n")
assert any("semantic version" in issue for issue in docs_verify.check_version_changelog())
def test_this_repos_boundary_is_accounted_for():
assert docs_verify.check_migration_for_boundary() == []
def _boundary_tree(tmp_path, monkeypatch, current: str, previous: str, marker: str = ""):
(tmp_path / "VERSION").write_text(f"{current}\n", encoding="utf-8")
(tmp_path / "CHANGES.md").write_text(
"# Changelog\n\n---\n\n"
f"## {current} - 2026-09-01 - New\n\n{marker}Body.\n\n---\n\n"
f"## {previous} - 2026-08-30 - Old\n\nBody.\n",
encoding="utf-8",
)
instructions = tmp_path / "instructions"
(instructions / "migrations").mkdir(parents=True, exist_ok=True)
monkeypatch.setattr(docs_verify.config, "ROOT", tmp_path)
monkeypatch.setattr(docs_verify.config, "INSTRUCTIONS_DIR", instructions)
return tmp_path
def test_a_breaking_release_without_a_migration_is_reported(tmp_path, monkeypatch):
"""`version check` tells an instance it must migrate; without this, that is
where the trail ends."""
_boundary_tree(tmp_path, monkeypatch, "2.0.0", "1.4.0")
issues = docs_verify.check_migration_for_boundary()
assert any("2.0.0" in issue and "must migrate" in issue for issue in issues)
def test_a_compatible_release_needs_no_migration(tmp_path, monkeypatch):
_boundary_tree(tmp_path, monkeypatch, "1.5.0", "1.4.0")
assert docs_verify.check_migration_for_boundary() == []
def test_an_explicit_none_required_marker_satisfies_the_check(tmp_path, monkeypatch):
from chemenu import version as version_mod
_boundary_tree(
tmp_path, monkeypatch, "2.0.0", "1.4.0",
marker=f"{version_mod.MIGRATION_NONE_MARKER} - nothing to change.\n\n",
)
assert docs_verify.check_migration_for_boundary() == []
def test_a_migration_document_satisfies_the_check(tmp_path, monkeypatch):
root = _boundary_tree(tmp_path, monkeypatch, "2.0.0", "1.4.0")
(root / "instructions" / "migrations" / "2.0.0-retype.md").write_text(
"---\ntype: types/instruction.md\nname: 2.0.0-retype\n"
"description: Retype.\nmanual: true\nmigrates_to: 2.0.0\n---\n",
encoding="utf-8",
)
assert docs_verify.check_migration_for_boundary() == []
def test_a_breaking_release_without_a_breaking_note_is_reported(tmp_path, monkeypatch):
"""A crossing that migrates nothing still leaves hand-work behind, so the
migration check passing is not evidence that anyone was told."""
from chemenu import version as version_mod
_boundary_tree(
tmp_path, monkeypatch, "2.0.0", "1.4.0",
marker=f"{version_mod.MIGRATION_NONE_MARKER} - nothing to change.\n\n",
)
assert docs_verify.check_migration_for_boundary() == []
issues = docs_verify.check_breaking_change_for_boundary()
assert any("2.0.0" in issue and "drop-in" in issue for issue in issues)
def test_a_compatible_release_needs_no_breaking_note(tmp_path, monkeypatch):
_boundary_tree(tmp_path, monkeypatch, "1.5.0", "1.4.0")
assert docs_verify.check_breaking_change_for_boundary() == []
def test_a_breaking_change_marker_satisfies_the_check(tmp_path, monkeypatch):
from chemenu import version as version_mod
_boundary_tree(
tmp_path, monkeypatch, "2.0.0", "1.4.0",
marker=f"{version_mod.BREAKING_CHANGE_MARKER} the feed moved.\n\n",
)
assert docs_verify.check_breaking_change_for_boundary() == []
def test_verify_raises_when_a_boundary_has_no_breaking_note(monkeypatch):
monkeypatch.setattr(
docs_verify, "check_breaking_change_for_boundary", lambda: ["unannounced"]
)
with pytest.raises(typer.Exit):
docs_verify.verify()
def test_verify_raises_when_a_boundary_has_no_migration(monkeypatch):
monkeypatch.setattr(docs_verify, "check_migration_for_boundary", lambda: ["unbridged"])
with pytest.raises(typer.Exit):
docs_verify.verify()
def test_verify_raises_when_the_version_is_undocumented(monkeypatch):
monkeypatch.setattr(docs_verify, "check_version_changelog", lambda: ["undocumented"])
with pytest.raises(typer.Exit):
docs_verify.verify()
def test_verify_raises_when_issues_exist(monkeypatch):
monkeypatch.setattr(docs_verify, "check_cli_readme", lambda: ["boom"])
with pytest.raises(typer.Exit):
docs_verify.verify()
def test_verify_raises_when_content_is_ignored(monkeypatch):
monkeypatch.setattr(docs_verify, "check_ignored_content", lambda: ["swallowed"])
with pytest.raises(typer.Exit):
docs_verify.verify()