Files
chemenu/tools/chemenu/tests/test_docs_verify.py
T
torben 26e1018766
CI / verify (push) Failing after 1m11s
Release / release (push) Successful in 37s
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
2026-09-26 07:53:10 +02:00

901 lines
39 KiB
Python

import pytest
import typer
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_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():
commands = docs_verify.registered_commands()
assert "new" in commands
assert "touch" in commands
assert "xref add" in commands
assert "migrate verify" in commands
assert "docs verify" in commands
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_a_groups_entry_with_no_registered_command_is_reported(monkeypatch):
monkeypatch.setattr(docs_verify, "registered_commands", lambda: set())
issues = docs_verify.check_command_contracts()
assert any("is not a registered wikitool command" 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)
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(
cli_contract, "all_records", lambda: {"frobnicate": _fixture_record("frobnicate")}
)
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():
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_development_md_is_checked_too(tmp_path, monkeypatch):
"""DEVELOPMENT.md drifted exactly this way once (Gitea #47): a table
describing what each verify command checks, removed by hand because nothing
compared it to anything."""
root = tmp_path
(root / "DEVELOPMENT.md").write_text(
"| Command | Purpose |\n| `docs verify` | checks docs |\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("`docs verify`" in issue for issue in issues)
def test_an_absent_listed_doc_is_skipped_not_reported(tmp_path, monkeypatch):
"""The distributed-instance case: DEVELOPMENT.md is not shipped, so listing
it must stay inert where the file does not exist rather than failing a tree
that is correct."""
root = tmp_path
monkeypatch.setattr(docs_verify.config, "ROOT", root)
monkeypatch.setattr(docs_verify, "ROOT_README", root / "README.md")
assert docs_verify.check_readmes_have_no_command_table() == []
def test_this_repos_type_specs_validate_against_their_own_schema():
"""Regression guard for Gitea #105: types/type-spec.schema.yaml declared
`additionalProperties: false` while real type-specs already carried
`root:` and `capture_fields:`, and nothing ever validated a type-spec's
own frontmatter against it - so the mismatch shipped silently."""
assert docs_verify.check_type_spec_frontmatter() == []
def test_an_unknown_type_spec_field_is_reported(tmp_path, monkeypatch):
"""Once the schema is enforced, a type-spec frontmatter field its own
schema does not know about must fail loudly rather than validating
silently - the other direction of the #105 regression guard above."""
types_dir = tmp_path / "types"
types_dir.mkdir()
(types_dir / "type-spec.md").write_text(
"---\n"
"type: types/type-spec.md\n"
"name: type-spec\n"
"description: Authoring and validation contract for type specs\n"
"schema: types/type-spec.schema.yaml\n"
"---\n",
encoding="utf-8",
)
(types_dir / "type-spec.schema.yaml").write_text(
(config.ROOT / "types" / "type-spec.schema.yaml").read_text(encoding="utf-8"),
encoding="utf-8",
)
(types_dir / "widget.md").write_text(
"---\n"
"type: types/type-spec.md\n"
"name: widget\n"
"description: A type-spec with a field its own schema does not know\n"
"schema: null\n"
"not_a_real_field: true\n"
"---\n",
encoding="utf-8",
)
monkeypatch.setattr(config, "ROOT", tmp_path)
monkeypatch.setattr(config, "TYPES_DIR", types_dir)
monkeypatch.setattr(type_resolver, "resolver", TypeResolver(repo_root=tmp_path))
issues = docs_verify.check_type_spec_frontmatter()
assert any("widget.md" in issue and "not_a_real_field" in issue for issue in issues)
def _stack_required_type_tree(tmp_path, monkeypatch, project_md: str = "", project_schema: str = ""):
"""A `types/` fixture carrying a valid `source` type-spec (copied from the
real repo, so it never drifts from what `docs verify` actually enforces)
plus whatever `project.md`/`project.schema.yaml` the caller supplies -
empty strings mean "write nothing", so a caller can exercise the
type-missing case. `check_stack_required_types()` (Gitea #123) has no
dedicated coverage otherwise: it is only ever exercised indirectly, via a
full `verify()` run against the real repo tree."""
types_dir = tmp_path / "types"
types_dir.mkdir()
for name in ("type-spec.md", "type-spec.schema.yaml", "source.md", "source.schema.yaml"):
(types_dir / name).write_text(
(config.ROOT / "types" / name).read_text(encoding="utf-8"), encoding="utf-8"
)
if project_md:
(types_dir / "project.md").write_text(project_md, encoding="utf-8")
if project_schema:
(types_dir / "project.schema.yaml").write_text(project_schema, encoding="utf-8")
monkeypatch.setattr(config, "ROOT", tmp_path)
monkeypatch.setattr(config, "TYPES_DIR", types_dir)
monkeypatch.setattr(type_resolver, "resolver", TypeResolver(repo_root=tmp_path))
_PROJECT_MD = (
"---\n"
"type: types/type-spec.md\n"
"name: project\n"
"description: Fixture project type\n"
"schema: types/project.schema.yaml\n"
"---\n"
)
def test_check_stack_required_types_reports_a_missing_project_type(tmp_path, monkeypatch):
"""No `project.md` at all - the type-missing case, worded to name the
type that is missing."""
_stack_required_type_tree(tmp_path, monkeypatch)
issues = docs_verify.check_stack_required_types()
assert any("name: project" in issue for issue in issues)
def test_check_stack_required_types_reports_project_schema_missing_state(tmp_path, monkeypatch):
"""A `project` type-spec exists, but its schema does not require `state:`
- the field-missing case, distinct from the type-missing one above."""
schema = (
"type: object\n"
"properties:\n"
" type: {type: string, const: 'types/project.md'}\n"
" state: {type: string, enum: [active, dormant, completed, abandoned]}\n"
"required: [type]\n"
"additionalProperties: false\n"
)
_stack_required_type_tree(tmp_path, monkeypatch, project_md=_PROJECT_MD, project_schema=schema)
issues = docs_verify.check_stack_required_types()
assert any("state" in issue and "project.md" in issue for issue in issues)
def test_check_stack_required_types_passes_when_both_types_satisfy_their_field(tmp_path, monkeypatch):
"""`source`/`raw_files:` and `project`/`state:` both satisfied - no issues."""
schema = (
"type: object\n"
"properties:\n"
" type: {type: string, const: 'types/project.md'}\n"
" state: {type: string, enum: [active, dormant, completed, abandoned]}\n"
"required: [type, state]\n"
"additionalProperties: false\n"
)
_stack_required_type_tree(tmp_path, monkeypatch, project_md=_PROJECT_MD, project_schema=schema)
assert docs_verify.check_stack_required_types() == []
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_incoming_inbox_is_ignored():
"""Unlike raw/, a file under incoming/ must never be committed - promotion
via `wikitool raw accept` is what makes it immutable, not the drop."""
assert docs_verify.ignored_canaries(("incoming/documents/probe.pdf",)) == [
"incoming/documents/probe.pdf"
]
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 = ""):
"""A changelog with `current` as the topmost entry and `previous` as the
last release beneath it. `current` is normally an open candidate
(`2.0.0-beta.1`) - the checks compare the newest entry against the **last
release** (`version_mod.last_release`), which skips right past a topmost
entry that is itself already a release (that one's crossing, if any, was
already checked while it was still the open candidate)."""
(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-beta.1", "1.4.0")
issues = docs_verify.check_migration_for_boundary()
assert any("2.0.0-beta.1" 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-beta.1", "1.4.0")
assert docs_verify.check_migration_for_boundary() == []
def test_a_fixed_release_is_never_re_checked_against_its_own_crossing(tmp_path, monkeypatch):
"""Regression for finding #2: comparing against the entry *beneath* the
newest one (rather than the last release) would find no boundary between
two betas of the same candidate - and would also, wrongly, re-flag an
already-fixed release forever. Once `current` is itself a release,
`last_release` returns it directly, so there is nothing left to compare."""
_boundary_tree(tmp_path, monkeypatch, "2.0.0", "1.4.0")
assert docs_verify.check_migration_for_boundary() == []
assert docs_verify.check_breaking_change_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-beta.1", "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):
"""The document targets the candidate's *base* (`2.0.0`), not its full
pre-release form - matching what `version bump` looks for."""
root = _boundary_tree(tmp_path, monkeypatch, "2.0.0-beta.1", "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-beta.1", "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-beta.1" 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-beta.1", "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-beta.1", "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_command_contracts", 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()
# --- issue references in shipped documents -----------------------------------
def test_every_reference_files_toc_is_current():
"""Forward direction, against the real tree: every file `toc.target_files()`
covers must already carry the region `wikitool docs toc --apply` would
write - this is what a session forgetting to re-run it after adding a
heading is caught by."""
assert docs_verify.check_toc_regions() == []
def test_a_shipped_template_over_the_threshold_without_a_region_is_reported(tmp_path, monkeypatch):
"""Regression guard for the defect this scope extension fixes: the template
an instance adopts was maintained by nothing and checked by nothing, so
`kb/CONVENTIONS.md.template` grew past the threshold carrying no region -
and every instance that adopted it got a `kb/CONVENTIONS.md` that fails
`docs verify` at the end of `setup-instance.md`, the one command that step
ends with. Before the template entered `toc.target_files()`, this check
returned nothing here."""
from chemenu import config, toc as toc_mod
monkeypatch.setattr(config, "ROOT", tmp_path)
(tmp_path / "kb").mkdir()
long_body = "# Conventions\n\nIntro.\n" + "".join(
f"\n## Section {i}\n\n" + "Body line.\n" * 12 for i in range(8)
)
assert toc_mod.needs_toc(long_body)
# The adopted file is current; only the template it was adopted from is not.
(tmp_path / "kb" / "CONVENTIONS.md").write_text(toc_mod.upsert(long_body), encoding="utf-8")
(tmp_path / "kb" / "CONVENTIONS.md.template").write_text(long_body, encoding="utf-8")
issues = docs_verify.check_toc_regions()
assert len(issues) == 1
assert "kb/CONVENTIONS.md.template" in issues[0]
assert "docs toc --apply" in issues[0]
def test_no_shipped_document_cites_an_issue():
"""Forward direction, against the real tree: a `#42` in a file `dist export`
ships points at a board only the origin repo has, and the reader of a
distributed instance can neither resolve it nor tell that it is
unresolvable."""
assert docs_verify.check_no_issue_references() == []
def test_a_dead_relative_link_is_reported(tmp_path, monkeypatch):
"""Regression guard for the bug this check exists to catch: a `../` count
wrong for the file's own depth is invisible to every other check - the
name it links to is real, the text renders, and nothing resolves the
target to notice it lands nowhere."""
fake = tmp_path / "example.md"
fake.write_text("See [tools/CONTRACT.md](../tools/CONTRACT.md) for the command table.\n", encoding="utf-8")
monkeypatch.setattr(docs_verify.toc, "target_files", lambda: [fake])
issues = docs_verify.check_reference_targets()
assert len(issues) == 1
assert "example.md:1" in issues[0]
assert "../tools/CONTRACT.md" in issues[0]
def test_a_resolving_relative_link_is_not_reported(tmp_path, monkeypatch):
(tmp_path / "tools").mkdir()
(tmp_path / "tools" / "CONTRACT.md").write_text("# Contract\n", encoding="utf-8")
fake = tmp_path / "example.md"
fake.write_text("See [tools/CONTRACT.md](tools/CONTRACT.md) for the command table.\n", encoding="utf-8")
monkeypatch.setattr(docs_verify.toc, "target_files", lambda: [fake])
assert docs_verify.check_reference_targets() == []
def test_an_absolute_url_is_not_resolved_as_a_path(tmp_path, monkeypatch):
fake = tmp_path / "example.md"
fake.write_text("See [Anthropic](https://www.anthropic.com).\n", encoding="utf-8")
monkeypatch.setattr(docs_verify.toc, "target_files", lambda: [fake])
assert docs_verify.check_reference_targets() == []
def test_a_section_anchor_is_stripped_before_resolving(tmp_path, monkeypatch):
"""CommonMark anchors are not filesystem paths - only the path part of
`target#anchor` is checked for existence."""
(tmp_path / "kb").mkdir()
(tmp_path / "kb" / "CONVENTIONS.md").write_text("## Tone\n", encoding="utf-8")
fake = tmp_path / "example.md"
fake.write_text("See [kb/CONVENTIONS.md § Tone](kb/CONVENTIONS.md#tone).\n", encoding="utf-8")
monkeypatch.setattr(docs_verify.toc, "target_files", lambda: [fake])
assert docs_verify.check_reference_targets() == []
def test_link_syntax_shown_as_an_example_in_a_fence_is_not_flagged(tmp_path, monkeypatch):
"""A passage documenting bad link syntax must not be mistaken for a real
reference - code fences are masked before scanning, mirroring `toc.py`."""
fake = tmp_path / "example.md"
fake.write_text(
"Do not write it like this:\n\n```markdown\n[gates.md](../nonexistent.md)\n```\n",
encoding="utf-8",
)
monkeypatch.setattr(docs_verify.toc, "target_files", lambda: [fake])
assert docs_verify.check_reference_targets() == []
def test_a_target_shipped_only_as_a_template_is_not_dead(tmp_path, monkeypatch):
"""Regression guard for a defect this check shipped with. A fresh
`dist export` carries `kb/CONVENTIONS.md.template`, not
`kb/CONVENTIONS.md` - the instance adopts it by renaming, during
`setup-instance.md`'s personalization step. `kb/CONTRACT.md` and three
flat instructions link to the adopted name, correctly. Before this
exemption the check reported 13 dead links on a just-exported tree, for
doing exactly what a fresh export is supposed to do."""
(tmp_path / "kb").mkdir()
(tmp_path / "kb" / "CONVENTIONS.md.template").write_text("# Conventions\n", encoding="utf-8")
fake = tmp_path / "kb" / "CONTRACT.md"
fake.write_text("What this instance decided: [kb/CONVENTIONS.md](CONVENTIONS.md).\n", encoding="utf-8")
monkeypatch.setattr(docs_verify.toc, "target_files", lambda: [fake])
assert docs_verify.check_reference_targets() == []
def test_a_target_with_neither_the_file_nor_a_template_is_still_dead(tmp_path, monkeypatch):
"""The exemption is narrow: it covers a file the stack ships as a
template, not any missing target."""
(tmp_path / "kb").mkdir()
fake = tmp_path / "kb" / "CONTRACT.md"
fake.write_text("See [kb/CONVENTIONS.md](CONVENTIONS.md).\n", encoding="utf-8")
monkeypatch.setattr(docs_verify.toc, "target_files", lambda: [fake])
issues = docs_verify.check_reference_targets()
assert len(issues) == 1
assert "CONVENTIONS.md" in issues[0]
def test_every_reference_files_link_targets_resolve():
"""Forward direction, against the real tree: every relative link in a file
`toc.target_files()` covers must resolve - this is what a `../` count
wrong for the file's own depth is caught by."""
assert docs_verify.check_reference_targets() == []
def test_verify_raises_when_a_reference_target_is_dead(monkeypatch):
monkeypatch.setattr(docs_verify, "check_reference_targets", lambda: ["dangling"])
with pytest.raises(typer.Exit):
docs_verify.verify()
def test_a_cited_issue_number_is_reported(monkeypatch):
monkeypatch.setattr(
docs_verify,
"shipped_prose",
lambda: {"instructions/example.md": "A rule.\nRemoved in Gitea #66.\n"},
)
issues = docs_verify.check_no_issue_references()
assert len(issues) == 1
assert "instructions/example.md:2" in issues[0]
assert "#66" in issues[0]
def test_a_markdown_anchor_is_not_an_issue_reference(monkeypatch):
"""An ordinary heading's slug is word characters, so it never collides
with `#<digits>` in the first place - this pins that the lookbehind
exclusion does not accidentally start matching it either."""
monkeypatch.setattr(
docs_verify,
"shipped_prose",
lambda: {
"AGENTS.md": (
"# Heading\n"
"See [Gates](#gates) and [Collections](kb/CONTRACT.md#collections).\n"
"Exit code 42 means a human must look.\n"
)
},
)
assert docs_verify.check_no_issue_references() == []
def test_a_numbered_heading_anchor_is_not_an_issue_reference(monkeypatch):
"""A table-of-contents entry for a numbered step (`toc.py`) anchors on
the number itself - `#2-fix-the-fidelity-before-writing-a-word` - unlike
an ordinary heading's slug, which starts with a letter. The bare
`#\\d+` pattern would flag that as citing issue #2; the lookbehind
excludes exactly the `](#...` link-fragment shape it appears in."""
monkeypatch.setattr(
docs_verify,
"shipped_prose",
lambda: {
"instructions/example.md": (
"# Heading\n"
"- [2. Fix the fidelity before writing a word](#2-fix-the-fidelity-before-writing-a-word)\n"
)
},
)
assert docs_verify.check_no_issue_references() == []
def test_a_parenthesized_issue_number_is_still_reported(monkeypatch):
"""The lookbehind excludes `](#...`, not bare `(#...` - a real citation
written as a plain parenthetical must still be caught."""
monkeypatch.setattr(
docs_verify,
"shipped_prose",
lambda: {"instructions/example.md": "A rule (#66).\n"},
)
issues = docs_verify.check_no_issue_references()
assert len(issues) == 1
assert "#66" in issues[0]
def test_python_source_is_out_of_scope():
"""The scope decision, pinned: `tools/` ships as runtime machinery, and a
code comment addresses whoever edits that line - which only ever happens in
the origin repo, because `dist export` prunes the `stack-dev` skill with the
rest of `instructions/dev/`. So a `.py` file is in the export plan and out
of the scanned set."""
plan = dist_cmd.build_plan()
scanned = docs_verify.shipped_prose()
assert "tools/chemenu/commands/raw_cmd.py" in plan
assert not [path for path in scanned if path.endswith(".py")]
# ...while the prose beside it is scanned, templates included.
assert "tools/CONTRACT.md" in scanned
assert "instructions/CONTRACT.md" in scanned
assert "types/source.schema.yaml.template" in scanned
def test_a_strip_marked_pointer_never_reaches_the_check():
"""The sanctioned way to keep a pointer that is worth having here and
meaningless anywhere else. `shipped_prose` reads the export plan, whose text
already has its marker regions removed, so a number inside a marker block is
present in the working tree and absent from what ships.
Asserted over whichever files carry a marker today rather than a named one,
so retiring any single passage does not fail this for an unrelated reason -
what is pinned is that the mechanism works, not where it is used.
"""
shipped = docs_verify.shipped_prose()
# Destination paths only coincide with source paths where nothing was
# re-keyed (`types/*.md` ships as `.template`), so the ones that do not
# exist in the tree are simply not this test's subject.
sources = {
path: source.read_text(encoding="utf-8")
for path in shipped
if (source := config.ROOT / path).is_file()
}
hidden = {
path: source
for path, source in sources.items()
if "dist:strip-start" in source and docs_verify.ISSUE_REFERENCE_RE.search(source)
}
assert hidden, "no shipped document keeps an issue number behind a strip marker"
for path, source in hidden.items():
assert docs_verify.ISSUE_REFERENCE_RE.search(source)
assert not docs_verify.ISSUE_REFERENCE_RE.search(shipped[path]), path
def test_verify_raises_when_a_shipped_document_cites_an_issue(monkeypatch):
monkeypatch.setattr(docs_verify, "check_no_issue_references", lambda: ["cited"])
with pytest.raises(typer.Exit):
docs_verify.verify()