import pytest import typer from chemenu import 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_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_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_documented_but_nonexistent_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) 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) # --- 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" ), ) 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 ) assert not any("§ Error contracts" 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 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_cli_readme", 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_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 `#` 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()