Files
LEDMatrix/test/test_doc_links.py
T
Claude 6edd6f9beb test: add regression guards for the bug classes fixed in this PR
Three lightweight static checks, all enrolled in CI's unit-test
allowlist along with the new web-cache test:

- test_template_targets.py: every literal render_template() target must
  exist (would have caught the weather/stocks partial 500s at commit
  time).
- test_widget_scripts.py: every widget JS file must be script-included
  in base.html or explicitly allowlisted with a reason (would have
  caught the unloaded time-picker/file-upload-single/plugin-file-manager
  widgets), and allowlisted files must NOT be included (prevents the
  example widget from shadowing the real color-picker).
- test_doc_links.py: relative markdown links in active docs must
  resolve (docs/archive/ exempt).

Each guard was verified to fail against the pre-PR tree and pass now.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SXb4mKcAkVaxkeTb3YnAdr
2026-08-06 00:11:48 +00:00

41 lines
1.4 KiB
Python

"""Guard: relative markdown links in active docs must resolve.
Scans repo-root *.md and docs/ (excluding docs/archive/, which is allowed
to rot). External URLs, mailto links, and pure anchors are skipped, as are
links inside fenced code blocks.
"""
import re
from pathlib import Path
PROJECT_ROOT = Path(__file__).resolve().parent.parent
LINK_RE = re.compile(r'\[[^\]]*\]\(([^)\s]+)\)')
FENCE_RE = re.compile(r'^(```|~~~)')
def _md_files():
yield from PROJECT_ROOT.glob('*.md')
for path in PROJECT_ROOT.glob('docs/**/*.md'):
if 'archive' not in path.parts:
yield path
def test_relative_markdown_links_resolve():
broken = []
for md in _md_files():
in_fence = False
for lineno, line in enumerate(md.read_text(encoding='utf-8').splitlines(), 1):
if FENCE_RE.match(line.strip()):
in_fence = not in_fence
continue
if in_fence:
continue
for target in LINK_RE.findall(line):
if target.startswith(('http://', 'https://', 'mailto:', '#')):
continue
resolved = (md.parent / target.split('#')[0]).resolve()
if not resolved.exists():
broken.append(
f'{md.relative_to(PROJECT_ROOT)}:{lineno} -> {target}'
)
assert not broken, 'Broken relative markdown links:\n' + '\n'.join(broken)