From 6edd6f9beb9fcb60930072b348d87a0d1d2c1934 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 6 Aug 2026 00:11:48 +0000 Subject: [PATCH] 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 Claude-Session: https://claude.ai/code/session_01SXb4mKcAkVaxkeTb3YnAdr --- .github/workflows/test.yml | 6 ++++- test/test_doc_links.py | 40 +++++++++++++++++++++++++++++ test/test_template_targets.py | 33 ++++++++++++++++++++++++ test/test_widget_scripts.py | 48 +++++++++++++++++++++++++++++++++++ 4 files changed, 126 insertions(+), 1 deletion(-) create mode 100644 test/test_doc_links.py create mode 100644 test/test_template_targets.py create mode 100644 test/test_widget_scripts.py diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 0000c47f..0583f0b2 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -80,4 +80,8 @@ jobs: test/test_version_consistency.py \ test/test_plugin_compatibility_gate.py \ test/test_install_preserves_existing.py \ - test/test_core_owned_config_keys.py + test/test_core_owned_config_keys.py \ + test/test_template_targets.py \ + test/test_widget_scripts.py \ + test/test_doc_links.py \ + test/web_interface/test_cache.py diff --git a/test/test_doc_links.py b/test/test_doc_links.py new file mode 100644 index 00000000..ac95ea37 --- /dev/null +++ b/test/test_doc_links.py @@ -0,0 +1,40 @@ +"""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) diff --git a/test/test_template_targets.py b/test/test_template_targets.py new file mode 100644 index 00000000..4a26d9ff --- /dev/null +++ b/test/test_template_targets.py @@ -0,0 +1,33 @@ +"""Guard: every literal render_template() target must exist on disk. + +Catches routes that reference templates deleted in a refactor (a real bug +class: the weather/stocks partials 500'd for months because their +templates were removed when those displays became plugins). +""" +import re +from pathlib import Path + +PROJECT_ROOT = Path(__file__).resolve().parent.parent +TEMPLATE_ROOT = PROJECT_ROOT / 'web_interface' / 'templates' +RENDER_RE = re.compile(r"""render_template\(\s*['"]([^'"]+)['"]""") + + +def _python_sources(): + yield PROJECT_ROOT / 'web_interface' / 'app.py' + yield from (PROJECT_ROOT / 'web_interface' / 'blueprints').glob('*.py') + + +def test_all_literal_render_template_targets_exist(): + missing = [] + for source in _python_sources(): + text = source.read_text(encoding='utf-8') + for lineno, line in enumerate(text.splitlines(), 1): + for target in RENDER_RE.findall(line): + if not (TEMPLATE_ROOT / target).is_file(): + missing.append( + f'{source.relative_to(PROJECT_ROOT)}:{lineno} -> {target}' + ) + assert not missing, ( + 'render_template() references templates that do not exist under ' + f'web_interface/templates/:\n' + '\n'.join(missing) + ) diff --git a/test/test_widget_scripts.py b/test/test_widget_scripts.py new file mode 100644 index 00000000..8c07a8b0 --- /dev/null +++ b/test/test_widget_scripts.py @@ -0,0 +1,48 @@ +"""Guard: every widget JS file must be loaded by base.html or explicitly allowlisted. + +Widget files register themselves with LEDMatrixWidgets at load time; a file +that exists but is never