Ctrl K
ComunidadGuías…

tests/test_validate_inline.py

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
import sys
import tempfile
import unittest
from pathlib import Path

REPO_ROOT = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(REPO_ROOT / "skills" / "caveman-compress"))

from scripts.validate import (  # noqa: E402
    ValidationResult,
    extract_code_blocks,
    extract_inline_codes,
    extract_paths,
    extract_urls,
    validate,
    validate_inline_codes,
)


class TestUrlInBackticks(unittest.TestCase):
    """A backtick is never a URL character; a URL in inline code used to be
    extracted with the closing backtick glued on (#163)."""

    def test_trailing_backtick_not_part_of_url(self):
        self.assertEqual(extract_urls("see `https://x.io/a` now"), {"https://x.io/a"})


class TestTrailingPunctuation(unittest.TestCase):
    """#1353: punctuation right after a URL or path was extracted as part of it,
    so a reply that only moved a comma or period failed validation."""

    def test_trailing_punctuation_not_part_of_url(self):
        self.assertEqual(
            extract_urls("See https://docs.example.com/billing/v2, then https://x.io/a."),
            {"https://docs.example.com/billing/v2", "https://x.io/a"},
        )

    def test_trailing_punctuation_not_part_of_path(self):
        self.assertEqual(extract_paths("Read docs/runbook.md. Then ./run.sh!"), {"docs/runbook.md", "./run.sh"})

    def test_dot_inside_path_kept(self):
        self.assertEqual(extract_paths("Edit src/app.config.ts"), {"src/app.config.ts"})

    def test_moved_comma_and_period_validate(self):
        orig = "# T\n\nSee https://docs.example.com/billing/v2 and docs/runbook.md for details.\n"
        comp = "# T\n\nSee https://docs.example.com/billing/v2, docs/runbook.md.\n"
        with tempfile.TemporaryDirectory() as tmp:
            o, c = Path(tmp) / "a.original.md", Path(tmp) / "a.md"
            o.write_text(orig, encoding="utf-8")
            c.write_text(comp, encoding="utf-8")
            result = validate(o, c)
        self.assertTrue(result.is_valid, result.errors)


class TestIndentedFence(unittest.TestCase):
    """#820: a fence indented 4+ spaces (nested in a list item) made the file
    permanently uncompressible. FENCE_OPEN_REGEX does not match it, so
    extract_code_blocks never removed it and its own backticks leaked into
    inline-code pairing, shifting every subsequent span."""

    NESTED = "# T\n\n* Example:\n    ```markdown\n    x = 1\n    ```\n* Use `alpha` and `beta` here.\n"

    def test_indented_fence_markers_not_leaked_as_inline(self):
        self.assertEqual(extract_inline_codes(self.NESTED), ["alpha", "beta"])

    def test_nested_fence_file_validates_against_itself(self):
        result = ValidationResult()
        validate_inline_codes(self.NESTED, self.NESTED, result)
        self.assertTrue(result.is_valid, result.errors)

    def test_deeply_indented_fence_markers_not_leaked(self):
        text = "* a\n  * b\n        ```\n        y = 2\n        ```\n* Use `gamma`.\n"
        self.assertEqual(extract_inline_codes(text), ["gamma"])

    def test_tilde_fence_indented_in_list(self):
        text = "* Example:\n    ~~~python\n    z = 3\n    ~~~\n* Use `delta`.\n"
        self.assertEqual(extract_inline_codes(text), ["delta"])


class TestIndentedFenceDoesNotSwallow(unittest.TestCase):
    """Widening FENCE_OPEN_REGEX to `\\s*` is the tempting fix for #820 and is a
    net regression: a lone indented ``` shown as an example then opens a block
    that runs to the next bare fence, hiding a REAL code block from validation.
    That converts a false failure into a false PASS, and a false PASS
    overwrites the user's original file with unvalidated output."""

    def test_lone_indented_marker_does_not_capture_the_real_block(self):
        doc = "To open a fence write:\n\n    ```\n\nThen prose with `alpha`.\n\n```js\nreal = 1\n```\n"
        # The lone indented marker is itself a CommonMark indented code block, so
        # it is now extracted as one (and must be preserved — it is literal
        # content the document is SHOWING). What must never happen is it opening
        # a fence that swallows the real block: `real = 1` stays its own entry.
        # Order is document position (the indented marker appears before the
        # real fenced block), not extraction type.
        self.assertEqual(
            extract_code_blocks(doc),
            ["    ```", "```js\nreal = 1\n```"],
        )
        self.assertEqual(extract_inline_codes(doc), ["alpha"])

    def test_content_loss_after_an_indented_marker_still_fails(self):
        orig = (
            "# Runbook\n\nA fence opens with three backticks:\n\n    ```\n\n"
            "Emergency rollback:\n\n```\nhelm rollback prod 41 --namespace production\n```\n"
        )
        comp = orig.replace("prod 41", "prod")
        with tempfile.TemporaryDirectory() as tmp:
            o, c = Path(tmp) / "o.md", Path(tmp) / "c.md"
            o.write_text(orig, encoding="utf-8")
            c.write_text(comp, encoding="utf-8")
            self.assertFalse(validate(o, c).is_valid, "dropped revision number must be caught")


class TestMultiLineSpansStillCompared(unittest.TestCase):
    """CommonMark permits a line ending inside a code span, and hard-wrapped
    markdown produces them. Restricting the span pattern to a single line drops
    those spans from the comparison set entirely, which silently downgrades a
    deleted or mutated span to PASS."""

    def test_multiline_span_is_extracted(self):
        self.assertEqual(
            extract_inline_codes("Run `npm install --save-dev\nsome-package` first and `x` after."),
            ["npm install --save-dev\nsome-package", "x"],
        )

    def test_deleting_a_multiline_span_is_an_error(self):
        result = ValidationResult()
        validate_inline_codes(
            "Pass the `--dangerously-skip-permissions\nflag` before running.",
            "Run it.",
            result,
        )
        self.assertFalse(result.is_valid)

    def test_mutating_a_multiline_span_is_an_error_not_a_warning(self):
        result = ValidationResult()
        validate_inline_codes("Set `--flag\nvalue` now.", "Set `--flag other` now.", result)
        self.assertFalse(result.is_valid, "a changed CLI argument must not pass as a warning")


class TestMidLineFenceRun(unittest.TestCase):
    """A backtick run in the MIDDLE of a prose line ("inside a ```diff fence")
    is matched by neither FENCE_OPEN_REGEX nor FENCE_MARKER_LINE_REGEX, so its
    backticks used to leak into inline-code pairing. The naive `([^`]+)`
    pattern then opened a span at the run's trailing backtick and closed it at
    the NEXT real span's opening backtick, shifting every following pair and
    making the file permanently uncompressible."""

    def test_midline_triple_backtick_does_not_shift_pairing(self):
        text = "Put the result inside a ```diff fence. Then state:\n\n- count of `BEGIN PRIVATE KEY` matches\n- check `base64 -d` output\n"
        self.assertEqual(extract_inline_codes(text), ["BEGIN PRIVATE KEY", "base64 -d"])

    def test_midline_run_file_validates_against_itself(self):
        text = "Wrap it in a ```json fence and run `jq .` after.\n"
        result = ValidationResult()
        validate_inline_codes(text, text, result)
        self.assertTrue(result.is_valid, result.errors)

    def test_span_after_midline_run_still_protected(self):
        orig = "Use a ```diff fence, then run `git apply /tmp/x.patch` to apply.\n"
        comp = "Use a ```diff fence, then apply.\n"
        result = ValidationResult()
        validate_inline_codes(orig, comp, result)
        self.assertFalse(result.is_valid, "span dropped after a mid-line run must be caught")


class TestErrorRendering(unittest.TestCase):
    """#820's failures were undiagnosable because a garbled span was printed
    whole. That is a presentation problem — fix it in the message, not by
    narrowing what counts as a span."""

    def test_long_span_is_truncated_and_newlines_escaped(self):
        result = ValidationResult()
        validate_inline_codes("a `" + "x" * 500 + "\nmore` b", "a b", result)
        message = result.errors[0]
        self.assertLess(len(message), 200, message)
        self.assertNotIn("\n", message[len("Inline code lost: "):])


class TestValidateInlineCodes(unittest.TestCase):
    def test_match(self):
        result = ValidationResult()
        validate_inline_codes("use `cmd` here", "use `cmd` here", result)
        self.assertTrue(result.is_valid)

    def test_lost(self):
        result = ValidationResult()
        validate_inline_codes("use `cmd` here", "use  here", result)
        self.assertFalse(result.is_valid)
        self.assertIn("Inline code lost", result.errors[0])

    def test_added(self):
        result = ValidationResult()
        validate_inline_codes("use  here", "use `new` here", result)
        self.assertTrue(result.is_valid)
        self.assertIn("Inline code added", result.warnings[0])

    def test_empty_orig(self):
        result = ValidationResult()
        validate_inline_codes("no codes", "use `new` here", result)
        self.assertTrue(result.is_valid)

    def test_both_empty(self):
        result = ValidationResult()
        validate_inline_codes("plain text", "also plain", result)
        self.assertTrue(result.is_valid)


class TestValidateIntegration(unittest.TestCase):
    def test_validate_inline_codes_wired(self):
        with tempfile.TemporaryDirectory() as tmp:
            orig = Path(tmp) / "original.md"
            comp = Path(tmp) / "compressed.md"
            orig.write_text("Run `rm -rf /` to delete", encoding="utf-8")
            comp.write_text("Run  to delete", encoding="utf-8")
            result = validate(orig, comp)
            self.assertFalse(result.is_valid)
            self.assertTrue(any("Inline code lost" in e for e in result.errors))



class TestInnerBacktickSpans(unittest.TestCase):
    """CommonMark lets a span carry a backtick run of another length inside it
    (``a`b``). A content class that excludes every backtick cannot match such a
    span at all, so a rewrite of its code went unvalidated."""

    def test_double_backtick_span_with_inner_backtick_is_extracted(self):
        self.assertEqual(extract_inline_codes("use ``a`b`` here and `c`"), ["a`b", "c"])

    def test_single_backtick_span_with_inner_double_run(self):
        self.assertEqual(extract_inline_codes("see `a``b` now"), ["a``b"])

    def test_inner_backtick_span_rewrite_still_fails_validation(self):
        orig = "# T\n\nRun ``git log --format=`%h` -1`` first.\n"
        comp = orig.replace("%h", "%H")
        result = ValidationResult()
        validate_inline_codes(orig, comp, result)
        self.assertFalse(result.is_valid, result.errors)

class TestIndentedCodeIsValidated(unittest.TestCase):
    """A 4-space-indented code block is code. It used to be prose to the
    validator: extract_code_blocks saw only fenced blocks, so "code blocks
    preserved exactly" compared empty to empty and PASSED while the compressor
    rewrote the command. A clean pass on a mutated destructive command is the
    worst failure mode this tool has — it overwrites the user's file."""

    def test_mutated_indented_command_fails(self):
        orig = "# Cleanup\n\nRun this:\n\n    kubectl delete pod --all -n prod\n\nDone.\n"
        comp = "# Cleanup\n\nRun this:\n\n    kubectl delete pod -n dev\n\nDone.\n"
        with tempfile.TemporaryDirectory() as tmp:
            o, c = Path(tmp) / "o.md", Path(tmp) / "c.md"
            o.write_text(orig, encoding="utf-8")
            c.write_text(comp, encoding="utf-8")
            result = validate(o, c)
            self.assertFalse(result.is_valid, "a mutated indented command must not pass")

    def test_nested_bullets_are_not_code(self):
        """Four spaces inside a list item is the item's content indentation.
        Treating it as code would make ordinary nested prose uncompressible."""
        doc = "# Doc\n\n- a bullet\n    - nested prose that should stay compressible\n"
        self.assertEqual(extract_code_blocks(doc), [])


class TestCrossTypeBlockOrderIsValidated(unittest.TestCase):
    """extract_code_blocks used to return all fenced blocks (in doc order)
    followed by all indented blocks (in doc order), so a fenced block and an
    indented block swapping RELATIVE position produced the same concatenated
    list on both sides of the diff. validate_code_blocks compares this list
    positionally, so the swap passed silently even though the invariant it
    checks (code block order preserved) was violated."""

    ORIG = "Intro.\n\n```python\nprint('hi')\n```\n\nMiddle.\n\n    kubectl delete pod --all -n prod\n\nEnd.\n"
    # Same two blocks, unchanged content, but the indented block now comes
    # BEFORE the fenced block instead of after it.
    SWAPPED = "Intro.\n\n    kubectl delete pod --all -n prod\n\nMiddle.\n\n```python\nprint('hi')\n```\n\nEnd.\n"

    def test_extraction_reflects_document_order(self):
        self.assertEqual(
            extract_code_blocks(self.ORIG),
            ["```python\nprint('hi')\n```", "    kubectl delete pod --all -n prod"],
        )
        self.assertEqual(
            extract_code_blocks(self.SWAPPED),
            ["    kubectl delete pod --all -n prod", "```python\nprint('hi')\n```"],
        )

    def test_swapped_block_order_fails_validation(self):
        with tempfile.TemporaryDirectory() as tmp:
            o, c = Path(tmp) / "o.md", Path(tmp) / "c.md"
            o.write_text(self.ORIG, encoding="utf-8")
            c.write_text(self.SWAPPED, encoding="utf-8")
            result = validate(o, c)
            self.assertFalse(result.is_valid, "a swapped block order must not pass")

    def test_identical_document_still_passes(self):
        with tempfile.TemporaryDirectory() as tmp:
            o, c = Path(tmp) / "o.md", Path(tmp) / "c.md"
            o.write_text(self.ORIG, encoding="utf-8")
            c.write_text(self.ORIG, encoding="utf-8")
            result = validate(o, c)
            self.assertTrue(result.is_valid, result.errors)


class TestPreservationPromisesAreErrors(unittest.TestCase):
    """SKILL.md and CLAUDE.md both state headings and file paths survive
    compression. Only heading COUNT was enforced; heading text and paths were
    warnings, so a run that renamed every heading and dropped a referenced path
    reported "Validation passed" and the in-place overwrite stood."""

    def _validate(self, orig, comp):
        with tempfile.TemporaryDirectory() as tmp:
            o, c = Path(tmp) / "o.md", Path(tmp) / "c.md"
            o.write_text(orig, encoding="utf-8")
            c.write_text(comp, encoding="utf-8")
            return validate(o, c)

    def test_renamed_heading_fails(self):
        orig = "# Configuration Options\n\nSome prose about the options here.\n"
        comp = "# Config\n\nOptions prose.\n"
        self.assertFalse(self._validate(orig, comp).is_valid)

    def test_dropped_path_fails(self):
        orig = "# Hooks\n\nThe shared module lives at src/hooks/caveman-config.js and is required.\n"
        comp = "# Hooks\n\nShared module required.\n"
        self.assertFalse(self._validate(orig, comp).is_valid)


if __name__ == "__main__":
    unittest.main()