7a0da7932b
OSV-Scanner (Scheduled) / scan-scheduled (push) Failing after 0s
Create Release / test-gate (push) Has been cancelled
Create Release / release-gate (push) Has been cancelled
Create Release / ci-gate (push) Has been cancelled
Create Release / version-check (push) Has been cancelled
Create Release / e2e-test-gate (push) Has been cancelled
Create Release / responsive-test-gate (push) Has been cancelled
Create Release / compat-test-gate (push) Has been cancelled
Create Release / compose-integration-gate (push) Has been cancelled
Create Release / vulture-gate (push) Has been cancelled
Create Release / build (push) Has been cancelled
Create Release / provenance (push) Has been cancelled
Create Release / prerelease-docker (push) Has been cancelled
Create Release / publish-docker (push) Has been cancelled
Create Release / create-release (push) Has been cancelled
Create Release / cleanup-changelog (push) Has been cancelled
Create Release / trigger-pypi (push) Has been cancelled
Create Release / monitor-pypi (push) Has been cancelled
Create Release / Clean up orphan prerelease tags and signatures (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-form] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-metrics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-workflow] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-core] (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [history-news] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [library] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [link-analytics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-core] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-lifecycle] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [error-benchmark] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) (push) Has been cancelled
Docker Tests (Consolidated) / Accessibility Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Unit Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Example Tests (push) Has been cancelled
Docker Tests (Consolidated) / Production Image Smoke Test (push) Has been cancelled
Docker Tests (Consolidated) / Infrastructure Tests (push) Has been cancelled
OSSF Scorecard / OSSF Security Scorecard Analysis (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [mobile] (push) Has been cancelled
Backwards Compatibility / Verify Encryption Constants (push) Has been cancelled
Backwards Compatibility / PyPI Version Compatibility (push) Has been cancelled
Backwards Compatibility / Database Migration Tests (push) Has been cancelled
CodeQL Advanced / Analyze (python) (push) Has been cancelled
Docker Tests (Consolidated) / detect-changes (push) Has been cancelled
Docker Tests (Consolidated) / Build Test Image (push) Has been cancelled
Docker Tests (Consolidated) / All Pytest Tests + Coverage (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [accessibility] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [api-crud] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-login] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-register] (push) Has been cancelled
170 lines
6.0 KiB
Python
170 lines
6.0 KiB
Python
"""Tests for the check-readme-links pre-commit hook.
|
|
|
|
Verifies that the hook flags broken relative links, dangling heading
|
|
anchors, non-runnable ``python -m`` examples, and unregistered
|
|
``engine="..."`` names in README.md and docs/**/*.md, while accepting
|
|
files whose references all resolve — including GitHub-style anchors for
|
|
headings that contain emoji and links inside fenced code blocks (which
|
|
GitHub does not render and must therefore be exempt).
|
|
"""
|
|
|
|
import subprocess
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
HOOK_SCRIPT = (
|
|
Path(__file__).parent.parent.parent
|
|
/ ".pre-commit-hooks"
|
|
/ "check-readme-links.py"
|
|
)
|
|
|
|
REGISTRY_REL = Path("src/local_deep_research/web_search_engines")
|
|
|
|
VALID_README = """# Test Project
|
|
|
|
[a doc](docs/real.md)
|
|
[an anchor](docs/real.md#real-heading)
|
|
[emoji same-file anchor](#-benchmarks)
|
|
[line anchor](docs/real.md#L1)
|
|
|
|
## 📊 Benchmarks
|
|
|
|
```bash
|
|
python -m local_deep_research.pkg
|
|
python -m local_deep_research.tool.cli run --flag
|
|
```
|
|
|
|
`search(query="x", engine="arxiv")`
|
|
"""
|
|
|
|
|
|
def _make_repo(tmp_path: Path, readme: str) -> Path:
|
|
"""Build a minimal fake repo the hook can be pointed at via --root."""
|
|
(tmp_path / "README.md").write_text(readme, encoding="utf-8")
|
|
(tmp_path / "SECURITY.md").write_text("# Security\n", encoding="utf-8")
|
|
(tmp_path / "CONTRIBUTING.md").write_text(
|
|
"# Contributing\n", encoding="utf-8"
|
|
)
|
|
docs = tmp_path / "docs"
|
|
docs.mkdir()
|
|
(docs / "real.md").write_text("## Real Heading\n", encoding="utf-8")
|
|
|
|
pkg = tmp_path / "src" / "local_deep_research" / "pkg"
|
|
pkg.mkdir(parents=True)
|
|
(pkg / "__main__.py").write_text("", encoding="utf-8")
|
|
tool = tmp_path / "src" / "local_deep_research" / "tool"
|
|
tool.mkdir()
|
|
(tool / "cli.py").write_text("", encoding="utf-8")
|
|
|
|
registry_dir = tmp_path / REGISTRY_REL
|
|
registry_dir.mkdir(parents=True)
|
|
(registry_dir / "engine_registry.py").write_text(
|
|
'ENGINE_REGISTRY = {\n "arxiv": EngineEntry(\n', encoding="utf-8"
|
|
)
|
|
return tmp_path
|
|
|
|
|
|
def _run_hook(root: Path):
|
|
return subprocess.run(
|
|
[sys.executable, str(HOOK_SCRIPT), "--root", str(root)],
|
|
capture_output=True,
|
|
text=True,
|
|
)
|
|
|
|
|
|
class TestAcceptsValidReadme:
|
|
def test_all_references_resolve(self, tmp_path):
|
|
result = _run_hook(_make_repo(tmp_path, VALID_README))
|
|
assert result.returncode == 0, result.stdout
|
|
|
|
def test_external_links_are_ignored(self, tmp_path):
|
|
readme = "[ext](https://example.com/gone) [m](mailto:a@b.c)\n"
|
|
result = _run_hook(_make_repo(tmp_path, readme))
|
|
assert result.returncode == 0, result.stdout
|
|
|
|
def test_non_ldr_modules_are_ignored(self, tmp_path):
|
|
result = _run_hook(_make_repo(tmp_path, "`python -m http.server`\n"))
|
|
assert result.returncode == 0, result.stdout
|
|
|
|
def test_links_inside_code_fences_are_ignored(self, tmp_path):
|
|
# GitHub renders no links inside fenced blocks, so example markup
|
|
# with runtime paths must not be flagged.
|
|
readme = (
|
|
"# T\n\n```html\n"
|
|
'<link href="/static/css/nope.css">\n'
|
|
"[fake](docs/nope.md)\n"
|
|
"```\n"
|
|
)
|
|
result = _run_hook(_make_repo(tmp_path, readme))
|
|
assert result.returncode == 0, result.stdout
|
|
|
|
|
|
class TestFlagsBrokenReferences:
|
|
def test_missing_link_target(self, tmp_path):
|
|
result = _run_hook(_make_repo(tmp_path, "[x](docs/nope.md)\n"))
|
|
assert result.returncode == 1
|
|
assert "docs/nope.md" in result.stdout
|
|
|
|
def test_dangling_heading_anchor(self, tmp_path):
|
|
result = _run_hook(_make_repo(tmp_path, "[x](docs/real.md#gone)\n"))
|
|
assert result.returncode == 1
|
|
assert "#gone" in result.stdout
|
|
|
|
def test_dangling_same_file_anchor(self, tmp_path):
|
|
result = _run_hook(_make_repo(tmp_path, "# T\n[x](#missing)\n"))
|
|
assert result.returncode == 1
|
|
assert "#missing" in result.stdout
|
|
|
|
def test_line_anchor_beyond_eof(self, tmp_path):
|
|
result = _run_hook(_make_repo(tmp_path, "[x](docs/real.md#L999)\n"))
|
|
assert result.returncode == 1
|
|
assert "L999" in result.stdout
|
|
|
|
def test_unrunnable_python_module(self, tmp_path):
|
|
readme = "`python -m local_deep_research.missing.mod`\n"
|
|
result = _run_hook(_make_repo(tmp_path, readme))
|
|
assert result.returncode == 1
|
|
assert "local_deep_research.missing.mod" in result.stdout
|
|
|
|
def test_package_without_dunder_main(self, tmp_path):
|
|
# src/local_deep_research/pkg exists but we point one level up,
|
|
# at a directory that has no __main__.py.
|
|
readme = "`python -m local_deep_research`\n"
|
|
result = _run_hook(_make_repo(tmp_path, readme))
|
|
assert result.returncode == 1
|
|
|
|
def test_unregistered_engine_name(self, tmp_path):
|
|
readme = 'search(query="x", engine="ghost")\n'
|
|
result = _run_hook(_make_repo(tmp_path, readme))
|
|
assert result.returncode == 1
|
|
assert 'engine="ghost"' in result.stdout
|
|
|
|
def test_missing_registry_fails_loudly(self, tmp_path):
|
|
root = _make_repo(tmp_path, 'search(query="x", engine="arxiv")\n')
|
|
(root / REGISTRY_REL / "engine_registry.py").unlink()
|
|
result = _run_hook(root)
|
|
assert result.returncode == 1
|
|
assert "cannot verify engine names" in result.stdout
|
|
|
|
def test_broken_link_in_docs_file(self, tmp_path):
|
|
# docs/**/*.md is covered, not just README.md.
|
|
root = _make_repo(tmp_path, "# T\n")
|
|
(root / "docs" / "real.md").write_text(
|
|
"## Real Heading\n[gone](missing.md)\n", encoding="utf-8"
|
|
)
|
|
result = _run_hook(root)
|
|
assert result.returncode == 1
|
|
assert "docs/real.md" in result.stdout
|
|
assert "missing.md" in result.stdout
|
|
|
|
|
|
class TestRealRepo:
|
|
def test_actual_readme_and_docs_pass(self):
|
|
"""The repo's own README and docs must satisfy their own check."""
|
|
result = subprocess.run(
|
|
[sys.executable, str(HOOK_SCRIPT)],
|
|
capture_output=True,
|
|
text=True,
|
|
)
|
|
assert result.returncode == 0, result.stdout
|