#!/usr/bin/env python3 """Adversarial tests for verify-motion.py and its canonical template.""" from __future__ import annotations import importlib.util import re import subprocess import sys import tempfile from pathlib import Path ROOT = Path(__file__).resolve().parent.parent VERIFIER = ROOT / "scripts/verify-motion.py" SEMANTIC_VERIFIER = ROOT / "scripts/verify-semantic-motion.py" TEMPLATE = ROOT / "skills/diagram-design/assets/template-motion.html" EXAMPLE = ROOT / "skills/diagram-design/assets/example-policy-trace-animated.html" def load_verifier(): spec = importlib.util.spec_from_file_location("verify_motion", VERIFIER) if spec is None or spec.loader is None: raise AssertionError("could not load verify-motion.py") module = importlib.util.module_from_spec(spec) spec.loader.exec_module(module) return module def load_semantic_verifier(): spec = importlib.util.spec_from_file_location("verify_semantic_motion", SEMANTIC_VERIFIER) if spec is None or spec.loader is None: raise AssertionError("could not load verify-semantic-motion.py") module = importlib.util.module_from_spec(spec) spec.loader.exec_module(module) return module def main() -> int: module = load_verifier() semantic_module = load_semantic_verifier() source = TEMPLATE.read_text(encoding="utf-8") with tempfile.TemporaryDirectory(prefix="verify-motion-") as temporary: directory = Path(temporary) def check(name: str, html: str, expected: str | None) -> None: path = directory / f"{name}.html" path.write_text(html, encoding="utf-8") errors = module.verify(path) if expected is None: if errors: raise AssertionError(f"{name}: expected pass, got {errors}") print(f"OK: {name} accepted") else: if not any(expected in error for error in errors): raise AssertionError(f"{name}: expected {expected!r}, got {errors}") print(f"OK: {name} rejected — {expected}") check("canonical-template", source, None) script_free_none = re.sub( r"", "", source, flags=re.IGNORECASE | re.DOTALL ) script_free_none = re.sub( r"
", "", script_free_none, flags=re.DOTALL ) script_free_none = re.sub( r"\sdata-motion-item\sdata-step=\"\d+\"", "", script_free_none ) script_free_none = script_free_none.replace( 'data-motion-mode="step" data-step-count="5"', 'data-motion-mode="none" data-step-count="0"', 1, ) check("script-free-none", script_free_none, None) none_with_controls = script_free_none.replace( "", '
', 1 ) check( "none-with-controls", none_with_controls, "none mode must not expose playback controls or live status", ) loop_two = script_free_none.replace( 'data-motion-mode="none" data-step-count="0"', 'data-motion-mode="loop" data-step-count="2"', 1, ).replace( "", '' '', 1, ) check( "loop-two-semantic-items", loop_two, "loop mode allows at most one semantic item", ) example_errors = module.verify(EXAMPLE) if example_errors: raise AssertionError(f"shipped example: expected pass, got {example_errors}") print("OK: shipped animated example accepted") shipped = module.shipped_motion_files() required_shipped = {TEMPLATE, EXAMPLE} if not required_shipped.issubset(set(shipped)): raise AssertionError(f"incomplete shipped motion inventory: {shipped}") print("OK: required shipped motion inventory is parsed and complete") check( "bad-mode", source.replace('data-motion-mode="step"', 'data-motion-mode="cinematic"', 1), "data-motion-mode must be one of", ) check( "too-many-steps", source.replace('data-step-count="5"', 'data-step-count="9"', 1), "semantic step count must be 1..8", ) check( "step-gap", source.replace('data-step="4" aria-label="Step 4', 'data-step="3" aria-label="Step 4', 1), "semantic steps must be contiguous", ) check( "missing-pause", source.replace('data-motion-action="pause"', 'data-motion-action="stop"', 1), "missing actions: pause", ) check( "bad-live-region", source.replace('aria-live="polite"', 'aria-live="assertive"', 1), "motion status needs role=status", ) status_line = ( ' ' ) check( "status-inside-hidden-controls", source.replace( f"