stack: docs verify prueft § Commands und § Error contracts in tools/CONTRACT.md getrennt, 10 fehlende Fehlerkontrakt-Zeilen nachgetragen (schliesst #91)
CI / verify (push) Successful in 55s
Release / release (push) Successful in 35s

Files changed:
- CHANGES.md
- VERSION
- tools/CONTRACT.md
- tools/chemenu/commands/docs_verify.py
- tools/chemenu/tests/test_docs_verify.py
This commit is contained in:
torben committed 2026-09-11 12:19:56 +02:00
1 parent 441a8151ab
commit 203084477f
5 files changed
+266 -31

No files matched your search

+124 -1
View File
@@ -42,13 +42,136 @@ def test_invented_subcommand_under_a_real_group_is_caught(tmp_path, monkeypatch)
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")
fake.write_text(
"## Commands\n\n"
"| `xref frobnicate --a X --b Y` | does not exist |\n\n"
"## Error contracts\n\n"
"| `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)
# --- section-scoped § Commands vs. § Error contracts (Gitea #91) ------------
def test_section_text_extracts_between_headings():
text = "# T\n\n## A\n\nfoo\n\n## B\n\nbar\n"
assert docs_verify.section_text(text, "## A").strip() == "foo"
def test_section_text_extends_to_end_of_file_when_last():
text = "# T\n\n## A\n\nfoo\nbar\n"
assert docs_verify.section_text(text, "## A").strip() == "foo\nbar"
def test_section_text_raises_on_missing_heading():
with pytest.raises(ValueError):
docs_verify.section_text("# T\n\nno headings here\n", "## Commands")
def _fake_contract(tmp_path, commands_rows: str, error_rows: str):
fake = tmp_path / "CONTRACT.md"
fake.write_text(
f"## Commands\n\n{commands_rows}\n## Error contracts\n\n{error_rows}\n",
encoding="utf-8",
)
return fake
def test_a_row_deleted_from_commands_is_caught_even_if_error_contracts_still_has_it(
tmp_path, monkeypatch
):
"""Regression for the bug the section split fixes: before, a name
surviving in either table hid its own deletion from the other, so
§ Commands losing a row was invisible as long as § Error contracts still
named it."""
fake = _fake_contract(
tmp_path,
commands_rows="| Command | Purpose |\n", # `frobnicate`'s row was deleted here
error_rows="| `frobnicate` | never | yes | retry |\n",
)
monkeypatch.setattr(docs_verify, "CLI_README", fake)
monkeypatch.setattr(docs_verify, "registered_commands", lambda: {"frobnicate"})
issues = docs_verify.check_cli_readme()
assert any(
"frobnicate" in issue and "§ Commands" in issue and "not documented" in issue
for issue in issues
)
def test_error_contracts_is_enforced_against_registered_commands(tmp_path, monkeypatch):
"""Before the split, § Error contracts was never itself compared against
the registered commands - a row missing there was invisible."""
fake = _fake_contract(
tmp_path,
commands_rows="| `frobnicate` | does things |\n",
error_rows="| Command | Exit 1 means | Atomic? | Retry policy |\n",
)
monkeypatch.setattr(docs_verify, "CLI_README", fake)
monkeypatch.setattr(docs_verify, "registered_commands", lambda: {"frobnicate"})
issues = docs_verify.check_cli_readme()
assert any(
"frobnicate" in issue and "§ Error contracts" in issue and "not documented" in issue
for issue in issues
)
def test_a_phantom_error_contract_row_is_reported(tmp_path, monkeypatch):
"""The reverse direction inside § Error contracts: a row for a command
that does not exist must be reported there too, not only in § Commands."""
fake = _fake_contract(
tmp_path,
commands_rows="| `frobnicate` | does things |\n",
error_rows="| `frobnicate` | never | yes | retry |\n| `ghost command` | never happened | no | n/a |\n",
)
monkeypatch.setattr(docs_verify, "CLI_README", fake)
monkeypatch.setattr(docs_verify, "registered_commands", lambda: {"frobnicate"})
issues = docs_verify.check_cli_readme()
assert any(
"ghost command" in issue and "§ Error contracts" in issue and "not a wikitool command" in issue
for issue in issues
)
def test_a_renamed_commands_heading_is_reported_not_silently_scanned(tmp_path, monkeypatch):
"""A renamed or removed `## Commands` heading must fail loudly - falling
back to scanning the whole file would make the two tables indistinguishable
again, which is the exact bug this check exists to prevent."""
fake = tmp_path / "CONTRACT.md"
fake.write_text(
"## Kommandos\n\n"
"| `frobnicate` | does things |\n\n"
"## Error contracts\n\n"
"| `frobnicate` | never | yes | retry |\n",
encoding="utf-8",
)
monkeypatch.setattr(docs_verify, "CLI_README", fake)
monkeypatch.setattr(docs_verify, "registered_commands", lambda: {"frobnicate"})
issues = docs_verify.check_cli_readme()
assert any("no '## Commands' heading found" in issue for issue in issues)
assert not any("§ Commands" in issue and "not documented" in issue for issue in issues)
def test_a_renamed_error_contracts_heading_is_reported_not_silently_scanned(tmp_path, monkeypatch):
fake = tmp_path / "CONTRACT.md"
fake.write_text(
"## Commands\n\n"
"| `frobnicate` | does things |\n\n"
"## Fehlerkontrakte\n\n"
"| `frobnicate` | never | yes | retry |\n",
encoding="utf-8",
)
monkeypatch.setattr(docs_verify, "CLI_README", fake)
monkeypatch.setattr(docs_verify, "registered_commands", lambda: {"frobnicate"})
issues = docs_verify.check_cli_readme()
assert any("no '## Error contracts' heading found" in issue for issue in issues)
assert not any("§ Error contracts" in issue and "not documented" in issue for issue in issues)
def test_collection_contracts_exist():
assert docs_verify.check_collection_contracts() == []