tools: one data record per command - -h, index and CONTRACT.md render from cli_contract (#121)
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:
1 parent
919e733e21
commit
26e1018766
39 files changed
+5203
-702
No files matched your search
@@ -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()
|
||||
|
||||
|
||||
Reference in new issue
Block a user