"""Tests for EvoScientist.stream.diff_format module.""" from __future__ import annotations import difflib from unittest import mock from EvoScientist.stream.diff_format import ( _detect_unicode_support, _escape_markup, build_edit_diff, format_diff_rich, ) # --------------------------------------------------------------------------- # _escape_markup # --------------------------------------------------------------------------- class TestEscapeMarkup: def test_escapes_brackets(self): assert _escape_markup("[bold]text[/bold]") == r"\[bold\]text\[/bold\]" def test_plain_text_unchanged(self): assert _escape_markup("hello world") == "hello world" def test_empty_string(self): assert _escape_markup("") == "" def test_nested_brackets(self): assert _escape_markup("a[b[c]]d") == r"a\[b\[c\]\]d" # --------------------------------------------------------------------------- # _detect_unicode_support # --------------------------------------------------------------------------- class TestDetectUnicodeSupport: def test_utf8_encoding(self): with mock.patch("sys.stdout") as mock_stdout: mock_stdout.encoding = "utf-8" assert _detect_unicode_support() is True def test_ascii_encoding_with_utf_lang(self): with mock.patch("sys.stdout") as mock_stdout: mock_stdout.encoding = "ascii" with mock.patch.dict("os.environ", {"LANG": "en_US.UTF-8", "LC_ALL": ""}): assert _detect_unicode_support() is True def test_ascii_encoding_no_utf_lang(self): with mock.patch("sys.stdout") as mock_stdout: mock_stdout.encoding = "ascii" with mock.patch.dict( "os.environ", {"LANG": "C", "LC_ALL": ""}, clear=False ): assert _detect_unicode_support() is False # --------------------------------------------------------------------------- # format_diff_rich # --------------------------------------------------------------------------- class TestFormatDiffRich: def test_empty_diff_returns_dim_message(self): result = format_diff_rich("") assert "No changes detected" in result assert "[dim]" in result def test_single_line_change(self): old = ["hello"] new = ["world"] diff = "\n".join(difflib.unified_diff(old, new, lineterm="", n=3)) result = format_diff_rich(diff) # Should contain addition and deletion markers assert "[green]+1[/green]" in result assert "[red]-1[/red]" in result # Deletion line (red background) assert "2d1515" in result # red background color # Addition line (green background) assert "152d15" in result # green background color def test_stats_header(self): old = ["a", "b", "c"] new = ["a", "x", "c", "d"] diff = "\n".join(difflib.unified_diff(old, new, lineterm="", n=3)) result = format_diff_rich(diff) # 1 deletion (b), 2 additions (x, d) assert "[green]+2[/green]" in result assert "[red]-1[/red]" in result def test_max_lines_truncation(self): old = [f"line{i}" for i in range(50)] new = [f"LINE{i}" for i in range(50)] diff = "\n".join(difflib.unified_diff(old, new, lineterm="", n=0)) result = format_diff_rich(diff, max_lines=10) assert "more lines" in result def test_context_lines_dimmed(self): old = ["a", "b", "c"] new = ["a", "B", "c"] diff = "\n".join(difflib.unified_diff(old, new, lineterm="", n=3)) result = format_diff_rich(diff) # Context lines should use dim styling assert "[dim]" in result def test_title_header(self): old = ["hello"] new = ["world"] diff = "\n".join(difflib.unified_diff(old, new, lineterm="", n=3)) result = format_diff_rich(diff, title="/path/to/file.py") # Title should appear with bold cyan and box-drawing chars assert "file.py" in result assert "[bold cyan]" in result def test_title_none_omits_header(self): old = ["hello"] new = ["world"] diff = "\n".join(difflib.unified_diff(old, new, lineterm="", n=3)) result = format_diff_rich(diff, title=None) assert "[bold cyan]" not in result def test_stats_footer(self): old = ["a"] new = ["b"] diff = "\n".join(difflib.unified_diff(old, new, lineterm="", n=3)) result = format_diff_rich(diff) # Stats should appear both at top and bottom lines = result.splitlines() # First non-empty line is stats header, last non-empty line is stats footer non_empty = [ln for ln in lines if ln.strip()] assert non_empty[0] == non_empty[-1] # header == footer def test_max_lines_none_unlimited(self): old = [f"line{i}" for i in range(200)] new = [f"LINE{i}" for i in range(200)] diff = "\n".join(difflib.unified_diff(old, new, lineterm="", n=0)) result = format_diff_rich(diff, max_lines=None) # Should NOT have truncation marker assert "more lines" not in result # --------------------------------------------------------------------------- # build_edit_diff # --------------------------------------------------------------------------- class TestBuildEditDiff: def test_returns_none_when_equal(self): assert build_edit_diff("/foo.py", "same", "same") is None def test_returns_none_when_both_empty(self): assert build_edit_diff("/foo.py", "", "") is None def test_returns_formatted_markup_for_valid_diff(self): result = build_edit_diff( "/foo.py", "old line", "new line", ) assert result is not None assert "[green]" in result assert "[red]" in result def test_multiline_diff(self): old = "line1\nline2\nline3" new = "line1\nmodified\nline3\nline4" result = build_edit_diff("/test.py", old, new) assert result is not None assert "+2" in result # 2 additions assert "-1" in result # 1 deletion def test_no_truncation_by_default(self): old = "\n".join(f"old{i}" for i in range(100)) new = "\n".join(f"new{i}" for i in range(100)) result = build_edit_diff("/big.py", old, new) assert result is not None # Default max_lines=None means no truncation assert "more lines" not in result def test_explicit_max_lines_truncates(self): old = "\n".join(f"old{i}" for i in range(100)) new = "\n".join(f"new{i}" for i in range(100)) result = build_edit_diff("/big.py", old, new, max_lines=5) assert result is not None assert "more lines" in result def test_file_path_shown_as_title(self): result = build_edit_diff("/my/file.py", "a", "b") assert result is not None # File path should appear as the title header assert "file.py" in result assert "[bold cyan]" in result # --------------------------------------------------------------------------- # Integration with format_tool_result_compact # --------------------------------------------------------------------------- class TestFormatToolResultCompactEditFile: def test_edit_file_with_tool_args_shows_diff(self): from EvoScientist.stream.display import format_tool_result_compact result = format_tool_result_compact( "edit_file", "[OK] Successfully replaced 1 instance(s)", tool_args={ "path": "/foo.py", "old_string": "hello", "new_string": "world", }, ) # Should return markup elements with diff content assert len(result) >= 1 plain = result[0].plain if hasattr(result[0], "plain") else str(result[0]) assert "+1" in plain or "world" in plain or "hello" in plain def test_edit_file_without_tool_args_falls_through(self): from EvoScientist.stream.display import format_tool_result_compact result = format_tool_result_compact( "edit_file", "[OK] Successfully replaced 1 instance(s)", ) # Without tool_args, falls through to normal rendering assert len(result) >= 1 def test_edit_file_error_shows_error_not_diff(self): from EvoScientist.stream.display import format_tool_result_compact result = format_tool_result_compact( "edit_file", "[ERROR] File not found", tool_args={ "path": "/foo.py", "old_string": "hello", "new_string": "world", }, ) # Error content should show error, not diff assert len(result) >= 1 def test_backward_compatible_no_tool_args(self): from EvoScientist.stream.display import format_tool_result_compact # Existing calls without tool_args should still work result = format_tool_result_compact("read_file", "[OK] 42 lines") assert len(result) >= 1