diff --git a/scripts/test_validate_examples.py b/scripts/test_validate_examples.py index f8338916a..103dd7373 100755 --- a/scripts/test_validate_examples.py +++ b/scripts/test_validate_examples.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# cspell:ignore shema directon +# cspell:ignore shema directon skiped """Contract conformance tests for validate_examples.py. Each test asserts one claim from the contract documented in @@ -261,6 +261,29 @@ def test_annotation_parsing() -> None: ) # Unknown attribute key rejected via reserved _error + # skip must be a whole word. Each typo below carries a valid reason=, so + # only a whole-word check can reject them — a reason-only guard cannot. + for typo in ( + 'skiped reason="oops"', + 'skip_the_check reason="oops"', + 'skipping validation reason="oops"', + ): + ann = v.parse_annotation(typo) + _check( + f"annotation_skip_typo_with_reason_not_skipped[{typo}]", + ann.get("skip") is None, + f"got {ann!r}", + ) + + # A bare or empty reason= is not auditable and must not skip. + for bad in ("skip", 'skip reason=""'): + ann = v.parse_annotation(bad) + _check( + f"annotation_skip_missing_reason_rejected[{bad}]", + ann.get("_error") is not None and ann.get("skip") is None, + f"got {ann!r}", + ) + ann = v.parse_annotation("shema=foo") # typo _check( "annotation_unknown_key_rejected", diff --git a/scripts/validate_examples.py b/scripts/validate_examples.py index 739d82b56..99b08adbb 100755 --- a/scripts/validate_examples.py +++ b/scripts/validate_examples.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# cspell:ignore shema directon +# cspell:ignore shema directon skiped """Validate JSON examples in UCP specification documentation. UCP doc examples use a bespoke JSON capability set: strict JSON plus @@ -136,6 +136,10 @@ {"schema", "op", "direction", "extract", "target", "def"} ) +# `skip` must appear as a whole word. A prefix match would let a typo such as +# `skiped` or `skip_the_check` silently disable validation for that example. +_SKIP_RE = re.compile(r"skip\b") + # ----------------------------------------------------------- # Annotation parsing # ----------------------------------------------------------- @@ -149,11 +153,18 @@ def parse_annotation(text: str) -> dict: reported via a reserved "_error" key (consumed by process_block). """ text = text.strip() - if text.startswith("skip"): - reason_match = re.search(r'reason="([^"]*)"', text) + if _SKIP_RE.match(text): + reason_match = re.search(r'reason="([^"]+)"', text) + if reason_match is None: + return { + "_error": ( + 'skip annotation requires a non-empty reason="..." ' + "so every skipped example stays auditable" + ) + } return { "skip": True, - "reason": (reason_match.group(1) if reason_match else ""), + "reason": reason_match.group(1), } attrs: dict = {} unknown: list[str] = []