tools: one data record per command - -h, index and CONTRACT.md render from cli_contract (#121)
CI / verify (push) Failing after 1m11s
Release / release (push) Successful in 37s

Files changed:
- AGENTS.md
- CHANGES.md
- VERSION
- instructions/dev/doc-pull-through.md
- instructions/dev/stack-close/SKILL.md
- tools/CONTRACT.md
- tools/README.md
- tools/chemenu/cli.py
- tools/chemenu/cli_contract.py
- tools/chemenu/commands/cite_cmd.py
- tools/chemenu/commands/dist_cmd.py
- tools/chemenu/commands/docs_verify.py
- tools/chemenu/commands/doctor.py
- tools/chemenu/commands/eval_cmd.py
- tools/chemenu/commands/git_publish.py
- tools/chemenu/commands/index_build.py
- tools/chemenu/commands/instructions_cmd.py
- tools/chemenu/commands/links_cmd.py
- tools/chemenu/commands/lint.py
- tools/chemenu/commands/log_append.py
- tools/chemenu/commands/migrate_cmd.py
- tools/chemenu/commands/new_page.py
- tools/chemenu/commands/page_ops.py
- tools/chemenu/commands/provenance_cmd.py
- tools/chemenu/commands/raw_cmd.py
- tools/chemenu/commands/review_cmd.py
- tools/chemenu/commands/run_budget.py
- tools/chemenu/commands/search.py
- tools/chemenu/commands/task_cmd.py
- tools/chemenu/commands/touch.py
- tools/chemenu/commands/types_cmd.py
- tools/chemenu/commands/upload_cmd.py
- tools/chemenu/commands/upstream_cmd.py
- tools/chemenu/commands/version_cmd.py
- tools/chemenu/commands/work_cmd.py
- tools/chemenu/commands/xref.py
- tools/chemenu/tests/test_cli_contract.py
- tools/chemenu/tests/test_docs_verify.py
- tools/chemenu/tests/test_run_budget.py
This commit is contained in:
torben committed 2026-09-26 07:53:10 +02:00
1 parent 919e733e21
commit 26e1018766
39 files changed
+5203 -702

No files matched your search

+198 -186
View File
@@ -1,15 +1,16 @@
import pytest
import typer
from chemenu import config, type_resolver
from chemenu import cli_contract, config, type_resolver
from chemenu.commands import dist_cmd, docs_verify
from chemenu.type_resolver import TypeResolver
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_every_registered_command_has_a_contract_record():
"""Forward direction: a command added to the CLI with no `@cli_contract.record`
(or missing from `cli_contract.GROUPS`) is exactly the drift this check
exists to catch (Gitea #121 B7)."""
assert docs_verify.check_command_contracts() == []
def test_registered_commands_include_groups_and_top_level():
@@ -21,197 +22,208 @@ def test_registered_commands_include_groups_and_top_level():
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_a_command_with_no_record_is_reported(monkeypatch):
monkeypatch.setattr(docs_verify, "registered_commands", lambda: {"new", "frobnicate"})
issues = docs_verify.check_command_contracts()
assert any("frobnicate" in issue and "no cli_contract record" in issue for issue in issues)
def test_documented_but_nonexistent_command_is_reported(monkeypatch):
def test_a_groups_entry_with_no_registered_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)
issues = docs_verify.check_command_contracts()
assert any("is not a registered 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(
"## 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)
def test_a_duplicate_groups_entry_is_reported(monkeypatch):
groups = (("Fixture", ("new", "new")),)
monkeypatch.setattr(cli_contract, "GROUPS", groups)
issues = docs_verify.check_command_contracts()
assert any("more than once" 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_a_section_runs_on_past_its_own_subheadings():
"""`###` groups inside a section must not end it. Both tables are split
into per-group subsections, each with its own table header, so a lookahead
that stopped at any `#` would truncate § Commands at its first group and
report every later command as undocumented."""
text = (
"## Commands\n\n"
"### Pages\n\n| `alpha` | does things |\n\n"
"### Git\n\n| `beta` | does other things |\n\n"
"## Error contracts\n\n| `gamma` | never | yes | retry |\n"
)
section = docs_verify.section_text(text, "## Commands")
assert docs_verify.documented_commands(section) == ["alpha", "beta"]
assert "gamma" not in section
def test_grouped_tables_are_still_checked_in_both_directions(tmp_path, monkeypatch):
"""The section split and the `###` grouping compose: each table is still
read as one pot of rows within its own section, however many subsections
and table headers it is broken into."""
fake = _fake_contract(
tmp_path,
commands_rows=(
"### Pages\n\n| Command | Purpose |\n| `alpha` | does things |\n\n"
"### Git\n\n| Command | Purpose |\n" # `beta`'s row was deleted here
),
error_rows=(
"### Pages\n\n| `alpha` | never | yes | retry |\n\n"
"### Git\n\n| `beta` | never | yes | retry |\n"
def _fixture_record(path: str) -> cli_contract.CommandRecord:
return cli_contract.CommandRecord(
path=path,
summary="Does a thing.",
synopsis=(cli_contract.Variant(usage=f"{path} --flag <v>"),),
properties=cli_contract.Properties(
effect=cli_contract.Effect.WRITE,
idempotent=cli_contract.Idempotent.NO,
atomic="Yes",
budget=cli_contract.Budget.COUNTED,
),
notes="Does a thing, mechanically.",
failures=(cli_contract.Failure(label="", exit_1="It broke", retry="Fix and retry"),),
)
def test_commands_region_matches_the_generated_form(tmp_path, monkeypatch):
"""Regression guard for the drift this check exists to catch: a region
hand-edited (or simply left stale after a record changed) must fail, the
same way `check_toc_regions` fails a stale table of contents."""
from chemenu import blocks
fake = tmp_path / "CONTRACT.md"
stale_region = (
blocks.open_marker("commands") + "\nstale content\n" + blocks.close_marker("commands")
)
fake.write_text(blocks.replace("## Commands\n", "commands", stale_region), encoding="utf-8")
monkeypatch.setattr(docs_verify, "CLI_README", fake)
monkeypatch.setattr(docs_verify, "registered_commands", lambda: {"alpha", "beta"})
issues = docs_verify.check_cli_readme()
assert any(
"beta" in issue and "§ Commands" in issue and "not documented" in issue
for issue in issues
monkeypatch.setattr(
cli_contract, "all_records", lambda: {"frobnicate": _fixture_record("frobnicate")}
)
assert not any("§ Error contracts" in issue for issue in issues)
monkeypatch.setattr(cli_contract, "GROUPS", (("Fixture", ("frobnicate",)),))
issues = docs_verify.check_commands_region()
assert any("stale" in issue for issue in issues)
def test_commands_region_is_accepted_once_regenerated(tmp_path, monkeypatch):
from chemenu import blocks
fake = tmp_path / "CONTRACT.md"
monkeypatch.setattr(docs_verify, "CLI_README", fake)
monkeypatch.setattr(
cli_contract, "all_records", lambda: {"frobnicate": _fixture_record("frobnicate")}
)
monkeypatch.setattr(cli_contract, "GROUPS", (("Fixture", ("frobnicate",)),))
content = cli_contract.render_commands_region(groups=(("Fixture", ("frobnicate",)),)).strip("\n")
wrapped = blocks.open_marker("commands") + "\n" + content + "\n" + blocks.close_marker("commands")
fake.write_text(blocks.replace("## Commands\n", "commands", wrapped), encoding="utf-8")
assert docs_verify.check_commands_region() == []
def test_a_missing_commands_region_is_reported(tmp_path, monkeypatch):
fake = tmp_path / "CONTRACT.md"
fake.write_text("## Commands\n\nnothing generated here yet.\n", encoding="utf-8")
monkeypatch.setattr(docs_verify, "CLI_README", fake)
issues = docs_verify.check_commands_region()
assert any("missing its" in issue for issue in issues)
def test_the_real_commands_region_is_current():
"""Forward direction against the real tree: this is what a session
forgetting to run `wikitool docs contract --apply` after changing a
record is caught by."""
assert docs_verify.check_commands_region() == []
def test_every_registered_commands_flags_are_documented():
"""Forward direction against the real tree and the real Click app: every
non-hidden flag of every command must appear in its cli_contract
record's SYNOPSIS, and vice versa."""
assert docs_verify.check_synopsis_flags() == []
def test_a_flag_the_synopsis_forgot_is_reported(monkeypatch):
import click
rec = _fixture_record("frobnicate")
monkeypatch.setattr(cli_contract, "get", lambda path: rec if path == "frobnicate" else None)
param = click.Option(["--flag"])
param2 = click.Option(["--forgotten"])
command = click.Command("frobnicate", params=[param, param2])
monkeypatch.setattr(
docs_verify, "_leaf_click_commands", lambda: [("frobnicate", command)]
)
issues = docs_verify.check_synopsis_flags()
assert any("--forgotten" in issue and "not documented" in issue for issue in issues)
def test_a_phantom_synopsis_flag_is_reported(monkeypatch):
import click
rec = cli_contract.CommandRecord(
path="frobnicate",
summary="Does a thing.",
synopsis=(cli_contract.Variant(usage="frobnicate --flag <v> --invented <v>"),),
properties=_fixture_record("frobnicate").properties,
notes="Does a thing.",
failures=(),
)
param = click.Option(["--flag"])
command = click.Command("frobnicate", params=[param])
monkeypatch.setattr(
docs_verify, "_leaf_click_commands", lambda: [("frobnicate", command)]
)
monkeypatch.setattr(cli_contract, "get", lambda path: rec if path == "frobnicate" else None)
issues = docs_verify.check_synopsis_flags()
assert any("--invented" in issue and "not a real flag" in issue for issue in issues)
def test_a_hidden_flag_need_not_be_documented(monkeypatch):
import click
rec = _fixture_record("frobnicate")
param = click.Option(["--flag"])
hidden = click.Option(["--secret"], hidden=True)
command = click.Command("frobnicate", params=[param, hidden])
monkeypatch.setattr(
docs_verify, "_leaf_click_commands", lambda: [("frobnicate", command)]
)
monkeypatch.setattr(cli_contract, "get", lambda path: rec if path == "frobnicate" else None)
assert docs_verify.check_synopsis_flags() == []
def test_a_boolean_flag_pair_is_satisfied_by_either_spelling(monkeypatch):
"""`--push`/`--no-push`: documenting only the negative spelling (the
existing SYNOPSIS convention `publish` used before this check existed)
must not be reported as an undocumented `--push`."""
import click
rec = cli_contract.CommandRecord(
path="frobnicate",
summary="Does a thing.",
synopsis=(cli_contract.Variant(usage="frobnicate [--no-push]"),),
properties=_fixture_record("frobnicate").properties,
notes="Does a thing.",
failures=(),
)
param = click.Option(["--push/--no-push"], default=True)
command = click.Command("frobnicate", params=[param])
monkeypatch.setattr(
docs_verify, "_leaf_click_commands", lambda: [("frobnicate", command)]
)
monkeypatch.setattr(cli_contract, "get", lambda path: rec if path == "frobnicate" else None)
assert docs_verify.check_synopsis_flags() == []
def test_no_registered_commands_help_cites_an_issue_number():
"""Forward direction against the real tree: a Gitea reference baked into
a docstring above its `\\f` marker, or into an Option's help text, reaches
a distributed instance with no tracker to resolve it against."""
assert docs_verify.check_no_issue_references_in_help() == []
def test_an_issue_number_above_the_form_feed_is_reported(monkeypatch):
import click
def frobnicate():
"""One sentence (Gitea #66)."""
command = click.Command("frobnicate", callback=frobnicate, help=frobnicate.__doc__)
monkeypatch.setattr(docs_verify, "_leaf_click_commands", lambda: [("frobnicate", command)])
issues = docs_verify.check_no_issue_references_in_help()
assert any("#66" in issue and "frobnicate" in issue for issue in issues)
def test_an_issue_number_below_the_form_feed_is_not_reported(monkeypatch):
import click
help_text = "One sentence.\n\f\nMaintenance text citing Gitea #66, never rendered."
command = click.Command("frobnicate", help=help_text)
monkeypatch.setattr(docs_verify, "_leaf_click_commands", lambda: [("frobnicate", command)])
assert docs_verify.check_no_issue_references_in_help() == []
def test_an_issue_number_in_an_options_help_text_is_reported(monkeypatch):
import click
param = click.Option(["--flag"], help="Do the thing (Gitea #66).")
command = click.Command("frobnicate", params=[param])
monkeypatch.setattr(docs_verify, "_leaf_click_commands", lambda: [("frobnicate", command)])
issues = docs_verify.check_no_issue_references_in_help()
assert any("#66" in issue and "--flag" in issue for issue in issues)
def test_collection_contracts_exist():
@@ -620,7 +632,7 @@ def test_verify_raises_when_the_version_is_undocumented(monkeypatch):
def test_verify_raises_when_issues_exist(monkeypatch):
monkeypatch.setattr(docs_verify, "check_cli_readme", lambda: ["boom"])
monkeypatch.setattr(docs_verify, "check_command_contracts", lambda: ["boom"])
with pytest.raises(typer.Exit):
docs_verify.verify()