diff --git a/headroom/transforms/content_router.py b/headroom/transforms/content_router.py index 4a4bbc8d0..a6c0ab411 100644 --- a/headroom/transforms/content_router.py +++ b/headroom/transforms/content_router.py @@ -1449,6 +1449,7 @@ class ContentRouter(Transform): Returns: RouterCompressionResult with compressed content and routing metadata. """ + context = context or "" debug_enabled = logger.isEnabledFor(logging.DEBUG) request_debug = ( { diff --git a/headroom/transforms/diff_compressor.py b/headroom/transforms/diff_compressor.py index 95885f651..4d1e2984e 100644 --- a/headroom/transforms/diff_compressor.py +++ b/headroom/transforms/diff_compressor.py @@ -103,7 +103,7 @@ class DiffCompressor: ) def compress(self, content: str, context: str = "") -> DiffCompressionResult: - r = self._rust.compress(content, context) + r = self._rust.compress(content, context or "") cache_key: str | None = r.cache_key if cache_key is not None: # Mirror log_compressor.py + search_compressor.py: when the diff --git a/tests/test_transforms/test_content_router.py b/tests/test_transforms/test_content_router.py index 7d53e584a..16c3e7f39 100644 --- a/tests/test_transforms/test_content_router.py +++ b/tests/test_transforms/test_content_router.py @@ -9,6 +9,7 @@ Comprehensive tests covering: """ import json +import logging from types import SimpleNamespace import pytest @@ -472,6 +473,23 @@ class TestContentRouter: assert result.original == content assert result.strategy_used is not None + def test_compress_diff_accepts_none_context_with_debug(self, router, caplog): + """None context is normalized before debug logging and compressor dispatch.""" + + class FakeDiffCompressor: + def compress(self, content, context): + assert context == "" + return SimpleNamespace(compressed="diff summary") + + diff = "diff --git a/file.py b/file.py\n@@ -1 +1 @@\n-old\n+new\n" + router._diff_compressor = FakeDiffCompressor() + + caplog.set_level(logging.DEBUG, logger="headroom.transforms.content_router") + result = router.compress(diff, context=None) + + assert result.compressed == "diff summary" + assert result.strategy_used == CompressionStrategy.DIFF + def test_name_property(self, router): """Router has correct name.""" assert router.name == "content_router" diff --git a/tests/test_transforms/test_diff_compressor.py b/tests/test_transforms/test_diff_compressor.py index 522434767..8490d1536 100644 --- a/tests/test_transforms/test_diff_compressor.py +++ b/tests/test_transforms/test_diff_compressor.py @@ -24,6 +24,30 @@ from headroom.transforms.diff_compressor import ( ) +def _fake_diff_result(compressed: str = "compressed") -> DiffCompressionResult: + return DiffCompressionResult( + compressed=compressed, + original_line_count=1, + compressed_line_count=1, + files_affected=1, + additions=0, + deletions=0, + hunks_kept=1, + hunks_removed=0, + ) + + +class _FakeRustDiffCompressor: + def __init__(self) -> None: + self.contexts: list[str] = [] + + def compress(self, content: str, context: str): + if context is None: + raise AssertionError("Rust diff compressor received None context") + self.contexts.append(context) + return _fake_diff_result(content) + + class TestContextReduction: """Tests for context line reduction.""" @@ -374,6 +398,32 @@ class TestEdgeCases: assert result.compressed is not None +class TestContextNormalization: + """Tests for the Python-to-Rust diff compressor boundary.""" + + def test_none_and_omitted_context_become_empty_string(self) -> None: + compressor = object.__new__(DiffCompressor) + fake_rust = _FakeRustDiffCompressor() + compressor._rust = fake_rust + + diff = "diff --git a/file.py b/file.py\n--- a/file.py\n+++ b/file.py\n" + + compressor.compress(diff, context=None) + compressor.compress(diff) + + assert fake_rust.contexts == ["", ""] + + def test_non_empty_context_passes_through_unchanged(self) -> None: + compressor = object.__new__(DiffCompressor) + fake_rust = _FakeRustDiffCompressor() + compressor._rust = fake_rust + + diff = "diff --git a/file.py b/file.py\n--- a/file.py\n+++ b/file.py\n" + compressor.compress(diff, context="question context") + + assert fake_rust.contexts == ["question context"] + + class TestConfigOptions: """Tests for configuration options."""