From 50b783bd336a8d96fbd06afd258694a9d0e20aa5 Mon Sep 17 00:00:00 2001 From: Patrick <320190286+muellerei@users.noreply.github.com> Date: Tue, 15 Sep 2026 22:35:05 +0200 Subject: [PATCH] test: hold the README's command tables to the registry The twenty undocumented options were the symptom. The defect is that a table is a hand-maintained view of something derivable and nothing recomputed it: --help is generated and stays complete, so the gap never hurt enough to be noticed, and every later check read the table - which looks complete when you read it. Three checks: every option in the registry appears in the README in one of its forms, every command has a row, the section counters sum to the number of commands. It found a further defect on its first run: ### Edit claimed 11 where there are 8 commands, because insert-block occupies five rows. Mutation-checked three ways - removing a documented option, falsifying a counter, and adding an undocumented command each turn it red. --- CHANGELOG.md | 17 ++++ README.md | 2 +- tests/test_readme_documents_options.py | 109 +++++++++++++++++++++++++ 3 files changed, 127 insertions(+), 1 deletion(-) create mode 100644 tests/test_readme_documents_options.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 550bb5d..8aeb697 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- A test now holds the README's command tables to the command registry. The + twenty missing options below were not the defect — they were the symptom. The + defect is that a table is a hand-maintained view of something derivable, and + nothing recomputed it: `--help` is generated by Click and stays complete, so + the gap never hurt enough to be noticed, and every later check *read* the + table, which looks complete when you read it. + + The test compares instead: every option in the registry must appear in the + README in one of its forms, every command must have a row, and the section + counters must sum to the number of commands. It found one more defect on its + first run — `### Edit` claimed 11 where there are 8 commands, because + `insert-block` occupies five rows. Corrected. + + Same shape as the `--dry-run` coverage test added earlier in this release, + and for the same reason: the source is the registry, the document is a view, + and a view must not be able to disagree with its source. + - The README documented 20 options that the CLI accepts but never named — among them `--min-refs`, `--min-shared`, `--upsert-heading`, `--no-backlinks` and the `--date` of the three journal writers. Some of them decide what a diff --git a/README.md b/README.md index 3a22eb1..4dbd7c6 100644 --- a/README.md +++ b/README.md @@ -217,7 +217,7 @@ logseq-cli get-page --name "My Page" # equivalent | `add-journal-content --content TEXT [--date DATE]` | Add hierarchical content to journal (`--under-heading`, `--top-level`, `--dry-run`). `--date` defaults to today | | `add-note-content --page NAME --content TEXT [--under-heading "## X"] [--no-create] [--property K=V] [--dry-run]` | Add content to any page; optionally under a heading (created if missing). The page is created when missing unless `--no-create` is given. `--property` sets `key:: value` on the root block, repeatable. `--dry-run` reports the target, the block count and whether page or heading would be created | -### Edit (11) +### Edit (8) | Command | Description | |---------|-------------| diff --git a/tests/test_readme_documents_options.py b/tests/test_readme_documents_options.py new file mode 100644 index 0000000..e1575f2 --- /dev/null +++ b/tests/test_readme_documents_options.py @@ -0,0 +1,109 @@ +"""The README's command tables must not fall behind the command registry. + +Twenty options went undocumented for the life of the project — `--min-refs`, +`--min-shared`, `--upsert-heading`, `--no-backlinks` and the `--date` of the +three journal writers among them. Nineteen of those came in with the initial +import, which wrote the code and the first README in one go; the table was a +selection from the start and was read as a reference afterwards. + +Nothing caught it, for a reason worth naming: ``--help`` is generated by Click +and is always complete, so the gap never hurt anyone enough to be noticed, and +every later check *read* the table — which looks complete when you read it. +Only a comparison shows what is not in it. + +So this compares. The registry is the source, the README a view on it, and a +view must not be able to disagree with its source. +""" +import pathlib +import re + +import pytest + +from logseq_cli.cli import cli + + +README = pathlib.Path(__file__).resolve().parent.parent / "README.md" + +# Present on nearly every command and documented once, in prose, rather than +# repeated in forty table rows. +UBIQUITOUS = {"--json", "--help"} + + +def _readme_text(): + return README.read_text(encoding="utf-8") + + +def _documented_forms(param): + """Every spelling of an option a caller could type. + + A boolean flag has a positive and a negative form (``--create`` / + ``--no-create``), and the README lists the one a caller actually passes — + which for a default-on flag is the negative. Either spelling counts as + documented; requiring a specific one would document a flag nobody uses. + """ + forms = list(getattr(param, "opts", ())) + list(getattr(param, "secondary_opts", ())) + return [f for f in forms if f.startswith("--") and f not in UBIQUITOUS] + + +def _all_options(): + for name, command in sorted(cli.commands.items()): + for param in command.params: + forms = _documented_forms(param) + if forms: + yield name, forms + + +class TestEveryOptionIsDocumented: + def test_the_scan_sees_the_options(self): + """Guards the guard: a scan that finds nothing would pass silently.""" + found = {opt for _, forms in _all_options() for opt in forms} + for expected in ("--dry-run", "--min-refs", "--resolve-refs", "--limit"): + assert expected in found, ( + f"{expected} exists but the scan missed it — the detection is " + "broken, not the README" + ) + + def test_every_option_appears_in_the_readme(self): + text = _readme_text() + missing = [ + (command, "/".join(forms)) + for command, forms in _all_options() + if not any(form in text for form in forms) + ] + assert not missing, ( + "these options exist but the README never names them, in any of " + f"their forms: {missing}" + ) + + def test_the_ubiquitous_options_are_explained_somewhere(self): + """--json and --help are exempt above, so they must be covered in prose.""" + text = _readme_text() + assert "--json" in text, "--json is exempt from the table check and must be documented in prose" + + +class TestSectionCountersMatchTheRegistry: + """The `### Read (14)` counters are a hand-maintained view of the same list. + + They are the other half of the same defect: a number beside a table that + nothing recomputes. `delete-block` is a documented alias of `remove-block` + and shares its row, so it is not counted twice. + """ + + ALIASES = {"delete-block"} + + def test_counters_sum_to_the_number_of_commands(self): + counters = [int(n) for n in re.findall(r"^### [A-Za-z][A-Za-z ]* \((\d+)\)", _readme_text(), re.M)] + assert counters, "no section counters found — the README layout changed" + expected = len(set(cli.commands) - self.ALIASES) + assert sum(counters) == expected, ( + f"section counters sum to {sum(counters)} but the registry has " + f"{expected} commands (aliases excluded: {sorted(self.ALIASES)})" + ) + + def test_every_command_has_a_row(self): + text = _readme_text() + missing = [ + name for name in sorted(cli.commands) + if name not in self.ALIASES and f"`{name}" not in text + ] + assert not missing, f"commands with no README row: {missing}"