Picarones / tests /docs /test_index_links_resolve.py
Claude
docs(index): repair broken links + lock against drift
32c3118 unverified
Raw
History Blame
3.38 kB
"""Garde-fou : tout lien interne dans ``docs/index.md`` doit pointer
vers un fichier réel.
Pourquoi ce test existe
-----------------------
``docs/index.md`` est l'**index canonique** de la documentation : il
est référencé depuis le README, depuis mkdocs.yml, et c'est la
première porte d'entrée pour un nouveau contributeur.
Avant Phase 1, ce fichier contenait 4 liens cassés (``first-benchmark``,
``writing-a-pipeline-module``, ``developer/narrative-engine``,
``user/...``) qui ont survécu pendant le rewrite parce qu'aucun test
ne validait ses propres liens. Ce garde-fou élimine la classe
d'erreur : si l'index ment, la CI échoue.
Périmètre
---------
On parse les liens markdown ``[texte](cible)`` et on vérifie que la
``cible`` :
- soit pointe vers un fichier existant (résolution relative à
``docs/`` ou à la racine pour les ``../X``) ;
- soit est une URL externe (``http://...``, ``mailto:...``) — non
vérifiée ici, c'est le rôle de tests externes ;
- soit est une ancre intra-document (``#section``) — non vérifiée.
Les liens vers des dossiers (``case-studies/``, ``audits/``) sont
vérifiés comme l'existence du dossier.
"""
from __future__ import annotations
import re
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parents[2]
INDEX = REPO_ROOT / "docs" / "index.md"
#: Pattern markdown standard : ``[texte](cible)``. On capture la
#: cible (groupe 2) qu'on évaluera comme chemin.
_LINK_RE = re.compile(r"\[([^\]]+)\]\(([^)]+)\)")
def _resolve_link(target: str) -> Path | None:
"""Résout une cible de lien relativement à ``docs/index.md``.
Retourne ``None`` si :
- URL externe (``http``, ``mailto``, ``#``) ;
- cible vide ;
- chemin qui ne se résout pas.
"""
target = target.strip()
# URL externe — pas notre problème ici.
if target.startswith(("http://", "https://", "mailto:", "#")):
return None
# Retirer l'ancre éventuelle (``foo.md#section``)
target = target.split("#", 1)[0]
if not target:
return None
# Les liens dans index.md sont relatifs à ``docs/``.
# Les liens vers la racine (``../GOVERNANCE.md``) doivent
# remonter au repo root.
base = INDEX.parent
resolved = (base / target).resolve()
return resolved
def test_index_md_exists() -> None:
assert INDEX.exists(), (
f"{INDEX} absent — c'est l'index canonique de la doc, il "
"ne peut pas manquer."
)
def test_all_internal_links_in_index_resolve() -> None:
"""Tout lien interne dans ``docs/index.md`` doit pointer vers
un fichier ou dossier existant."""
text = INDEX.read_text(encoding="utf-8")
offenders: list[str] = []
for match in _LINK_RE.finditer(text):
target = match.group(2)
resolved = _resolve_link(target)
if resolved is None:
continue # URL externe / ancre — pas notre périmètre
if not resolved.exists():
offenders.append(
f" « {match.group(1)} » → {target!r} "
f"(résolu vers {resolved.relative_to(REPO_ROOT) if resolved.is_relative_to(REPO_ROOT) else resolved})"
)
assert not offenders, (
f"{len(offenders)} lien(s) cassé(s) dans docs/index.md :\n"
+ "\n".join(offenders)
+ "\n\n→ Soit créer le fichier cible, soit corriger le lien."
)