Files
dzentra_bot/app/tests/static/test_documentation_integrity_gate.py

3556 lines
101 KiB
Python

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](<docs/architecture/overview.md> \"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,
"<!-- [commented](missing-commented.md) -->\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"
"[inside](missing-inside-html-comment.md)\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"
"[real](missing-between-code.md)\n"
"`-->`\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"
"```\n"
"-->\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",
),
(
"- <!-- comment -->\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",
"- <!-- comment -->\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,
'<a href="docs/architecture/overview.md">overview</a>\n',
)
result = _run_gate(repository_root)
assert result.returncode == 1
assert "UNSUPPORTED_HTML_LINK" in result.stderr
@pytest.mark.parametrize(
"html_link",
(
'<a title=">" href="missing.md">text</a>',
"<img alt='>' src='missing.png'>",
),
)
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,
'<a title="href=missing.md">text without link</a>\n',
)
result = _run_gate(repository_root)
assert result.returncode == 0, _combined_output(result)
@pytest.mark.parametrize(
"content",
(
'<div title="<a href=missing.md>">plain</div>',
"<div title='<IMG SRC=missing.png>'>plain</div>",
"< 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",
(
'<span title="[inside](missing-span-attribute.md)">text</span>',
'<a title="[inside](missing-anchor-attribute.md)">text</a>',
'<img alt="[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",
(
'<a <img src="missing.png">',
'<a title=x <a href="missing.md">x</a>',
'<a title="unterminated <img src=missing.png>',
),
)
def test_raw_html_link_after_malformed_candidate_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
@pytest.mark.parametrize(
"content",
(
'</span title="[real](missing-invalid-closing-tag.md)">',
'<span !!! [real](missing-invalid-opening-tag.md)>',
'<span =bad [real](missing-invalid-equals-tag.md)>',
),
)
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",
(
"<a href>text</a>",
"<img src>",
"<A HREF >text</A>",
),
)
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](<docs>)\n"
"[reference][docs-ref]\n"
"\n"
"[docs-ref]: <docs>\n"
'[title](docs "<a href=missing.md>")\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,
'<span title="\n'
"[inside]: missing-reference-in-attribute.md\n"
'">text</span>\n',
)
result = _run_gate(repository_root)
assert result.returncode == 0, _combined_output(result)
@pytest.mark.parametrize(
"definition",
(
'[docs-ref]: README.md\n "<a href=x> [inside](missing-title.md)"',
'> [docs-ref]: README.md\n> "<a href=x> [inside](missing-title.md)"',
'- [docs-ref]: README.md\n "<a href=x> [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<a href=x>\n',
)
result = _run_gate(repository_root)
assert result.returncode == 1
assert "UNSUPPORTED_HTML_LINK" in result.stderr
@pytest.mark.parametrize(
"content",
(
'<a\n\nhref=missing.md>text</a>',
'<a title=x\fhref=missing.md>text</a>',
),
)
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, '<a\n href=missing.md>text</a>\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 <span title="]">text</span>](README.md "<a href=x>")',
),
)
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 <https://example.test/]>](README.md "<a href=x>")\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 \<https://example.test/]>](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 "<a href=x>")\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"),
("<foo)bar>", "foo)bar"),
(r"<foo\>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](<foo\nbar>)\n",
"[invalid](<foo<a href=x>)\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](<foo\tbar>)\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 "<a href=x>")\n',
'[real](README.md\n\n"<a href=x>")\n',
'[real](README.md "<a href=x>"\n\n)\n',
'[real](<README.md>\n\n"<a href=x>")\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 "<a href=x>\\"\n',
r"[id]: README.md (<a href=x>\)" "\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, "<ftp://example.test/file>\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_many_unclosed_inline_html_starts_use_bounded_scan(
prefix: str,
) -> None:
source = "prefix " + prefix * 20_000 + "[real](missing.md)\n"
links, issues = documentation_integrity._parse_markdown_links(
source,
source="README.md",
)
assert [link.target for link in links] == ["missing.md"]
assert issues == ()
def test_many_line_numbers_use_shared_index() -> None:
source = "\n".join("[a](README.md)" for _ in range(20_000))
links, issues = documentation_integrity._parse_markdown_links(
source,
source="README.md",
)
assert len(links) == 20_000
assert links[-1].line == 20_000
assert issues == ()
def test_deep_container_prefix_is_consumed_in_one_pass() -> None:
deep_line = "> " * 4_000 + "text\n"
shallower_line = "> " * 2_000 + "continuation\n"
contexts = documentation_integrity._build_line_contexts(
deep_line + shallower_line
)
assert len(contexts) == 2
assert contexts[1].container.quote_depth == 4_000
assert contexts[1].semantic_content == "continuation"
def test_exact_path_inspection_reuses_directory_cache(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
repository_root = tmp_path / "repository"
repository_root.mkdir()
targets = tuple(repository_root / f"target-{index}.md" for index in range(500))
for target in targets:
target.write_text("target\n", encoding="utf-8")
calls = 0
original_iterdir = Path.iterdir
def counting_iterdir(path: Path) -> Iterator[Path]:
nonlocal calls
calls += 1
return original_iterdir(path)
monkeypatch.setattr(Path, "iterdir", counting_iterdir)
directory_cache: dict[Path, documentation_integrity._DirectoryIndex] = {}
inspection_cache: dict[str, documentation_integrity._PathInspection] = {}
for target in targets:
inspection = documentation_integrity._inspect_exact_path(
repository_root,
target,
directory_cache=directory_cache,
inspection_cache=inspection_cache,
)
assert inspection.path == target
assert calls == 1
def test_windows_path_cache_keys_preserve_exact_case() -> None:
exact = documentation_integrity._path_cache_key(
PureWindowsPath("C:/repo/docs/Foo.md")
)
wrong_case = documentation_integrity._path_cache_key(
PureWindowsPath("C:/repo/docs/foo.md")
)
assert exact != wrong_case
def test_junction_component_is_rejected_as_target_indirection(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
repository_root = _make_repository(tmp_path)
junction = repository_root / "docs" / "architecture"
original_is_junction = Path.is_junction
def fake_is_junction(path: Path) -> bool:
return path == junction or original_is_junction(path)
monkeypatch.setattr(Path, "is_junction", fake_is_junction)
inspection = documentation_integrity._inspect_exact_path(
repository_root,
junction / "overview.md",
)
assert inspection.path is None
assert inspection.issue_code == "SYMLINK_TARGET"
def test_junction_directory_is_pruned_from_documentation_scope(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
repository_root = _make_repository(tmp_path)
junction = repository_root / "docs" / "junction"
junction.mkdir()
(junction / "hidden.md").write_text("hidden\n", encoding="utf-8")
original_is_junction = Path.is_junction
def fake_is_junction(path: Path) -> bool:
return path == junction or original_is_junction(path)
monkeypatch.setattr(Path, "is_junction", fake_is_junction)
result = check_documentation_integrity(repository_root)
assert "SYMLINK_SOURCE" in {issue.code for issue in result.issues}
assert "docs/junction" in {issue.source for issue in result.issues}
def test_single_crlf_is_allowed_between_destination_and_title(
tmp_path: Path,
) -> None:
repository_root = _make_repository(tmp_path)
_write_readme(repository_root, '[x](README.md\r\n"title")\r\n')
result = _run_gate(repository_root)
assert result.returncode == 0, _combined_output(result)
def test_single_crlf_reference_title_continuation_is_allowed(
tmp_path: Path,
) -> None:
repository_root = _make_repository(tmp_path)
_write_readme(
repository_root,
'[id]: README.md\r\n"title"\r\n\r\n[use][id]\r\n',
)
result = _run_gate(repository_root)
assert result.returncode == 0, _combined_output(result)
@pytest.mark.parametrize(
"nested",
("[inner][id]", "[inner][]", "[inner]"),
)
def test_nested_reference_deactivates_outer_inline_link(nested: str) -> None:
label = "inner" if nested != "[inner][id]" else "id"
links, issues = documentation_integrity._parse_markdown_links(
f"[{label}]: inner.md\n[outer {nested}](outer.md)\n",
source="README.md",
)
assert [link.target for link in links] == ["inner.md", "inner.md"]
assert issues == ()
def test_nested_inline_link_deactivates_outer_but_suffix_is_shortcut() -> None:
links, issues = documentation_integrity._parse_markdown_links(
"[outer-id]: outer.md\n[outer [inner](inner.md)][outer-id]\n",
source="README.md",
)
assert [link.target for link in links] == [
"outer.md",
"inner.md",
"outer.md",
]
assert issues == ()
def test_image_inside_outer_reference_preserves_both_links() -> None:
links, issues = documentation_integrity._parse_markdown_links(
"[outer-id]: outer.md\n[![alt](image.png)][outer-id]\n",
source="README.md",
)
assert [link.target for link in links] == [
"outer.md",
"outer.md",
"image.png",
]
assert [link.is_image for link in links] == [False, False, True]
assert issues == ()
def test_link_inside_reference_image_preserves_both_links() -> None:
links, issues = documentation_integrity._parse_markdown_links(
"[image-id]: image.png\n![[inner](inner.md)][image-id]\n",
source="README.md",
)
assert [link.target for link in links] == [
"image.png",
"image.png",
"inner.md",
]
assert [link.is_image for link in links] == [False, True, False]
assert issues == ()
def test_link_shaped_text_inside_valid_title_is_not_parsed() -> None:
links, issues = documentation_integrity._parse_markdown_links(
'[outer](README.md "[fake](")\n',
source="README.md",
)
assert [link.target for link in links] == ["README.md"]
assert issues == ()
def test_link_shaped_text_inside_valid_angle_target_is_not_parsed() -> None:
links, issues = documentation_integrity._parse_markdown_links(
"[outer](<[fake](>)\n",
source="README.md",
)
assert [link.target for link in links] == ["[fake]("]
assert issues == ()
def test_image_argument_tokens_do_not_deactivate_outer_link() -> None:
links, issues = documentation_integrity._parse_markdown_links(
"[outer ![img](<https://img.example/x>)](outer.md)\n",
source="README.md",
)
assert [link.target for link in links] == [
"outer.md",
"https://img.example/x",
]
assert issues == ()
@pytest.mark.parametrize(
"content",
(
'[open\n\nlabel](README.md "<a href=x>")\n',
'[open\n# heading\nlabel](README.md "<a href=x>")\n',
'[open\n> quote\nlabel](README.md "<a href=x>")\n',
'- [open\n- label](README.md "<a href=x>")\n',
'| [open | close](README.md "<a href=x>") |\n| --- | --- |\n',
'[open\n```text\ncode\n```\nlabel](README.md "<a href=x>")\n',
'[open\n<div>block</div>\nlabel](README.md "<a href=x>")\n',
),
)
def test_link_label_cannot_cross_block_boundary(
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_link_label_allows_one_soft_line_ending(tmp_path: Path) -> None:
repository_root = _make_repository(tmp_path)
_write_readme(repository_root, "[open\nlabel](README.md)\n")
result = _run_gate(repository_root)
assert result.returncode == 0, _combined_output(result)
@pytest.mark.parametrize(
"content",
(
'[x](\n# "<a href=x>")\n',
'[x](\n> "<a href=x>")\n',
'[x](\n- "<a href=x>")\n',
'[x](\n---\n"<a href=x>")\n',
),
)
def test_inline_argument_cannot_cross_block_boundary(
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_malformed_reference_does_not_hide_same_line_link() -> None:
links, issues = documentation_integrity._parse_markdown_links(
"[id]: README.md trailing [real](missing.md)\n",
source="README.md",
)
assert [link.target for link in links] == ["missing.md"]
assert [issue.code for issue in issues] == ["MALFORMED_REFERENCE"]
def test_malformed_reference_does_not_hide_next_line_link() -> None:
links, issues = documentation_integrity._parse_markdown_links(
'[id]: README.md trailing\n"[real](missing.md)"\n',
source="README.md",
)
assert [link.target for link in links] == ["missing.md"]
assert [issue.code for issue in issues] == ["MALFORMED_REFERENCE"]
def test_reference_definition_cannot_interrupt_paragraph(tmp_path: Path) -> None:
repository_root = _make_repository(tmp_path)
_write_readme(
repository_root,
'paragraph\n[id]: README.md "<a href=x>"\n\n[use][id]\n',
)
result = _run_gate(repository_root)
assert result.returncode == 1
assert "UNSUPPORTED_HTML_LINK" in result.stderr
def test_escaped_reference_label_is_matched_and_validated(tmp_path: Path) -> None:
repository_root = _make_repository(tmp_path)
_write_readme(
repository_root,
r"[foo\]]: missing.md" "\n\n" r"[foo\]]" "\n",
)
result = _run_gate(repository_root)
assert result.returncode == 1
assert "target='missing.md'" in result.stderr
@pytest.mark.parametrize("prefix", ("\f", "\v", "\N{NO-BREAK SPACE}"))
def test_non_commonmark_whitespace_does_not_form_continuation_title(
tmp_path: Path,
prefix: str,
) -> None:
repository_root = _make_repository(tmp_path)
_write_readme(
repository_root,
f'[id]: README.md\n{prefix}"<a href=x>"\n',
)
result = _run_gate(repository_root)
assert result.returncode == 1
assert "UNSUPPORTED_HTML_LINK" in result.stderr
def test_nested_argument_candidates_are_not_rescanned_quadratically() -> None:
source = "[x](" * 8_000 + "target" + ")" * 8_000
links, issues = documentation_integrity._parse_markdown_links(
source,
source="README.md",
)
assert len(links) == 1
assert len(issues) == 8_000 - 33
assert {issue.code for issue in issues} == {"MALFORMED_LINK"}
def test_balanced_nested_labels_do_not_normalise_every_suffix() -> None:
source = "[" * 64_000 + "x" + "]" * 64_000
links, issues = documentation_integrity._parse_markdown_links(
source,
source="README.md",
)
assert links == ()
assert issues == ()
def test_path_symlink_probe_error_is_reported(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
repository_root = tmp_path / "repository"
repository_root.mkdir()
target = repository_root / "target.md"
target.write_text("target\n", encoding="utf-8")
original_is_symlink = Path.is_symlink
def broken_is_symlink(path: Path) -> bool:
if path == target:
raise PermissionError("denied")
return original_is_symlink(path)
monkeypatch.setattr(Path, "is_symlink", broken_is_symlink)
inspection = documentation_integrity._inspect_exact_path(
repository_root,
target,
)
assert inspection.issue_code == "PATH_IO_ERROR"
@pytest.mark.parametrize(
"content",
(
'<script>\nconst value = "[fake](missing.md)";\n</script>\n',
'<style>\n.fake { value: "[fake](missing.md)"; }\n</style>\n',
'<pre>\n[fake](missing.md)\n</pre>\n',
'<?render value="[fake](missing.md)"?>\n',
'<!DOCTYPE demo "[fake](missing.md)">\n',
'<![CDATA[[fake](missing.md)]]>\n',
),
)
def test_commonmark_raw_html_blocks_do_not_expose_markdown_links(
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_reference_title_continuation_does_not_open_paragraph() -> None:
links, issues = documentation_integrity._parse_markdown_links(
'[one]: README.md\n"title"\n[two]: missing-two.md\n[use][two]\n',
source="README.md",
)
assert [link.target for link in links] == [
"README.md",
"missing-two.md",
"missing-two.md",
]
assert issues == ()
def test_internal_title_delimiter_cannot_protect_invalid_reference_tail(
tmp_path: Path,
) -> None:
repository_root = _make_repository(tmp_path)
_write_readme(
repository_root,
'[id]: README.md "<a href=x>" [real](missing-inline-title.md) "\n',
)
result = _run_gate(repository_root)
assert result.returncode == 1
assert "MALFORMED_REFERENCE" in result.stderr
def test_invalid_continuation_title_does_not_hide_its_content(
tmp_path: Path,
) -> None:
repository_root = _make_repository(tmp_path)
_write_readme(
repository_root,
'[id]: README.md\n"<a href=x>" [real](missing-title-tail.md) "\n',
)
result = _run_gate(repository_root)
assert result.returncode == 1
assert "UNSUPPORTED_HTML_LINK" in result.stderr
assert "target='missing-title-tail.md'" in result.stderr
def test_blank_line_inside_inline_title_is_not_protected(
tmp_path: Path,
) -> None:
repository_root = _make_repository(tmp_path)
_write_readme(
repository_root,
'[real](README.md "\n\n<a href=x>\n")\n',
)
result = _run_gate(repository_root)
assert result.returncode == 1
assert "UNSUPPORTED_HTML_LINK" in result.stderr
def test_escaped_internal_title_delimiter_is_supported(tmp_path: Path) -> None:
repository_root = _make_repository(tmp_path)
_write_readme(
repository_root,
'[id]: README.md "before \\"quoted\\" after"\n',
)
result = _run_gate(repository_root)
assert result.returncode == 0, _combined_output(result)
def test_many_unclosed_link_parentheses_use_indexed_failure_path() -> None:
links, issues = documentation_integrity._parse_markdown_links(
"[a](" * 5_000,
source="README.md",
)
assert links == ()
assert len(issues) == 5_000
def test_many_reference_definitions_use_indexed_range_lookup() -> None:
definitions = "\n".join(
f"[reference-{index}]: README.md" for index in range(8_000)
)
links, issues = documentation_integrity._parse_markdown_links(
definitions,
source="README.md",
)
assert len(links) == 8_000
assert issues == ()
def test_many_resolved_reference_siblings_use_bounded_selection() -> None:
source = "[id]: README.md\n\n" + "\n".join(
"[text][id]" for _ in range(8_000)
)
links, issues = documentation_integrity._parse_markdown_links(
source,
source="README.md",
)
assert len(links) == 8_001
assert issues == ()
def test_unclosed_multiline_reference_title_uses_bounded_scan(
monkeypatch: pytest.MonkeyPatch,
) -> None:
source = '[foo]: /url "\n' + "x\n" * 8_000
original_parse = documentation_integrity._parse_destination_and_title
scanned_characters = 0
def recording_parse(value: str) -> tuple[str | None, bool, str | None]:
nonlocal scanned_characters
scanned_characters += len(value)
return original_parse(value)
monkeypatch.setattr(
documentation_integrity,
"_parse_destination_and_title",
recording_parse,
)
documentation_integrity._parse_markdown_links(
source,
source="README.md",
)
assert scanned_characters <= len(source) * 10
def test_markdown_between_raw_html_tags_remains_visible(tmp_path: Path) -> None:
repository_root = _make_repository(tmp_path)
_write_readme(
repository_root,
'<span title="label">[real](missing-between-html-tags.md)</span>\n',
)
result = _run_gate(repository_root)
assert result.returncode == 1
assert "target='missing-between-html-tags.md'" in result.stderr
def test_definition_shaped_line_cannot_break_multiline_link_label() -> None:
links, issues = documentation_integrity._parse_markdown_links(
"[outer\n[id]: literal text\nlabel](missing.md)\n",
source="README.md",
)
assert [link.target for link in links] == ["missing.md"]
assert issues == ()
def test_inline_html_tag_cannot_break_multiline_link_label() -> None:
links, issues = documentation_integrity._parse_markdown_links(
"<span>[outer\nlabel](missing.md)</span>\n",
source="README.md",
)
assert [link.target for link in links] == ["missing.md"]
assert issues == ()
@pytest.mark.parametrize("tag", ("div", "table", "custom-element"))
def test_html_block_suppresses_markdown_until_blank_line(tag: str) -> None:
links, issues = documentation_integrity._parse_markdown_links(
f"<{tag}>\n[fake](missing-inside.md)\n</{tag}>\n\n"
"[real](README.md)\n",
source="README.md",
)
assert [link.target for link in links] == ["README.md"]
assert issues == ()
def test_raw_html_link_policy_remains_active_inside_html_block() -> None:
links, issues = documentation_integrity._parse_markdown_links(
'<div>\n<a href="missing.md">x</a>\n</div>\n',
source="README.md",
)
assert links == ()
assert [issue.code for issue in issues] == ["UNSUPPORTED_HTML_LINK"]
def test_malformed_definition_keeps_following_definition_in_paragraph() -> None:
links, issues = documentation_integrity._parse_markdown_links(
"[bad]: README.md trailing\n"
"[next]: missing.md\n"
"[use][next]\n",
source="README.md",
)
assert links == ()
assert [issue.code for issue in issues] == [
"MALFORMED_REFERENCE",
"UNDEFINED_REFERENCE",
]
def test_blank_line_after_malformed_definition_allows_next_definition() -> None:
links, issues = documentation_integrity._parse_markdown_links(
"[bad]: README.md trailing\n\n"
"[next]: missing.md\n"
"[use][next]\n",
source="README.md",
)
assert [link.target for link in links] == ["missing.md", "missing.md"]
assert [issue.code for issue in issues] == ["MALFORMED_REFERENCE"]
@pytest.mark.parametrize("label", (" ", "\t", "a" * 1_000))
def test_invalid_reference_definition_label_is_not_registered(
label: str,
) -> None:
links, issues = documentation_integrity._parse_markdown_links(
f"[{label}]: missing.md\n[{label}]\n",
source="README.md",
)
assert links == ()
assert [issue.code for issue in issues] == ["MALFORMED_REFERENCE"]
def test_long_link_text_preserves_full_reference_image_identity() -> None:
links, issues = documentation_integrity._parse_markdown_links(
"[id]: docs\n\n![" + "x" * 1_000 + "][id]\n",
source="README.md",
)
assert [link.target for link in links] == ["docs", "docs"]
assert [link.is_image for link in links] == [False, True]
assert issues == ()
@pytest.mark.parametrize("fragment", ("[x](<", '[x](a "'))
def test_closed_nested_malformed_arguments_have_bounded_scan_work(
monkeypatch: pytest.MonkeyPatch,
fragment: str,
) -> None:
source = fragment * 8_000 + "z" + ")" * 8_000
original_split = documentation_integrity._split_destination_and_title
scanned_characters = 0
def recording_split(value: str) -> tuple[str | None, str | None]:
nonlocal scanned_characters
scanned_characters += len(value)
return original_split(value)
monkeypatch.setattr(
documentation_integrity,
"_split_destination_and_title",
recording_split,
)
documentation_integrity._parse_markdown_links(
source,
source="README.md",
)
assert scanned_characters <= len(source) * 100
def test_nested_parenthesized_titles_have_bounded_scan_work(
monkeypatch: pytest.MonkeyPatch,
) -> None:
source = "[x](a (" * 8_000 + "z" + ")" * 16_000
original_split = documentation_integrity._split_destination_and_title
scanned_characters = 0
def recording_split(value: str) -> tuple[str | None, str | None]:
nonlocal scanned_characters
scanned_characters += len(value)
return original_split(value)
monkeypatch.setattr(
documentation_integrity,
"_split_destination_and_title",
recording_split,
)
documentation_integrity._parse_markdown_links(
source,
source="README.md",
)
assert scanned_characters <= len(source) * 100
@pytest.mark.parametrize(
"content",
(
'prefix <?x value="[fake](missing.md) <a href=x>"?> suffix\n',
'prefix <!DOCTYPE x "[fake](missing.md) <a href=x>"> suffix\n',
'prefix <![CDATA[[fake](missing.md) <a href=x>]]> suffix\n',
),
)
def test_inline_special_html_constructs_are_opaque(content: str) -> None:
links, issues = documentation_integrity._parse_markdown_links(
content,
source="README.md",
)
assert links == ()
assert issues == ()
@pytest.mark.parametrize(
"prefix",
(
"prefix <!-- broken ",
"prefix <?broken ",
"prefix <![CDATA[ broken ",
"prefix <!DOCTYPE broken ",
"<!lowercase declaration ",
),
)
def test_unclosed_inline_special_html_keeps_following_link_visible(
prefix: str,
) -> None:
links, issues = documentation_integrity._parse_markdown_links(
prefix + "[real](missing.md)\n",
source="README.md",
)
assert [link.target for link in links] == ["missing.md"]
assert issues == ()
@pytest.mark.parametrize(
"hidden",
(
'<!-- <a href="missing.md">x</a> -->',
'<![CDATA[<a href="missing.md">x</a>]]>',
'<?x value="<a href=missing.md>">',
'<script>const x = "<a href=missing.md>";</script>',
),
)
def test_non_rendered_html_inside_block_does_not_trigger_policy(
hidden: str,
) -> None:
links, issues = documentation_integrity._parse_markdown_links(
f"<div>\n{hidden}\n</div>\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(
'<pre>\n<a href="missing.md">x</a>\n</pre>\n',
source="README.md",
)
assert links == ()
assert [issue.code for issue in issues] == ["UNSUPPORTED_HTML_LINK"]
@pytest.mark.parametrize("prefix", ("<span>", "<img alt=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)</span>\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'<span title="{ticks}">[real](missing.md)'
f'<span title="{ticks}">{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"<https://example.test/{ticks}>[real](missing.md)"
f"<https://example.test/{ticks}>{ticks}\n",
source="README.md",
)
assert "missing.md" in {link.target for link in links}
assert issues == ()
@pytest.mark.parametrize(
"would_be_token",
(
'<a href="`">',
"<https://example.test/`>",
),
)
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",
"<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](<docs/my file.md>)\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](<docs/foo.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_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](<docs/foo.md >)\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",
(
"<a href=x>",
"<https://identifier.example.test/value>",
"<file:///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`",
"<span>id</span>",
),
)
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 `"),
("<https://id", ">"),
("<span title=x", ">"),
),
)
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&amp;b", "a&b"),
("a&#38;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[<a href=x>][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]: <path with spaces.md>\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<my url>\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](<unterminated",
),
)
def test_unclosed_outer_argument_recovers_inner_link(outer: str) -> 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&copy.md", "a&notit;.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&copy;.md").write_text(
"literal entity\n",
encoding="utf-8",
)
_write_readme(repository_root, "[literal](docs/a&amp;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&amp;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&bsol;/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" / "&copy;.md").write_text(
"literal\n",
encoding="utf-8",
)
_write_readme(repository_root, r"[literal](docs/\&copy;.md)" "\n")
result = _run_gate(repository_root)
assert result.returncode == 0, _combined_output(result)
@pytest.mark.parametrize("entity", ("&copy;", "&#169;", "&#xA9;"))
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"
"<wss://unresolvable.invalid/socket>\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