c5a4d559a2
- Added blank lines for better separation of test cases in multiple test files. - Reformatted event handling in tests for clarity and consistency. - Ensured consistent use of multi-line formatting for dictionary arguments in event handling. - Improved assertions and test descriptions for better understanding. - Updated test cases across various modules including test_stream_state, test_stream_utils, test_summarization, test_thread_selector, test_tool_error_handler, test_tui_widgets, test_ui_runtime, and test_wechat_channel.
252 lines
9.0 KiB
Python
252 lines
9.0 KiB
Python
"""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
|