from __future__ import annotations import os import subprocess import sys from collections.abc import Callable, Iterator from pathlib import Path, PureWindowsPath import pytest PROJECT_ROOT = Path(__file__).resolve().parents[3] GATE_SCRIPT = PROJECT_ROOT / "scripts" / "check_documentation_integrity.py" sys.path.insert(0, str(PROJECT_ROOT)) import scripts.check_documentation_integrity as documentation_integrity from scripts.check_documentation_integrity import ( DocumentationIntegrityResult, check_documentation_integrity, ) REQUIRED_DOCUMENTS = ( "README.md", "app/README.md", "app/src/storage/README.md", "docs/architecture/overview.md", "docs/architecture/project_structure.md", "docs/architecture/dzentra_target_architecture.md", "docs/architecture/trades_feed.md", "docs/operations/trades_feed_runtime.md", "docs/roadmap/master-roadmap.md", "docs/migrations/build_060_30_architecture.md", "docs/migrations/build_060_20.md", "docs/migrations/build_060_20_architecture.md", "docs/migrations/build_060_20_1.md", "docs/migrations/build_060_20_1_architecture.md", "docs/migrations/build_060_21.md", "docs/migrations/build_060_21_architecture.md", "docs/migrations/build_060_22.md", "docs/migrations/build_060_22_architecture.md", "docs/migrations/build_060_23.md", "docs/migrations/build_060_23_architecture.md", "docs/migrations/build_060_24.md", "docs/migrations/build_060_24_architecture.md", "docs/migrations/build_060_25.md", "docs/migrations/build_060_25_architecture.md", "docs/migrations/build_060_26.md", "docs/migrations/build_060_26_architecture.md", "docs/migrations/build_060_27.md", "docs/migrations/build_060_27_architecture.md", "docs/migrations/build_060_28.md", "docs/migrations/build_060_28_architecture.md", "docs/migrations/build_060_29.md", "docs/migrations/build_060_29_architecture.md", ) def _run_gate( repository_root: Path, *, environment: dict[str, str] | None = None, timeout_seconds: float = 60.0, ) -> subprocess.CompletedProcess[str]: return subprocess.run( ( sys.executable, str(GATE_SCRIPT), "--repository-root", str(repository_root), ), cwd=PROJECT_ROOT, capture_output=True, text=True, check=False, timeout=timeout_seconds, env=environment, ) def _make_repository(tmp_path: Path) -> Path: repository_root = tmp_path / "repository" for relative_path in REQUIRED_DOCUMENTS: target = repository_root / relative_path target.parent.mkdir(parents=True, exist_ok=True) target.write_text(f"# {target.stem}\n", encoding="utf-8") (repository_root / "app" / "tools").mkdir(parents=True, exist_ok=True) return repository_root def _write_readme(repository_root: Path, content: str) -> None: (repository_root / "README.md").write_text(content, encoding="utf-8") def _combined_output(result: subprocess.CompletedProcess[str]) -> str: return "\n".join(part for part in (result.stdout, result.stderr) if part) def test_real_repository_documentation_is_clean() -> None: result = _run_gate(PROJECT_ROOT) assert result.returncode == 0, _combined_output(result) assert "issues=0" in result.stdout def test_public_api_supports_overrides_and_rejects_escaping_sources( tmp_path: Path, ) -> None: repository_root = _make_repository(tmp_path) result = check_documentation_integrity( repository_root, required_documents=("README.md",), markdown_sources=(Path("README.md"),), ) assert isinstance(result, DocumentationIntegrityResult) assert result.is_clean is True assert result.scanned_documents == 1 assert result.checked_links == 0 assert result.issues == () for source in (Path("../outside"), Path("docs/../../outside"), Path("/tmp")): with pytest.raises(ValueError, match="repository-relative"): check_documentation_integrity( repository_root, required_documents=(), markdown_sources=(source,), ) def test_missing_required_document_is_reported(tmp_path: Path) -> None: repository_root = _make_repository(tmp_path) (repository_root / "docs" / "architecture" / "trades_feed.md").unlink() result = _run_gate(repository_root) assert result.returncode == 1 assert "MANIFEST_MISSING" in result.stderr assert "docs/architecture/trades_feed.md" in result.stderr def test_required_document_must_be_regular_file(tmp_path: Path) -> None: repository_root = _make_repository(tmp_path) document = repository_root / "docs" / "architecture" / "trades_feed.md" document.unlink() document.mkdir() result = _run_gate(repository_root) assert result.returncode == 1 assert "MANIFEST_NOT_FILE" in result.stderr def test_required_document_cannot_be_symlink(tmp_path: Path) -> None: repository_root = _make_repository(tmp_path) document = repository_root / "docs" / "architecture" / "trades_feed.md" document.unlink() document.symlink_to("overview.md") result = _run_gate(repository_root) assert result.returncode == 1 assert "SYMLINK_TARGET" in result.stderr def test_existing_file_directory_and_parent_link_are_accepted(tmp_path: Path) -> None: repository_root = _make_repository(tmp_path) guide = repository_root / "docs" / "guides" / "guide.md" guide.parent.mkdir(parents=True) guide.write_text( "[root](../../README.md)\n" "[directory](../architecture)\n" "[file](../architecture/overview.md)\n", encoding="utf-8", ) result = _run_gate(repository_root) assert result.returncode == 0, _combined_output(result) def test_missing_relative_target_is_reported(tmp_path: Path) -> None: repository_root = _make_repository(tmp_path) _write_readme(repository_root, "[missing](docs/missing.md)\n") result = _run_gate(repository_root) assert result.returncode == 1 assert "MISSING_TARGET" in result.stderr assert "target='docs/missing.md'" in result.stderr @pytest.mark.parametrize( "relative_document", ("CONTRIBUTING.md", "app/DEVELOPMENT.md"), ) def test_all_root_and_app_level_markdown_files_are_scanned( tmp_path: Path, relative_document: str, ) -> None: repository_root = _make_repository(tmp_path) document = repository_root / relative_document document.parent.mkdir(parents=True, exist_ok=True) document.write_text("[missing](missing.md)\n", encoding="utf-8") result = _run_gate(repository_root) assert result.returncode == 1 assert f"{relative_document}:1: MISSING_TARGET" in result.stderr @pytest.mark.parametrize( "target", ( "../outside.md", "%2e%2e/outside.md", "docs/%2e%2e/%2e%2e/outside.md", ), ) def test_plain_and_encoded_repository_escape_is_rejected( tmp_path: Path, target: str, ) -> None: repository_root = _make_repository(tmp_path) _write_readme(repository_root, f"[outside]({target})\n") result = _run_gate(repository_root) assert result.returncode == 1 assert "OUTSIDE_REPOSITORY" in result.stderr def test_symlink_cancelled_by_parent_segment_is_still_rejected( tmp_path: Path, ) -> None: repository_root = _make_repository(tmp_path) external = tmp_path / "outside" nested = external / "nested" nested.mkdir(parents=True) (external / "README.md").write_text("outside\n", encoding="utf-8") (repository_root / "alias").symlink_to(nested, target_is_directory=True) _write_readme(repository_root, "[outside](alias/../README.md)\n") result = _run_gate(repository_root) assert result.returncode == 1 assert "SYMLINK_TARGET" in result.stderr def test_parent_segment_through_real_directory_stays_inside_repository( tmp_path: Path, ) -> None: repository_root = _make_repository(tmp_path) _write_readme(repository_root, "[root](docs/../README.md)\n") result = _run_gate(repository_root) assert result.returncode == 0, _combined_output(result) @pytest.mark.parametrize( ("target", "expected_code"), ( ("/etc/passwd", "ABSOLUTE_TARGET"), ("C:/Windows/system.ini", "ABSOLUTE_TARGET"), ("//server/share", "ABSOLUTE_TARGET"), (r"\\server\share", "ABSOLUTE_TARGET"), (r"docs\architecture\overview.md", "BACKSLASH_TARGET"), ("%43%3Afoo", "ABSOLUTE_TARGET"), ("file:///tmp/document.md", "FORBIDDEN_SCHEME"), ("data:text/plain,test", "FORBIDDEN_SCHEME"), ("javascript:alert(1)", "FORBIDDEN_SCHEME"), ("ftp://example.test/file", "FORBIDDEN_SCHEME"), ), ) def test_unsafe_absolute_and_scheme_targets_are_rejected( tmp_path: Path, target: str, expected_code: str, ) -> None: repository_root = _make_repository(tmp_path) _write_readme(repository_root, f"[unsafe]({target})\n") result = _run_gate(repository_root) assert result.returncode == 1 assert expected_code in result.stderr @pytest.mark.parametrize( "target", ( "docs/architecture/overview.md?raw=1", "docs/architecture/overview.md?", "docs/architecture/overview.md?#section", ), ) def test_local_query_string_is_rejected( tmp_path: Path, target: str, ) -> None: repository_root = _make_repository(tmp_path) _write_readme(repository_root, f"[query]({target})\n") result = _run_gate(repository_root) assert result.returncode == 1 assert "LOCAL_QUERY" in result.stderr def test_malformed_url_is_reported_without_stopping_other_documents( tmp_path: Path, ) -> None: repository_root = _make_repository(tmp_path) _write_readme( repository_root, "[bad](http://[::1)\n" "[missing](missing-after-malformed-url.md)\n", ) result = _run_gate(repository_root) assert result.returncode == 1 assert "MALFORMED_TARGET" in result.stderr assert "target='missing-after-malformed-url.md'" in result.stderr assert "Traceback" not in result.stderr def test_case_mismatch_is_reported_portably(tmp_path: Path) -> None: repository_root = _make_repository(tmp_path) _write_readme(repository_root, "[case](docs/Architecture/overview.md)\n") result = _run_gate(repository_root) assert result.returncode == 1 assert "PATH_CASE_MISMATCH" in result.stderr def test_symlink_target_is_rejected(tmp_path: Path) -> None: repository_root = _make_repository(tmp_path) link = repository_root / "docs" / "architecture" / "overview-link.md" link.symlink_to("overview.md") _write_readme(repository_root, "[link](docs/architecture/overview-link.md)\n") result = _run_gate(repository_root) assert result.returncode == 1 assert "SYMLINK_TARGET" in result.stderr def test_symlink_directory_component_is_rejected(tmp_path: Path) -> None: repository_root = _make_repository(tmp_path) alias = repository_root / "docs" / "architecture-alias" alias.symlink_to("architecture", target_is_directory=True) _write_readme(repository_root, "[link](docs/architecture-alias/overview.md)\n") result = _run_gate(repository_root) assert result.returncode == 1 assert "SYMLINK_TARGET" in result.stderr def test_symlink_markdown_source_is_reported(tmp_path: Path) -> None: repository_root = _make_repository(tmp_path) source = repository_root / "docs" / "linked-source.md" source.symlink_to("architecture/overview.md") result = _run_gate(repository_root) assert result.returncode == 1 assert "SYMLINK_TARGET" in result.stderr assert "docs/linked-source.md" in result.stderr def test_symlink_directory_inside_markdown_scope_is_reported( tmp_path: Path, ) -> None: repository_root = _make_repository(tmp_path) external = tmp_path / "external-docs" external.mkdir() (external / "hidden.md").write_text( "[hidden](missing.md)\n", encoding="utf-8", ) linked_directory = repository_root / "docs" / "linked-directory" linked_directory.symlink_to(external, target_is_directory=True) result = _run_gate(repository_root) assert result.returncode == 1 assert "SYMLINK_SOURCE" in result.stderr assert "docs/linked-directory" in result.stderr def test_recursive_discovery_reports_io_error( tmp_path: Path, monkeypatch: pytest.MonkeyPatch, ) -> None: repository_root = _make_repository(tmp_path) def broken_walk( top: str | os.PathLike[str], topdown: bool = True, onerror: Callable[[OSError], object] | None = None, followlinks: bool = False, ) -> Iterator[tuple[str, list[str], list[str]]]: del topdown, followlinks if onerror is not None: onerror(PermissionError(13, "denied", os.fspath(top))) return iter(()) monkeypatch.setattr(os, "walk", broken_walk) result = check_documentation_integrity( repository_root, required_documents=(), markdown_sources=(Path("docs"),), ) assert result.is_clean is False assert [issue.code for issue in result.issues] == ["SOURCE_IO_ERROR"] def test_shallow_discovery_reports_io_error( tmp_path: Path, monkeypatch: pytest.MonkeyPatch, ) -> None: repository_root = _make_repository(tmp_path) original_glob = Path.glob def broken_glob(self: Path, pattern: str) -> Iterator[Path]: if self == repository_root: raise PermissionError(13, "denied", os.fspath(self)) return original_glob(self, pattern) monkeypatch.setattr(Path, "glob", broken_glob) result = check_documentation_integrity(repository_root) assert result.is_clean is False assert "SOURCE_IO_ERROR" in {issue.code for issue in result.issues} def test_image_requires_file_while_normal_directory_link_is_allowed( tmp_path: Path, ) -> None: repository_root = _make_repository(tmp_path) image = repository_root / "docs" / "image.png" image.write_bytes(b"not-a-real-image") _write_readme( repository_root, "![file](docs/image.png)\n" "[directory](docs/architecture)\n" "![directory](docs/architecture)\n", ) result = _run_gate(repository_root) assert result.returncode == 1 assert result.stderr.count("IMAGE_NOT_FILE") == 1 @pytest.mark.parametrize( "link", ( "[file](README.md/)", "[file](README.md/.)", "[file](README.md/missing/..)", "![image](README.md/)", ), ) def test_file_target_cannot_have_directory_uri_intent( tmp_path: Path, link: str, ) -> None: repository_root = _make_repository(tmp_path) _write_readme(repository_root, f"{link}\n") result = _run_gate(repository_root) assert result.returncode == 1 assert "FILE_AS_DIRECTORY" in result.stderr def test_directory_target_may_have_trailing_slash(tmp_path: Path) -> None: repository_root = _make_repository(tmp_path) _write_readme(repository_root, "[directory](docs/architecture/)\n") result = _run_gate(repository_root) assert result.returncode == 0, _combined_output(result) @pytest.mark.parametrize( "target", ("README.md/child", "README.md/child/grandchild"), ) def test_file_cannot_be_intermediate_path_component( tmp_path: Path, target: str, ) -> None: repository_root = _make_repository(tmp_path) _write_readme(repository_root, f"[child]({target})\n") result = _run_gate(repository_root) assert result.returncode == 1 assert "FILE_AS_DIRECTORY" in result.stderr assert "PATH_IO_ERROR" not in result.stderr def test_fragments_validate_base_path_but_not_heading_slug(tmp_path: Path) -> None: repository_root = _make_repository(tmp_path) plain = repository_root / "docs" / "plain.txt" plain.write_text("plain\n", encoding="utf-8") _write_readme( repository_root, "[same](#not-checked)\n" "[cross](docs/architecture/overview.md#not-checked)\n" "[directory](docs/architecture#section)\n" "[plain](docs/plain.txt#section)\n" "[empty](docs/architecture/overview.md#)\n", ) result = _run_gate(repository_root) assert result.returncode == 1 assert "DIRECTORY_FRAGMENT" in result.stderr assert "NON_MARKDOWN_FRAGMENT" in result.stderr assert "EMPTY_FRAGMENT" in result.stderr assert "#not-checked" not in result.stderr def test_reference_links_images_angle_targets_and_titles_are_supported( tmp_path: Path, ) -> None: repository_root = _make_repository(tmp_path) (repository_root / "docs" / "image.png").write_bytes(b"image") _write_readme( repository_root, "[guide][guide-ref]\n" "![image][image-ref]\n" "[angle]( \"Overview\")\n" "\n" "[guide-ref]: docs/architecture/overview.md 'Guide'\n" "[image-ref]: docs/image.png\n", ) result = _run_gate(repository_root) assert result.returncode == 0, _combined_output(result) def test_reference_definitions_inside_quote_and_list_are_supported( tmp_path: Path, ) -> None: repository_root = _make_repository(tmp_path) _write_readme( repository_root, "> [quote][quote-ref]\n" ">\n" "> [quote-ref]: docs/architecture/overview.md\n" "- [list][list-ref]\n" "- [list-ref]: docs/architecture/project_structure.md\n", ) result = _run_gate(repository_root) assert result.returncode == 0, _combined_output(result) @pytest.mark.parametrize("marker", ("- ", "10. ")) def test_reference_definition_on_list_continuation_uses_list_indent( tmp_path: Path, marker: str, ) -> None: repository_root = _make_repository(tmp_path) continuation_indent = " " * len(marker) _write_readme( repository_root, f"{marker}[list][list-ref]\n" f"{continuation_indent}\n" f"{continuation_indent}[list-ref]: " "docs/architecture/overview.md\n", ) result = _run_gate(repository_root) assert result.returncode == 0, _combined_output(result) def test_nested_list_reference_definition_uses_full_container_path( tmp_path: Path, ) -> None: repository_root = _make_repository(tmp_path) _write_readme( repository_root, "- outer\n" " - [nested][nested-ref]\n" " \n" " [nested-ref]: docs/architecture/overview.md\n", ) result = _run_gate(repository_root) assert result.returncode == 0, _combined_output(result) def test_undefined_and_duplicate_references_are_reported(tmp_path: Path) -> None: repository_root = _make_repository(tmp_path) _write_readme( repository_root, "[missing][unknown]\n" "\n" "[known]: docs/architecture/overview.md\n" "[known]: docs/architecture/project_structure.md\n", ) result = _run_gate(repository_root) assert result.returncode == 1 assert "UNDEFINED_REFERENCE" in result.stderr assert "DUPLICATE_REFERENCE" in result.stderr def test_links_inside_inline_and_fenced_code_are_ignored(tmp_path: Path) -> None: repository_root = _make_repository(tmp_path) _write_readme( repository_root, "`[inline](missing-inline.md)`\n" "```markdown\n" "[fenced](missing-fenced.md)\n" "```\n" "[real](docs/architecture/overview.md)\n", ) result = _run_gate(repository_root) assert result.returncode == 0, _combined_output(result) def test_escaped_markdown_delimiters_use_backslash_parity(tmp_path: Path) -> None: repository_root = _make_repository(tmp_path) _write_readme( repository_root, "\\[literal](missing-literal.md)\n" "\\`[real](missing-real.md)\\`\n" "\\\\[also-real](missing-even.md)\n", ) result = _run_gate(repository_root) assert result.returncode == 1 assert "missing-literal.md" not in result.stderr assert "target='missing-real.md'" in result.stderr assert "target='missing-even.md'" in result.stderr def test_links_inside_html_comments_are_ignored(tmp_path: Path) -> None: repository_root = _make_repository(tmp_path) _write_readme( repository_root, "\n" "[real](docs/architecture/overview.md)\n", ) result = _run_gate(repository_root) assert result.returncode == 0, _combined_output(result) def test_comment_backtick_cannot_mask_link_after_comment(tmp_path: Path) -> None: repository_root = _make_repository(tmp_path) _write_readme( repository_root, "\n" "[real](missing-after-comment.md)\n" "`\n", ) result = _run_gate(repository_root) assert result.returncode == 1 assert "target='missing-after-comment.md'" in result.stderr @pytest.mark.parametrize( "paragraph", ( "> paragraph", "- paragraph", "- > paragraph", ), ) def test_html_comment_interrupts_lazy_container_paragraph( tmp_path: Path, paragraph: str, ) -> None: repository_root = _make_repository(tmp_path) _write_readme( repository_root, f"{paragraph}\n" "\n" "[outside](missing-after-html-comment.md)\n", ) result = _run_gate(repository_root) assert result.returncode == 1 assert "missing-inside-html-comment.md" not in result.stderr assert "target='missing-after-html-comment.md'" in result.stderr def test_comment_markers_inside_code_cannot_mask_link_between_code_spans( tmp_path: Path, ) -> None: repository_root = _make_repository(tmp_path) _write_readme( repository_root, "``\n", ) result = _run_gate(repository_root) assert result.returncode == 1 assert "target='missing-between-code.md'" in result.stderr def test_fence_marker_inside_comment_cannot_hide_link_and_unclosed_fence( tmp_path: Path, ) -> None: repository_root = _make_repository(tmp_path) _write_readme( repository_root, "\n" "[real](missing-after-block-comment.md)\n" "```\n", ) result = _run_gate(repository_root) assert result.returncode == 1 assert "target='missing-after-block-comment.md'" in result.stderr assert "README.md:5: UNCLOSED_CODE_FENCE" in result.stderr def test_inline_backtick_before_fence_cannot_consume_block_opener( tmp_path: Path, ) -> None: repository_root = _make_repository(tmp_path) _write_readme( repository_root, "text ```\n" "```\n" "[inside](missing-inside-unclosed-fence.md)\n", ) result = _run_gate(repository_root) assert result.returncode == 1 assert "README.md:2: UNCLOSED_CODE_FENCE" in result.stderr assert "missing-inside-unclosed-fence.md" not in result.stderr def test_inline_code_cannot_cross_blank_line_paragraph_boundary( tmp_path: Path, ) -> None: repository_root = _make_repository(tmp_path) _write_readme( repository_root, "`\n" "[real](missing-before-blank.md)\n" "\n" "`\n", ) result = _run_gate(repository_root) assert result.returncode == 1 assert "target='missing-before-blank.md'" in result.stderr def test_semantic_blank_inside_blockquote_ends_inline_code_span( tmp_path: Path, ) -> None: repository_root = _make_repository(tmp_path) _write_readme( repository_root, "> `\n" "> [real](missing-before-quote-blank.md)\n" ">\n" "> `\n", ) result = _run_gate(repository_root) assert result.returncode == 1 assert "target='missing-before-quote-blank.md'" in result.stderr def test_fenced_block_interrupts_inline_span_and_masks_only_fence_content( tmp_path: Path, ) -> None: repository_root = _make_repository(tmp_path) _write_readme( repository_root, "text ```\n" "```\n" "[inside](missing-inside-fence.md)\n" "```\n" "[real](missing-after-fence.md)\n", ) result = _run_gate(repository_root) assert result.returncode == 1 assert "missing-inside-fence.md" not in result.stderr assert "target='missing-after-fence.md'" in result.stderr def test_atx_heading_ends_inline_span_before_next_paragraph( tmp_path: Path, ) -> None: repository_root = _make_repository(tmp_path) _write_readme( repository_root, "# `\n" "[real](missing-after-heading.md)\n" "`\n", ) result = _run_gate(repository_root) assert result.returncode == 1 assert "target='missing-after-heading.md'" in result.stderr def test_multiline_inline_code_inside_one_paragraph_is_still_masked( tmp_path: Path, ) -> None: repository_root = _make_repository(tmp_path) _write_readme( repository_root, "paragraph `[inside](missing-inline-code.md)\n" "continues here` after code\n", ) result = _run_gate(repository_root) assert result.returncode == 0, _combined_output(result) def test_pipe_in_ordinary_inline_code_does_not_create_table_cells( tmp_path: Path, ) -> None: repository_root = _make_repository(tmp_path) _write_readme( repository_root, "paragraph `[inside](missing-inline-pipe.md) | continues` after\n", ) result = _run_gate(repository_root) assert result.returncode == 0, _combined_output(result) def test_pipe_lines_without_delimiter_row_remain_one_inline_paragraph( tmp_path: Path, ) -> None: repository_root = _make_repository(tmp_path) _write_readme( repository_root, "paragraph `left | [inside](missing-pseudo-table.md)\n" "right | continues` after\n", ) result = _run_gate(repository_root) assert result.returncode == 0, _combined_output(result) def test_indented_continuation_cannot_hide_rendered_link(tmp_path: Path) -> None: repository_root = _make_repository(tmp_path) _write_readme( repository_root, "paragraph\n" " [real](missing-indented-continuation.md)\n", ) result = _run_gate(repository_root) assert result.returncode == 1 assert "target='missing-indented-continuation.md'" in result.stderr def test_nested_list_continuation_cannot_hide_rendered_link(tmp_path: Path) -> None: repository_root = _make_repository(tmp_path) _write_readme( repository_root, "- item\n" " [nested](missing-nested-list.md)\n", ) result = _run_gate(repository_root) assert result.returncode == 1 assert "target='missing-nested-list.md'" in result.stderr def test_blockquote_fence_is_recognised_as_unclosed_block(tmp_path: Path) -> None: repository_root = _make_repository(tmp_path) _write_readme( repository_root, "> ```\n" "> [inside](missing-inside-blockquote-fence.md)\n", ) result = _run_gate(repository_root) assert result.returncode == 1 assert "README.md:1: UNCLOSED_CODE_FENCE" in result.stderr assert "missing-inside-blockquote-fence.md" not in result.stderr def test_blockquote_fence_cannot_be_closed_by_top_level_marker( tmp_path: Path, ) -> None: repository_root = _make_repository(tmp_path) _write_readme( repository_root, "> ```\n" "> [inside](missing-inside-blockquote.md)\n" "[outside](missing-outside-blockquote.md)\n" "```\n", ) result = _run_gate(repository_root) assert result.returncode == 1 assert "README.md:1: UNCLOSED_CODE_FENCE" in result.stderr assert "missing-inside-blockquote.md" not in result.stderr assert "target='missing-outside-blockquote.md'" in result.stderr def test_top_level_fence_cannot_be_closed_by_blockquote_marker( tmp_path: Path, ) -> None: repository_root = _make_repository(tmp_path) _write_readme( repository_root, "```\n" "> ```\n" "[inside](missing-inside-top-level.md)\n" "```\n" "[outside](docs/architecture/overview.md)\n", ) result = _run_gate(repository_root) assert result.returncode == 0, _combined_output(result) def test_correctly_closed_blockquote_fence_masks_only_its_content( tmp_path: Path, ) -> None: repository_root = _make_repository(tmp_path) _write_readme( repository_root, "> ```\n" "> [inside](missing-inside-closed-blockquote.md)\n" "> ```\n" "[outside](missing-after-closed-blockquote.md)\n", ) result = _run_gate(repository_root) assert result.returncode == 1 assert "missing-inside-closed-blockquote.md" not in result.stderr assert "target='missing-after-closed-blockquote.md'" in result.stderr def test_multiline_inline_code_inside_blockquote_remains_one_span( tmp_path: Path, ) -> None: repository_root = _make_repository(tmp_path) _write_readme( repository_root, "> paragraph `[inside](missing-inline-blockquote.md)\n" "> continues` after\n" "[outside](docs/architecture/overview.md)\n", ) result = _run_gate(repository_root) assert result.returncode == 0, _combined_output(result) def test_lazy_blockquote_continuation_keeps_rendered_link_visible( tmp_path: Path, ) -> None: repository_root = _make_repository(tmp_path) _write_readme( repository_root, "> paragraph\n" " [real](missing-lazy-blockquote-link.md)\n", ) result = _run_gate(repository_root) assert result.returncode == 1 assert "target='missing-lazy-blockquote-link.md'" in result.stderr @pytest.mark.parametrize( "content", ( "> paragraph `[inside](missing-lazy-quote-code.md)\n" "continues` after\n", "- paragraph `[inside](missing-lazy-list-code.md)\n" "continues` after\n", ), ) def test_lazy_container_continuation_keeps_inline_code_span( tmp_path: Path, content: str, ) -> None: repository_root = _make_repository(tmp_path) _write_readme(repository_root, content) result = _run_gate(repository_root) assert result.returncode == 0, _combined_output(result) def test_multiline_inline_code_inside_list_item_remains_one_span( tmp_path: Path, ) -> None: repository_root = _make_repository(tmp_path) _write_readme( repository_root, "- paragraph `[inside](missing-inline-list.md)\n" " continues` after\n" "[outside](docs/architecture/overview.md)\n", ) result = _run_gate(repository_root) assert result.returncode == 0, _combined_output(result) def test_inline_code_cannot_cross_into_sibling_list_item( tmp_path: Path, ) -> None: repository_root = _make_repository(tmp_path) _write_readme( repository_root, "- first `\n" "- [real](missing-sibling-list-item.md)\n" " `\n", ) result = _run_gate(repository_root) assert result.returncode == 1 assert "target='missing-sibling-list-item.md'" in result.stderr def test_inline_code_cannot_cross_nested_sibling_list_item( tmp_path: Path, ) -> None: repository_root = _make_repository(tmp_path) _write_readme( repository_root, "- outer\n" " - first `\n" " - [real](missing-nested-sibling-list-item.md)\n" " `\n", ) result = _run_gate(repository_root) assert result.returncode == 1 assert "target='missing-nested-sibling-list-item.md'" in result.stderr def test_ordered_list_fence_uses_its_continuation_indent( tmp_path: Path, ) -> None: repository_root = _make_repository(tmp_path) _write_readme( repository_root, "10. ```\n" " [inside](missing-inside-list-fence.md)\n" " ```\n" "[outside](missing-after-list-fence.md)\n", ) result = _run_gate(repository_root) assert result.returncode == 1 assert "missing-inside-list-fence.md" not in result.stderr assert "target='missing-after-list-fence.md'" in result.stderr @pytest.mark.parametrize( "content", ( "- > ```\n" " > [inside](missing-list-quote-fence.md)\n" " > ```\n", "> - ```\n" "> [inside](missing-quote-list-fence.md)\n" "> ```\n", ), ) def test_fence_uses_full_ordered_container_path( tmp_path: Path, content: str, ) -> None: repository_root = _make_repository(tmp_path) _write_readme(repository_root, content) result = _run_gate(repository_root) assert result.returncode == 0, _combined_output(result) def test_blank_line_without_quote_marker_splits_list_quote_fences( tmp_path: Path, ) -> None: repository_root = _make_repository(tmp_path) _write_readme( repository_root, "- > ```\n" " > hidden\n" "\n" " > ```\n" " > [after](missing-inside-second-fence.md)\n", ) result = _run_gate(repository_root) assert result.returncode == 1 assert result.stderr.count("UNCLOSED_CODE_FENCE") == 2 assert "README.md:1: UNCLOSED_CODE_FENCE" in result.stderr assert "README.md:4: UNCLOSED_CODE_FENCE" in result.stderr assert "missing-inside-second-fence.md" not in result.stderr @pytest.mark.parametrize( ("content", "target"), ( ( "- ```\n" " ```\n" " [real](missing-after-unordered-list-fence.md)\n", "missing-after-unordered-list-fence.md", ), ( "10. ```\n" " ```\n" " [real](missing-after-ordered-list-fence.md)\n", "missing-after-ordered-list-fence.md", ), ( "- \n" " [real](missing-after-list-comment.md)\n", "missing-after-list-comment.md", ), ), ) def test_list_block_preserves_context_for_following_paragraph( tmp_path: Path, content: str, target: str, ) -> None: repository_root = _make_repository(tmp_path) _write_readme(repository_root, content) result = _run_gate(repository_root) assert result.returncode == 1 assert f"target='{target}'" in result.stderr @pytest.mark.parametrize( "block", ( "- ```\n ```\n", "- \n", "10. ```\n ```\n", ), ) def test_inline_span_after_list_block_cannot_cross_dedent( tmp_path: Path, block: str, ) -> None: repository_root = _make_repository(tmp_path) marker_indent = 4 if block.startswith("10.") else 2 _write_readme( repository_root, block + " " * (marker_indent + 2) + "paragraph `\n" + "[outside](missing-after-list-dedent.md) `\n", ) result = _run_gate(repository_root) assert result.returncode == 1 assert "target='missing-after-list-dedent.md'" in result.stderr def test_gfm_table_cells_are_independent_inline_containers(tmp_path: Path) -> None: repository_root = _make_repository(tmp_path) _write_readme( repository_root, "| A | B | C |\n" "|---|---|---|\n" "| ` | [real](missing-table-cell.md) | ` |\n", ) result = _run_gate(repository_root) assert result.returncode == 1 assert "target='missing-table-cell.md'" in result.stderr def test_gfm_table_without_outer_pipes_is_confirmed_by_delimiter_row( tmp_path: Path, ) -> None: repository_root = _make_repository(tmp_path) _write_readme( repository_root, "A | B | C\n" "---|---|---\n" "` | [real](missing-table-without-outer-pipes.md) | `\n", ) result = _run_gate(repository_root) assert result.returncode == 1 assert "target='missing-table-without-outer-pipes.md'" in result.stderr @pytest.mark.parametrize( "header", ( "- `[inside](missing-lazy-table-code.md) | x`", "> `[inside](missing-lazy-table-code.md) | x`", "- > `[inside](missing-lazy-table-code.md) | x`", ), ) def test_table_delimiter_cannot_be_lazy_container_continuation( tmp_path: Path, header: str, ) -> None: repository_root = _make_repository(tmp_path) _write_readme(repository_root, f"{header}\n---|---\n") result = _run_gate(repository_root) assert result.returncode == 0, _combined_output(result) def test_indented_table_delimiter_remains_inside_list_container( tmp_path: Path, ) -> None: repository_root = _make_repository(tmp_path) _write_readme( repository_root, "- A | B\n" " ---|---\n" " [real](missing-inside-list-table.md) | value\n", ) result = _run_gate(repository_root) assert result.returncode == 1 assert "target='missing-inside-list-table.md'" in result.stderr @pytest.mark.parametrize( "content", ( "A | B\n" "---|---\n" "- paragraph `[inside](missing-after-table-list.md) | continues`\n", "A | B\n" "---|---\n" "# heading `[inside](missing-after-table-heading.md) | continues`\n", "- A | B\n" " ---|---\n" "- paragraph `[inside](missing-sibling-after-list-table.md) | continues`\n", "- `[inside](missing-between-sibling-items.md) | text`\n" "- ---|---\n", ), ) def test_gfm_table_does_not_cross_new_block_or_list_item( tmp_path: Path, content: str, ) -> None: repository_root = _make_repository(tmp_path) _write_readme(repository_root, content) result = _run_gate(repository_root) assert result.returncode == 0, _combined_output(result) def test_top_level_and_list_indented_code_are_masked_until_dedent( tmp_path: Path, ) -> None: repository_root = _make_repository(tmp_path) _write_readme( repository_root, " [first](missing-top-level-indented.md)\n" "paragraph\n" "\n" " [second](missing-after-blank-indented.md)\n" "- item\n" "\n" " [third](missing-list-indented.md)\n" "> quote block\n" " [fourth](missing-after-block-indented.md)\n" "[outside](missing-after-indented-code.md)\n", ) result = _run_gate(repository_root) assert result.returncode == 1 assert "missing-top-level-indented.md" not in result.stderr assert "missing-after-blank-indented.md" not in result.stderr assert "missing-list-indented.md" not in result.stderr assert "target='missing-after-block-indented.md'" in result.stderr assert "target='missing-after-indented-code.md'" in result.stderr def test_indented_code_after_quote_setext_and_table_blocks_is_masked( tmp_path: Path, ) -> None: repository_root = _make_repository(tmp_path) _write_readme( repository_root, ">\n" "> [quote](missing-quote-indented.md)\n" "Heading\n" "=======\n" " [setext](missing-setext-indented.md)\n" "A | B\n" "---|---\n" "x | y\n" " [table](missing-table-indented.md)\n", ) result = _run_gate(repository_root) assert result.returncode == 0, _combined_output(result) def test_many_unique_unmatched_backtick_runs_complete_within_cli_timeout( tmp_path: Path, ) -> None: repository_root = _make_repository(tmp_path) _write_readme( repository_root, "\n".join( f"text {'`' * run_length} unmatched" for run_length in range(1, 701) ), ) result = _run_gate(repository_root, timeout_seconds=15.0) assert result.returncode == 0, _combined_output(result) def test_closing_parenthesis_inside_quoted_title_is_supported( tmp_path: Path, ) -> None: repository_root = _make_repository(tmp_path) _write_readme( repository_root, '[valid](docs/architecture/overview.md "title ) remains title")\n', ) result = _run_gate(repository_root) assert result.returncode == 0, _combined_output(result) def test_unclosed_fenced_code_is_reported_at_opening_line(tmp_path: Path) -> None: repository_root = _make_repository(tmp_path) _write_readme(repository_root, "# Title\n\n```python\nprint('open')\n") result = _run_gate(repository_root) assert result.returncode == 1 assert "README.md:3: UNCLOSED_CODE_FENCE" in result.stderr def test_raw_html_link_is_rejected(tmp_path: Path) -> None: repository_root = _make_repository(tmp_path) _write_readme( repository_root, 'overview\n', ) result = _run_gate(repository_root) assert result.returncode == 1 assert "UNSUPPORTED_HTML_LINK" in result.stderr @pytest.mark.parametrize( "html_link", ( 'text', ">", ), ) def test_raw_html_link_with_angle_bracket_inside_quoted_attribute_is_rejected( tmp_path: Path, html_link: str, ) -> None: repository_root = _make_repository(tmp_path) _write_readme(repository_root, f"{html_link}\n") result = _run_gate(repository_root) assert result.returncode == 1 assert "UNSUPPORTED_HTML_LINK" in result.stderr def test_raw_html_attribute_name_inside_quoted_value_is_not_a_link( tmp_path: Path, ) -> None: repository_root = _make_repository(tmp_path) _write_readme( repository_root, 'text without link\n', ) result = _run_gate(repository_root) assert result.returncode == 0, _combined_output(result) @pytest.mark.parametrize( "content", ( '
plain
', "
plain
", "< a href=missing.md>", ), ) def test_target_like_text_outside_real_raw_html_link_is_ignored( tmp_path: Path, content: str, ) -> None: repository_root = _make_repository(tmp_path) _write_readme(repository_root, f"{content}\n") result = _run_gate(repository_root) assert result.returncode == 0, _combined_output(result) @pytest.mark.parametrize( "content", ( 'text', 'text', '[inside](missing-image-attribute.md)', ), ) def test_markdown_shape_inside_raw_html_attribute_is_not_a_link( tmp_path: Path, content: str, ) -> None: repository_root = _make_repository(tmp_path) _write_readme(repository_root, f"{content}\n") result = _run_gate(repository_root) assert result.returncode == 0, _combined_output(result) @pytest.mark.parametrize( "content", ( '', 'x', '', '', '', ), ) def test_malformed_raw_html_tag_cannot_hide_markdown_link( tmp_path: Path, content: str, ) -> None: repository_root = _make_repository(tmp_path) _write_readme(repository_root, f"{content}\n") result = _run_gate(repository_root) assert result.returncode == 1 assert "MISSING_TARGET" in result.stderr @pytest.mark.parametrize( "content", ( "text", "", "text", ), ) def test_valueless_raw_html_link_attribute_is_rejected( tmp_path: Path, content: str, ) -> None: repository_root = _make_repository(tmp_path) _write_readme(repository_root, f"{content}\n") result = _run_gate(repository_root) assert result.returncode == 1 assert "UNSUPPORTED_HTML_LINK" in result.stderr def test_tag_shaped_angle_destination_has_markdown_precedence( tmp_path: Path, ) -> None: repository_root = _make_repository(tmp_path) _write_readme( repository_root, "[inline]()\n" "[reference][docs-ref]\n" "\n" "[docs-ref]: \n" '[title](docs "")\n', ) result = _run_gate(repository_root) assert result.returncode == 0, _combined_output(result) def test_multiline_raw_html_attribute_cannot_define_reference( tmp_path: Path, ) -> None: repository_root = _make_repository(tmp_path) _write_readme( repository_root, 'text\n', ) result = _run_gate(repository_root) assert result.returncode == 0, _combined_output(result) @pytest.mark.parametrize( "definition", ( '[docs-ref]: README.md\n " [inside](missing-title.md)"', '> [docs-ref]: README.md\n> " [inside](missing-title.md)"', '- [docs-ref]: README.md\n " [inside](missing-title.md)"', ), ) def test_reference_title_on_next_line_stays_in_same_container( tmp_path: Path, definition: str, ) -> None: repository_root = _make_repository(tmp_path) _write_readme( repository_root, f"[reference][docs-ref]\n\n{definition}\n", ) result = _run_gate(repository_root) assert result.returncode == 0, _combined_output(result) def test_reference_title_in_different_container_is_not_protected( tmp_path: Path, ) -> None: repository_root = _make_repository(tmp_path) _write_readme( repository_root, '> [docs-ref]: README.md\n\n', ) result = _run_gate(repository_root) assert result.returncode == 1 assert "UNSUPPORTED_HTML_LINK" in result.stderr @pytest.mark.parametrize( "content", ( 'text', 'text', ), ) def test_invalid_raw_html_spacing_does_not_create_link_tag( tmp_path: Path, content: str, ) -> None: repository_root = _make_repository(tmp_path) _write_readme(repository_root, f"{content}\n") result = _run_gate(repository_root) assert result.returncode == 0, _combined_output(result) def test_one_line_ending_inside_raw_html_tag_is_supported(tmp_path: Path) -> None: repository_root = _make_repository(tmp_path) _write_readme(repository_root, 'text\n') result = _run_gate(repository_root) assert result.returncode == 1 assert "UNSUPPORTED_HTML_LINK" in result.stderr def test_many_unmatched_brackets_complete_within_cli_timeout( tmp_path: Path, ) -> None: repository_root = _make_repository(tmp_path) _write_readme(repository_root, "[" * 20_000 + "\n") result = _run_gate(repository_root, timeout_seconds=15.0) assert result.returncode == 0, _combined_output(result) @pytest.mark.parametrize( "content", ( '[real text](README.md "")', ), ) def test_inline_tokens_inside_label_do_not_close_markdown_link( tmp_path: Path, content: str, ) -> None: repository_root = _make_repository(tmp_path) _write_readme(repository_root, f"{content}\n") result = _run_gate(repository_root) assert result.returncode == 0, _combined_output(result) def test_autolink_inside_label_deactivates_outer_link(tmp_path: Path) -> None: repository_root = _make_repository(tmp_path) _write_readme( repository_root, '[real ](README.md "")\n', ) result = _run_gate(repository_root) assert result.returncode == 1 assert "UNSUPPORTED_HTML_LINK" in result.stderr assert "links=1" in result.stderr def test_escaped_autolink_does_not_receive_token_priority(tmp_path: Path) -> None: repository_root = _make_repository(tmp_path) _write_readme( repository_root, r"[real \](missing.md)" "\n", ) result = _run_gate(repository_root) assert result.returncode == 0, _combined_output(result) def test_inner_link_deactivates_outer_link_and_exposes_outer_title( tmp_path: Path, ) -> None: repository_root = _make_repository(tmp_path) _write_readme( repository_root, '[outer [inner](missing-inner.md)](README.md "")\n', ) result = _run_gate(repository_root) assert result.returncode == 1 assert "target='missing-inner.md'" in result.stderr assert "UNSUPPORTED_HTML_LINK" in result.stderr assert "links=1" in result.stderr def test_image_inside_link_preserves_both_targets(tmp_path: Path) -> None: repository_root = _make_repository(tmp_path) _write_readme( repository_root, "[![alt](missing-image.png)](missing-outer.md)\n", ) result = _run_gate(repository_root) assert result.returncode == 1 assert "target='missing-image.png'" in result.stderr assert "target='missing-outer.md'" in result.stderr assert "links=2" in result.stderr def test_link_inside_image_preserves_both_targets(tmp_path: Path) -> None: repository_root = _make_repository(tmp_path) _write_readme( repository_root, "![[inner](missing-inner.md)](missing-image.png)\n", ) result = _run_gate(repository_root) assert result.returncode == 1 assert "target='missing-inner.md'" in result.stderr assert "target='missing-image.png'" in result.stderr assert "links=2" in result.stderr def test_malformed_outer_angle_target_does_not_hide_inner_link( tmp_path: Path, ) -> None: repository_root = _make_repository(tmp_path) _write_readme( repository_root, "[bad](< [good](missing-good.md)\n", ) result = _run_gate(repository_root) assert result.returncode == 1 assert "MALFORMED_LINK" in result.stderr assert "target='missing-good.md'" in result.stderr @pytest.mark.parametrize( ("target", "filename"), ( ("", "foo(bar"), ("", "foo)bar"), (r"bar>", "foo>bar"), ), ) def test_angle_destination_supports_parentheses_and_escaped_closer( tmp_path: Path, target: str, filename: str, ) -> None: repository_root = _make_repository(tmp_path) (repository_root / filename).write_text("target\n", encoding="utf-8") _write_readme(repository_root, f"[valid]({target})\n") result = _run_gate(repository_root) assert result.returncode == 0, _combined_output(result) @pytest.mark.parametrize( "content", ( "[invalid]()\n", "[invalid]()\n", ), ) def test_invalid_angle_destination_is_not_protected( tmp_path: Path, content: str, ) -> None: repository_root = _make_repository(tmp_path) _write_readme(repository_root, content) result = _run_gate(repository_root) assert result.returncode == 1 assert "MALFORMED_LINK" in result.stderr def test_angle_destination_tab_reaches_target_control_policy( tmp_path: Path, ) -> None: repository_root = _make_repository(tmp_path) _write_readme(repository_root, "[invalid]()\n") result = _run_gate(repository_root) assert result.returncode == 1 assert "CONTROL_CHARACTER" in result.stderr assert "MALFORMED_LINK" not in result.stderr @pytest.mark.parametrize( "content", ( '[real](\n\nREADME.md "")\n', '[real](README.md\n\n"")\n', '[real](README.md ""\n\n)\n', '[real](\n\n"")\n', ), ) def test_blank_line_around_link_title_is_not_protected( tmp_path: Path, content: str, ) -> None: repository_root = _make_repository(tmp_path) _write_readme(repository_root, content) result = _run_gate(repository_root) assert result.returncode == 1 assert "UNSUPPORTED_HTML_LINK" in result.stderr @pytest.mark.parametrize( "content", ( '[id]: README.md "\\"\n', r"[id]: README.md (\)" "\n", ), ) def test_escaped_final_title_delimiter_is_not_protected( tmp_path: Path, content: str, ) -> None: repository_root = _make_repository(tmp_path) _write_readme(repository_root, content) result = _run_gate(repository_root) assert result.returncode == 1 assert "UNSUPPORTED_HTML_LINK" in result.stderr def test_forbidden_uri_autolink_is_validated(tmp_path: Path) -> None: repository_root = _make_repository(tmp_path) _write_readme(repository_root, "\n") result = _run_gate(repository_root) assert result.returncode == 1 assert "FORBIDDEN_SCHEME" in result.stderr def test_cr_only_line_numbers_are_reported_correctly(tmp_path: Path) -> None: repository_root = _make_repository(tmp_path) _write_readme(repository_root, "ok\r[x](missing.md)\r") result = _run_gate(repository_root) assert result.returncode == 1 assert "README.md:2" in result.stderr def test_form_feed_does_not_start_reference_definition(tmp_path: Path) -> None: repository_root = _make_repository(tmp_path) _write_readme(repository_root, "plain\f[id]: README.md\n[use][id]\n") result = _run_gate(repository_root) assert result.returncode == 1 assert "UNDEFINED_REFERENCE" in result.stderr def test_long_title_escape_parity_uses_forward_scan() -> None: assert documentation_integrity._valid_link_title( '"' + "\\" * 40_000 + '"' ) assert not documentation_integrity._valid_link_title( '"' + "\\" * 40_001 + '"' ) @pytest.mark.parametrize("source", ("[a](<" * 20_000, '[a](x "' * 20_000)) def test_many_semantic_link_failures_use_indexed_scan(source: str) -> None: links, issues = documentation_integrity._parse_markdown_links( source, source="README.md", ) assert links == () assert len(issues) == 20_000 @pytest.mark.parametrize("prefix", ("', 'x]]>', '">', '', ), ) def test_non_rendered_html_inside_block_does_not_trigger_policy( hidden: str, ) -> None: links, issues = documentation_integrity._parse_markdown_links( f"
\n{hidden}\n
\n", source="README.md", ) assert links == () assert issues == () def test_pre_block_still_rejects_rendered_raw_html_link() -> None: links, issues = documentation_integrity._parse_markdown_links( '
\nx\n
\n', source="README.md", ) assert links == () assert [issue.code for issue in issues] == ["UNSUPPORTED_HTML_LINK"] @pytest.mark.parametrize("prefix", ("", "x ")) def test_inline_html_in_table_cell_does_not_break_table_boundary( prefix: str, ) -> None: links, issues = documentation_integrity._parse_markdown_links( "A | B\n---|---\n" f"{prefix}[not | a-link](missing.md)\n", source="README.md", ) assert links == () assert issues == () @pytest.mark.parametrize("ticks", ("`", "``")) def test_backticks_inside_raw_html_attributes_do_not_open_code_span( ticks: str, ) -> None: links, issues = documentation_integrity._parse_markdown_links( f'[real](missing.md)' f'{ticks}\n', source="README.md", ) assert [link.target for link in links] == ["missing.md"] assert issues == () @pytest.mark.parametrize("ticks", ("`", "``")) def test_backticks_inside_autolinks_do_not_open_code_span( ticks: str, ) -> None: links, issues = documentation_integrity._parse_markdown_links( f"[real](missing.md)" f"{ticks}\n", source="README.md", ) assert "missing.md" in {link.target for link in links} assert issues == () @pytest.mark.parametrize( "would_be_token", ( '', "", ), ) def test_earlier_backtick_takes_precedence_over_html_token( would_be_token: str, ) -> None: links, issues = documentation_integrity._parse_markdown_links( f"`{would_be_token}[real](missing.md)`\n", source="README.md", ) assert [link.target for link in links] == ["missing.md"] assert issues == () @pytest.mark.parametrize( ("opening", "closing"), ( ("`foo\\", "`"), ("``foo\\", "``"), ), ) def test_backslash_does_not_escape_code_span_closer( opening: str, closing: str, ) -> None: links, issues = documentation_integrity._parse_markdown_links( f"{opening}{closing} [real](missing.md) {closing}\n", source="README.md", ) assert [link.target for link in links] == ["missing.md"] assert issues == () @pytest.mark.parametrize( "argument", ( "foo`bar`baz.md", "", ), ) def test_backticks_inside_link_destination_are_literal(argument: str) -> None: links, issues = documentation_integrity._parse_markdown_links( f"[link]({argument})\n", source="README.md", ) assert [link.target for link in links] == ["foo`bar`baz.md"] assert issues == () def test_angle_inline_destination_may_contain_spaces(tmp_path: Path) -> None: repository_root = _make_repository(tmp_path) target = repository_root / "docs" / "my file.md" target.write_text("space\n", encoding="utf-8") _write_readme(repository_root, "[link]()\n") result = _run_gate(repository_root) assert result.returncode == 0, _combined_output(result) def test_angle_destination_preserves_significant_trailing_space( tmp_path: Path, ) -> None: repository_root = _make_repository(tmp_path) (repository_root / "docs" / "foo.md").write_text( "without space\n", encoding="utf-8", ) _write_readme(repository_root, "[link]()\n") result = _run_gate(repository_root) assert result.returncode == 1 assert "MISSING_TARGET" in result.stderr def test_angle_destination_can_resolve_filename_with_trailing_space( tmp_path: Path, ) -> None: repository_root = _make_repository(tmp_path) (repository_root / "docs" / "foo.md ").write_text( "with space\n", encoding="utf-8", ) _write_readme(repository_root, "[link]()\n") result = _run_gate(repository_root) assert result.returncode == 0, _combined_output(result) def test_angle_destination_preserves_significant_leading_space( tmp_path: Path, ) -> None: repository_root = _make_repository(tmp_path) _write_readme(repository_root, "[link](< README.md>)\n") result = _run_gate(repository_root) assert result.returncode == 1 assert "MISSING_TARGET" in result.stderr def test_angle_destination_can_resolve_filename_with_leading_space( tmp_path: Path, ) -> None: repository_root = _make_repository(tmp_path) (repository_root / " leading.md").write_text( "with space\n", encoding="utf-8", ) _write_readme(repository_root, "[link](< leading.md>)\n") result = _run_gate(repository_root) assert result.returncode == 0, _combined_output(result) @pytest.mark.parametrize( "identifier", ( "", "", "", ), ) def test_reference_identifiers_are_not_rendered_as_html_or_autolinks( identifier: str, ) -> None: links, issues = documentation_integrity._parse_markdown_links( f"[{identifier}]: README.md\n\n[use][{identifier}]\n", source="README.md", ) assert [link.target for link in links] == ["README.md", "README.md"] assert issues == () @pytest.mark.parametrize( "identifier", ( "`id`", "id", ), ) def test_reference_labels_are_matched_as_literal_identifiers( identifier: str, ) -> None: links, issues = documentation_integrity._parse_markdown_links( f"[{identifier}]: README.md\n\n[use][{identifier}]\n", source="README.md", ) assert [link.target for link in links] == ["README.md", "README.md"] assert issues == () @pytest.mark.parametrize( ("identifier", "trailing"), ( ("`id", " later `"), (""), (""), ), ) def test_resolved_full_reference_suffix_precedes_inline_tokens( identifier: str, trailing: str, ) -> None: links, issues = documentation_integrity._parse_markdown_links( f"[{identifier}]: README.md\n\n[use][{identifier}]{trailing}\n", source="README.md", ) assert [link.target for link in links] == ["README.md", "README.md"] assert issues == () @pytest.mark.parametrize( ("definition_label", "usage_label"), ( (r"foo\!", "foo!"), ("a&b", "a&b"), ("a&b", "a&b"), ("a\N{NO-BREAK SPACE}b", "a b"), ("a\fb", "a b"), ), ) def test_reference_labels_match_normalised_source_not_inline_content( definition_label: str, usage_label: str, ) -> None: links, issues = documentation_integrity._parse_markdown_links( f"[{definition_label}]: README.md\n\n[use][{usage_label}]\n", source="README.md", ) assert [link.target for link in links] == ["README.md"] assert [issue.code for issue in issues] == ["UNDEFINED_REFERENCE"] def test_unescaped_nested_bracket_does_not_form_full_reference_label() -> None: links, issues = documentation_integrity._parse_markdown_links( r"[ref\[bar\]]: missing.md" + "\n\n[use][ref[bar]]\n", source="README.md", ) assert [link.target for link in links] == ["missing.md"] assert issues == () def test_escaped_brackets_form_full_reference_label() -> None: links, issues = documentation_integrity._parse_markdown_links( r"[ref\[bar\]]: missing.md" + "\n\n" + r"[use][ref\[bar\]]" + "\n", source="README.md", ) assert [link.target for link in links] == ["missing.md", "missing.md"] assert issues == () def test_successful_later_reference_consumes_earlier_suffix() -> None: links, issues = documentation_integrity._parse_markdown_links( "[baz]: baz.md\n\n[foo][bar][baz]\n", source="README.md", ) assert [link.target for link in links] == ["baz.md", "baz.md"] assert issues == () def test_selected_outer_reference_leaves_trailing_shortcut_available() -> None: links, issues = documentation_integrity._parse_markdown_links( "[bar]: bar.md\n[baz]: baz.md\n\n[foo][bar][baz]\n", source="README.md", ) assert [link.target for link in links] == [ "bar.md", "baz.md", "bar.md", "baz.md", ] assert issues == () @pytest.mark.parametrize( ("body", "expected_usages"), ( ("[[x][bar]][baz]", (("bar.md", False), ("baz.md", False))), ("![[x][bar]][baz]", (("baz.md", True), ("bar.md", False))), ("[![x][bar]][baz]", (("baz.md", False), ("bar.md", True))), ), ) def test_nested_reference_precedence_preserves_actual_targets( body: str, expected_usages: tuple[tuple[str, bool], ...], ) -> None: links, issues = documentation_integrity._parse_markdown_links( f"[bar]: bar.md\n[baz]: baz.md\n\n{body}\n", source="README.md", ) assert [(link.target, link.is_image) for link in links[2:]] == list( expected_usages ) assert issues == () def test_successful_inline_link_consumes_unresolved_outer_suffix() -> None: links, issues = documentation_integrity._parse_markdown_links( "[foo][bar](missing.md)\n", source="README.md", ) assert [link.target for link in links] == ["missing.md"] assert issues == () def test_unresolved_full_reference_does_not_fallback_to_shortcut() -> None: links, issues = documentation_integrity._parse_markdown_links( "[foo]: foo.md\n\n[foo][bar]\n", source="README.md", ) assert [link.target for link in links] == ["foo.md"] assert [issue.code for issue in issues] == ["UNDEFINED_REFERENCE"] def test_unoverlapped_full_reference_remains_strictly_undefined() -> None: links, issues = documentation_integrity._parse_markdown_links( "[foo][missing]\n", source="README.md", ) assert links == () assert [issue.code for issue in issues] == ["UNDEFINED_REFERENCE"] def test_raw_html_in_rendered_reference_text_is_still_rejected() -> None: links, issues = documentation_integrity._parse_markdown_links( "[label]: README.md\n\n[][label]\n", source="README.md", ) assert [link.target for link in links] == ["README.md", "README.md"] assert [issue.code for issue in issues] == ["UNSUPPORTED_HTML_LINK"] @pytest.mark.parametrize( "destination", ( "foo)bar.md", "(" * 33 + "target.md" + ")" * 33, ), ) def test_reference_definition_rejects_invalid_commonmark_destination( destination: str, ) -> None: links, issues = documentation_integrity._parse_markdown_links( f"[identifier]: {destination}\n\n[identifier]\n", source="README.md", ) assert links == () assert "MALFORMED_REFERENCE" in {issue.code for issue in issues} def test_reference_definition_angle_destination_may_contain_spaces() -> None: links, issues = documentation_integrity._parse_markdown_links( "[identifier]: \n\n[identifier]\n", source="README.md", ) assert [link.target for link in links] == [ "path with spaces.md", "path with spaces.md", ] assert issues == () def test_reference_definition_accepts_escaped_closing_parenthesis() -> None: links, issues = documentation_integrity._parse_markdown_links( r"[identifier]: foo\)bar.md" "\n\n[identifier]\n", source="README.md", ) assert [link.target for link in links] == [r"foo\)bar.md", r"foo\)bar.md"] assert issues == () def test_multiline_reference_definition_label_is_supported() -> None: links, issues = documentation_integrity._parse_markdown_links( "[foo\n bar]: README.md\n\n[use][foo bar]\n", source="README.md", ) assert [link.target for link in links] == ["README.md", "README.md"] assert issues == () def test_reference_destination_may_start_after_one_line_ending() -> None: links, issues = documentation_integrity._parse_markdown_links( "[foo]:\n /url\n\n[foo]\n", source="README.md", ) assert [link.target for link in links] == ["/url", "/url"] assert issues == () def test_indented_reference_destination_after_line_ending_is_supported() -> None: links, issues = documentation_integrity._parse_markdown_links( " [foo]:\n /url\n 'the title'\n\n[foo]\n", source="README.md", ) assert [link.target for link in links] == ["/url", "/url"] assert issues == () def test_angle_reference_destination_may_contain_spaces() -> None: links, issues = documentation_integrity._parse_markdown_links( "[Foo bar]:\n\n'title'\n\n[Foo bar]\n", source="README.md", ) assert [link.target for link in links] == ["my url", "my url"] assert issues == () def test_reference_title_may_contain_one_line_ending() -> None: links, issues = documentation_integrity._parse_markdown_links( '[foo]: /url "first\nsecond"\n\n[foo]\n', source="README.md", ) assert [link.target for link in links] == ["/url", "/url"] assert issues == () def test_reference_title_may_span_several_lines() -> None: links, issues = documentation_integrity._parse_markdown_links( "[foo]: /url '\n title\n line1\n line2\n '\n\n[foo]\n", source="README.md", ) assert [link.target for link in links] == ["/url", "/url"] assert issues == () def test_bare_reference_definition_requires_destination() -> None: links, issues = documentation_integrity._parse_markdown_links( "[foo]:\n\n[foo]\n", source="README.md", ) assert links == () assert [issue.code for issue in issues] == ["MALFORMED_REFERENCE"] @pytest.mark.parametrize( "outer", ( '[bad](README.md "unterminated ', "[bad](README.md 'unterminated ", "[bad](README.md (unterminated ", "[bad]( None: links, issues = documentation_integrity._parse_markdown_links( outer + "[good](missing.md)\n", source="README.md", ) assert [link.target for link in links] == ["missing.md"] assert "MALFORMED_LINK" in {issue.code for issue in issues} def test_long_valid_inner_link_is_not_replaced_by_complexity_policy() -> None: target = "https://example.test/" + "x" * 5_000 links, issues = documentation_integrity._parse_markdown_links( f"[bad](< [good]({target}))\n", source="README.md", ) assert [link.target for link in links] == [target] assert "MALFORMED_LINK" in {issue.code for issue in issues} def test_encoded_reserved_characters_remain_part_of_local_filename( tmp_path: Path, ) -> None: repository_root = _make_repository(tmp_path) architecture = repository_root / "docs" / "architecture" (architecture / "a#b.md").write_text("hash\n", encoding="utf-8") (architecture / "a?b.md").write_text("query\n", encoding="utf-8") _write_readme( repository_root, "[hash](docs/architecture/a%23b.md)\n" "[query](docs/architecture/a%3Fb.md)\n", ) result = _run_gate(repository_root) assert result.returncode == 0, _combined_output(result) @pytest.mark.parametrize( "target", ( "docs%2Farchitecture%2Foverview.md", "docs%5Carchitecture%5Coverview.md", "docs%2F..%2FREADME.md", ), ) def test_encoded_path_separator_is_not_treated_as_hierarchy( tmp_path: Path, target: str, ) -> None: repository_root = _make_repository(tmp_path) _write_readme(repository_root, f"[encoded]({target})\n") result = _run_gate(repository_root) assert result.returncode == 1 assert "ENCODED_PATH_SEPARATOR" in result.stderr def test_double_encoded_separator_remains_literal_filename( tmp_path: Path, ) -> None: repository_root = _make_repository(tmp_path) (repository_root / "docs" / "literal%2Fname.md").write_text( "literal\n", encoding="utf-8", ) _write_readme(repository_root, "[literal](docs/literal%252Fname.md)\n") result = _run_gate(repository_root) assert result.returncode == 0, _combined_output(result) def test_encoded_separator_remains_valid_inside_fragment( tmp_path: Path, ) -> None: repository_root = _make_repository(tmp_path) _write_readme( repository_root, "[same document](#section%2Fpart)\n" "[other document](docs/architecture/overview.md#section%2Fpart)\n", ) result = _run_gate(repository_root) assert result.returncode == 0, _combined_output(result) @pytest.mark.parametrize("literal_name", ("a©.md", "a¬it;.md")) def test_unterminated_or_unknown_entities_remain_literal_in_target( tmp_path: Path, literal_name: str, ) -> None: repository_root = _make_repository(tmp_path) target = repository_root / "docs" / literal_name target.write_text("literal\n", encoding="utf-8") _write_readme(repository_root, f"[literal](docs/{literal_name})\n") result = _run_gate(repository_root) assert result.returncode == 0, _combined_output(result) def test_commonmark_entities_are_decoded_exactly_once(tmp_path: Path) -> None: repository_root = _make_repository(tmp_path) (repository_root / "docs" / "a©.md").write_text( "literal entity\n", encoding="utf-8", ) _write_readme(repository_root, "[literal](docs/a&copy;.md)\n") result = _run_gate(repository_root) assert result.returncode == 0, _combined_output(result) def test_commonmark_entity_is_not_decoded_twice_to_other_filename( tmp_path: Path, ) -> None: repository_root = _make_repository(tmp_path) (repository_root / "docs" / "a©.md").write_text( "copyright\n", encoding="utf-8", ) _write_readme(repository_root, "[literal](docs/a&copy;.md)\n") result = _run_gate(repository_root) assert result.returncode == 1 assert "MISSING_TARGET" in result.stderr def test_entity_produced_backslash_is_not_a_markdown_escape( tmp_path: Path, ) -> None: repository_root = _make_repository(tmp_path) (repository_root / "docs" / "target.md").write_text( "target\n", encoding="utf-8", ) _write_readme(repository_root, "[target](docs\/target.md)\n") result = _run_gate(repository_root) assert result.returncode == 1 assert "BACKSLASH_TARGET" in result.stderr def test_backslash_escaped_entity_opener_remains_literal( tmp_path: Path, ) -> None: repository_root = _make_repository(tmp_path) (repository_root / "docs" / "©.md").write_text( "literal\n", encoding="utf-8", ) _write_readme(repository_root, r"[literal](docs/\©.md)" "\n") result = _run_gate(repository_root) assert result.returncode == 0, _combined_output(result) @pytest.mark.parametrize("entity", ("©", "©", "©")) def test_terminated_entities_are_decoded_in_target( tmp_path: Path, entity: str, ) -> None: repository_root = _make_repository(tmp_path) target = repository_root / "docs" / "a©.md" target.write_text("decoded\n", encoding="utf-8") _write_readme(repository_root, f"[decoded](docs/a{entity}.md)\n") result = _run_gate(repository_root) assert result.returncode == 0, _combined_output(result) def test_discovery_source_type_probe_error_is_reported( tmp_path: Path, monkeypatch: pytest.MonkeyPatch, ) -> None: repository_root = _make_repository(tmp_path) source = repository_root / "docs" original_is_file = Path.is_file def broken_is_file(path: Path) -> bool: if path == source: raise PermissionError("denied") return original_is_file(path) monkeypatch.setattr(Path, "is_file", broken_is_file) result = check_documentation_integrity( repository_root, required_documents=(), markdown_sources=(Path("docs"),), ) assert [issue.code for issue in result.issues] == ["SOURCE_IO_ERROR"] def test_discovery_candidate_type_probe_error_is_reported( tmp_path: Path, monkeypatch: pytest.MonkeyPatch, ) -> None: repository_root = _make_repository(tmp_path) candidate = repository_root / "docs" / "architecture" / "overview.md" original_is_file = Path.is_file def broken_is_file(path: Path) -> bool: if path == candidate: raise PermissionError("denied") return original_is_file(path) monkeypatch.setattr(Path, "is_file", broken_is_file) result = check_documentation_integrity( repository_root, required_documents=(), markdown_sources=(Path("docs"),), ) assert "SOURCE_IO_ERROR" in {issue.code for issue in result.issues} def test_manifest_type_probe_error_is_reported( tmp_path: Path, monkeypatch: pytest.MonkeyPatch, ) -> None: repository_root = _make_repository(tmp_path) manifest = repository_root / "README.md" original_is_file = Path.is_file def broken_is_file(path: Path) -> bool: if path == manifest: raise PermissionError("denied") return original_is_file(path) monkeypatch.setattr(Path, "is_file", broken_is_file) result = check_documentation_integrity( repository_root, required_documents=("README.md",), markdown_sources=(), ) assert [issue.code for issue in result.issues] == ["MANIFEST_IO_ERROR"] @pytest.mark.parametrize( ("target", "expected_code"), ( ("docs/%ZZ/overview.md", "INVALID_PERCENT_ESCAPE"), ("docs/architecture/over\x00view.md", "CONTROL_CHARACTER"), ("docs/%09architecture/overview.md", "CONTROL_CHARACTER"), ("docs/%0Aarchitecture/overview.md", "CONTROL_CHARACTER"), ("docs/\x85architecture/overview.md", "CONTROL_CHARACTER"), ("docs/%C2%85architecture/overview.md", "CONTROL_CHARACTER"), ), ) def test_malformed_encoded_and_control_targets_are_rejected( tmp_path: Path, target: str, expected_code: str, ) -> None: repository_root = _make_repository(tmp_path) _write_readme(repository_root, f"[invalid]({target})\n") result = _run_gate(repository_root) assert result.returncode == 1 assert expected_code in result.stderr def test_external_links_are_not_fetched(tmp_path: Path) -> None: repository_root = _make_repository(tmp_path) _write_readme( repository_root, "[https](https://unresolvable.invalid/document)\n" "[mail](mailto:nobody@unresolvable.invalid)\n" "\n", ) environment = dict(os.environ) environment.update( { "HTTP_PROXY": "http://127.0.0.1:1", "HTTPS_PROXY": "http://127.0.0.1:1", "ALL_PROXY": "http://127.0.0.1:1", } ) result = _run_gate(repository_root, environment=environment) assert result.returncode == 0, _combined_output(result) def test_invalid_utf8_document_is_reported(tmp_path: Path) -> None: repository_root = _make_repository(tmp_path) invalid = repository_root / "docs" / "invalid.md" invalid.write_bytes(b"\xff\xfe") result = _run_gate(repository_root) assert result.returncode == 1 assert "INVALID_UTF8" in result.stderr def test_multiple_diagnostics_have_deterministic_path_and_line_order( tmp_path: Path, ) -> None: repository_root = _make_repository(tmp_path) first = repository_root / "docs" / "a.md" second = repository_root / "docs" / "z.md" first.write_text("\n[missing](missing-a.md)\n", encoding="utf-8") second.write_text("[missing](missing-z.md)\n", encoding="utf-8") first_run = _run_gate(repository_root) second_run = _run_gate(repository_root) assert first_run.returncode == 1 assert first_run.stderr == second_run.stderr assert first_run.stderr.index("docs/a.md:2") < first_run.stderr.index("docs/z.md:1") def test_invalid_repository_root_returns_cli_usage_error(tmp_path: Path) -> None: result = _run_gate(tmp_path / "missing") assert result.returncode == 2 assert "Repository root не существует" in result.stderr def test_symlink_loop_repository_root_returns_cli_usage_error( tmp_path: Path, ) -> None: loop = tmp_path / "loop" loop.symlink_to("loop") result = _run_gate(loop) assert result.returncode == 2 assert "Repository root не существует" in result.stderr assert "Traceback" not in result.stderr def test_unresolvable_home_repository_root_returns_cli_usage_error() -> None: result = _run_gate(Path("~dzentra_documentation_gate_missing_user")) assert result.returncode == 2 assert "Repository root не существует" in result.stderr assert "Traceback" not in result.stderr