| """Tests for agent/display.py — build_tool_preview() and inline diff previews.""" |
|
|
| import os |
| import pytest |
| from unittest.mock import MagicMock, patch |
|
|
| from agent.display import ( |
| build_tool_preview, |
| capture_local_edit_snapshot, |
| extract_edit_diff, |
| _render_inline_unified_diff, |
| _summarize_rendered_diff_sections, |
| render_edit_diff_with_delta, |
| ) |
|
|
|
|
| class TestBuildToolPreview: |
| """Tests for build_tool_preview defensive handling and normal operation.""" |
|
|
| def test_none_args_returns_none(self): |
| """PR #453: None args should not crash, should return None.""" |
| assert build_tool_preview("terminal", None) is None |
|
|
| def test_empty_dict_returns_none(self): |
| """Empty dict has no keys to preview.""" |
| assert build_tool_preview("terminal", {}) is None |
|
|
| def test_known_tool_with_primary_arg(self): |
| """Known tool with its primary arg should return a preview string.""" |
| result = build_tool_preview("terminal", {"command": "ls -la"}) |
| assert result is not None |
| assert "ls -la" in result |
|
|
| def test_web_search_preview(self): |
| result = build_tool_preview("web_search", {"query": "hello world"}) |
| assert result is not None |
| assert "hello world" in result |
|
|
| def test_read_file_preview(self): |
| result = build_tool_preview("read_file", {"path": "/tmp/test.py", "offset": 1}) |
| assert result is not None |
| assert "/tmp/test.py" in result |
|
|
| def test_unknown_tool_with_fallback_key(self): |
| """Unknown tool but with a recognized fallback key should still preview.""" |
| result = build_tool_preview("custom_tool", {"query": "test query"}) |
| assert result is not None |
| assert "test query" in result |
|
|
| def test_unknown_tool_no_matching_key(self): |
| """Unknown tool with no recognized keys should return None.""" |
| result = build_tool_preview("custom_tool", {"foo": "bar"}) |
| assert result is None |
|
|
| def test_long_value_truncated(self): |
| """Preview should truncate long values.""" |
| long_cmd = "a" * 100 |
| result = build_tool_preview("terminal", {"command": long_cmd}, max_len=40) |
| assert result is not None |
| assert len(result) <= 43 |
|
|
| def test_process_tool_with_none_args(self): |
| """Process tool special case should also handle None args.""" |
| assert build_tool_preview("process", None) is None |
|
|
| def test_process_tool_normal(self): |
| result = build_tool_preview("process", {"action": "poll", "session_id": "abc123"}) |
| assert result is not None |
| assert "poll" in result |
|
|
| def test_todo_tool_read(self): |
| result = build_tool_preview("todo", {"merge": False}) |
| assert result is not None |
| assert "reading" in result |
|
|
| def test_todo_tool_with_todos(self): |
| result = build_tool_preview("todo", {"todos": [{"id": "1", "content": "test", "status": "pending"}]}) |
| assert result is not None |
| assert "1 task" in result |
|
|
| def test_memory_tool_add(self): |
| result = build_tool_preview("memory", {"action": "add", "target": "user", "content": "test note"}) |
| assert result is not None |
| assert "user" in result |
|
|
| def test_memory_replace_missing_old_text_marked(self): |
| |
| result = build_tool_preview("memory", {"action": "replace", "target": "memory"}) |
| assert result == '~memory: "<missing old_text>"' |
| result = build_tool_preview("memory", {"action": "remove", "target": "memory", "old_text": None}) |
| assert result == '-memory: "<missing old_text>"' |
|
|
| def test_session_search_preview(self): |
| result = build_tool_preview("session_search", {"query": "find something"}) |
| assert result is not None |
| assert "find something" in result |
|
|
| def test_false_like_args_zero(self): |
| """Non-dict falsy values should return None, not crash.""" |
| assert build_tool_preview("terminal", 0) is None |
| assert build_tool_preview("terminal", "") is None |
| assert build_tool_preview("terminal", []) is None |
|
|
|
|
| class TestEditDiffPreview: |
| def test_extract_edit_diff_for_patch(self): |
| diff = extract_edit_diff("patch", '{"success": true, "diff": "--- a/x\\n+++ b/x\\n"}') |
| assert diff is not None |
| assert "+++ b/x" in diff |
|
|
| def test_render_inline_unified_diff_colors_added_and_removed_lines(self): |
| rendered = _render_inline_unified_diff( |
| "--- a/cli.py\n" |
| "+++ b/cli.py\n" |
| "@@ -1,2 +1,2 @@\n" |
| "-old line\n" |
| "+new line\n" |
| " context\n" |
| ) |
|
|
| assert "a/cli.py" in rendered[0] |
| assert "b/cli.py" in rendered[0] |
| assert any("old line" in line for line in rendered) |
| assert any("new line" in line for line in rendered) |
| assert any("48;2;" in line for line in rendered) |
|
|
| def test_extract_edit_diff_ignores_non_edit_tools(self): |
| assert extract_edit_diff("web_search", '{"diff": "--- a\\n+++ b\\n"}') is None |
|
|
| def test_extract_edit_diff_uses_local_snapshot_for_write_file(self, tmp_path): |
| target = tmp_path / "note.txt" |
| target.write_text("old\n", encoding="utf-8") |
|
|
| snapshot = capture_local_edit_snapshot("write_file", {"path": str(target)}) |
|
|
| target.write_text("new\n", encoding="utf-8") |
|
|
| diff = extract_edit_diff( |
| "write_file", |
| '{"bytes_written": 4}', |
| function_args={"path": str(target)}, |
| snapshot=snapshot, |
| ) |
|
|
| assert diff is not None |
| assert "--- a/" in diff |
| assert "+++ b/" in diff |
| assert "-old" in diff |
| assert "+new" in diff |
|
|
| def test_render_edit_diff_with_delta_invokes_printer(self): |
| printer = MagicMock() |
|
|
| rendered = render_edit_diff_with_delta( |
| "patch", |
| '{"diff": "--- a/x\\n+++ b/x\\n@@ -1 +1 @@\\n-old\\n+new\\n"}', |
| print_fn=printer, |
| ) |
|
|
| assert rendered is True |
| assert printer.call_count >= 2 |
| calls = [call.args[0] for call in printer.call_args_list] |
| assert any("a/x" in line and "b/x" in line for line in calls) |
| assert any("old" in line for line in calls) |
| assert any("new" in line for line in calls) |
|
|
| def test_render_edit_diff_with_delta_skips_without_diff(self): |
| rendered = render_edit_diff_with_delta( |
| "patch", |
| '{"success": true}', |
| ) |
|
|
| assert rendered is False |
|
|
| def test_render_edit_diff_with_delta_handles_renderer_errors(self, monkeypatch): |
| printer = MagicMock() |
|
|
| monkeypatch.setattr("agent.display._summarize_rendered_diff_sections", MagicMock(side_effect=RuntimeError("boom"))) |
|
|
| rendered = render_edit_diff_with_delta( |
| "patch", |
| '{"diff": "--- a/x\\n+++ b/x\\n"}', |
| print_fn=printer, |
| ) |
|
|
| assert rendered is False |
| assert printer.call_count == 0 |
|
|
| def test_summarize_rendered_diff_sections_truncates_large_diff(self): |
| diff = "--- a/x.py\n+++ b/x.py\n" + "".join(f"+line{i}\n" for i in range(120)) |
|
|
| rendered = _summarize_rendered_diff_sections(diff, max_lines=20) |
|
|
| assert len(rendered) == 21 |
| assert "omitted" in rendered[-1] |
|
|
| def test_summarize_rendered_diff_sections_limits_file_count(self): |
| diff = "".join( |
| f"--- a/file{i}.py\n+++ b/file{i}.py\n+line{i}\n" |
| for i in range(8) |
| ) |
|
|
| rendered = _summarize_rendered_diff_sections(diff, max_files=3, max_lines=50) |
|
|
| assert any("a/file0.py" in line for line in rendered) |
| assert any("a/file1.py" in line for line in rendered) |
| assert any("a/file2.py" in line for line in rendered) |
| assert not any("a/file7.py" in line for line in rendered) |
| assert "additional file" in rendered[-1] |
|
|