"""Tests for ToolResultCompactor.""" import asyncio import tempfile from datetime import datetime, timedelta from pathlib import Path from agentscope.message import Msg from reme.memory.file_based.components import ToolResultCompactor from reme.memory.file_based.utils import TRUNCATION_NOTICE_MARKER def create_tool_result_msg(output: str | list, tool_name: str = "test_tool") -> Msg: """Create a Msg with tool_result content block.""" return Msg( name="tool", role="user", content=[ { "type": "tool_result", "id": "call_123", "name": tool_name, "output": output, }, ], ) class TestToolResultCompactor: """Tests for ToolResultCompactor.""" def test_no_truncation_when_under_threshold(self): """Test that short content is not truncated.""" with tempfile.TemporaryDirectory() as tmpdir: op = ToolResultCompactor(tool_result_dir=tmpdir, recent_max_bytes=1000) messages = [create_tool_result_msg("short content")] result = asyncio.run(op.call(messages=messages)) assert result == messages assert messages[0].content[0]["output"] == "short content" assert len(list(Path(tmpdir).glob("*.txt"))) == 0 def test_truncation_when_over_threshold(self): """Test that long content is truncated and saved to file.""" with tempfile.TemporaryDirectory() as tmpdir: op = ToolResultCompactor(tool_result_dir=tmpdir, recent_max_bytes=100) long_content = "x" * 500 messages = [create_tool_result_msg(long_content)] _ = asyncio.run(op.call(messages=messages)) output = messages[0].content[0]["output"] assert TRUNCATION_NOTICE_MARKER in output assert "[Full content saved to:" in output # Verify file was created files = list(Path(tmpdir).glob("*.txt")) assert len(files) == 1 # Verify file content content = files[0].read_text() assert "# tool_name: test_tool" in content assert "# created_at:" in content assert long_content in content def test_skip_already_truncated(self): """Test that already truncated content is not re-truncated.""" with tempfile.TemporaryDirectory() as tmpdir: op = ToolResultCompactor(tool_result_dir=tmpdir, recent_max_bytes=100) truncated_content = "head<<>>(100 chars omitted)<<>>tail" messages = [create_tool_result_msg(truncated_content)] asyncio.run(op.call(messages=messages)) assert messages[0].content[0]["output"] == truncated_content assert len(list(Path(tmpdir).glob("*.txt"))) == 0 def test_truncation_list_output(self): """Test truncation of list output with text blocks.""" with tempfile.TemporaryDirectory() as tmpdir: op = ToolResultCompactor(tool_result_dir=tmpdir, recent_max_bytes=100) list_output = [{"type": "text", "text": "y" * 500}] messages = [create_tool_result_msg(list_output)] asyncio.run(op.call(messages=messages)) text_block = messages[0].content[0]["output"][0] assert TRUNCATION_NOTICE_MARKER in text_block["text"] assert len(list(Path(tmpdir).glob("*.txt"))) == 1 def test_list_output_no_truncation_when_short(self): """Test that short list output is not truncated.""" with tempfile.TemporaryDirectory() as tmpdir: op = ToolResultCompactor(tool_result_dir=tmpdir, recent_max_bytes=1000) list_output = [{"type": "text", "text": "short"}] messages = [create_tool_result_msg(list_output)] asyncio.run(op.call(messages=messages)) assert messages[0].content[0]["output"][0]["text"] == "short" assert len(list(Path(tmpdir).glob("*.txt"))) == 0 def test_list_output_multiple_text_blocks(self): """Test truncation of multiple text blocks in list output.""" with tempfile.TemporaryDirectory() as tmpdir: op = ToolResultCompactor(tool_result_dir=tmpdir, recent_max_bytes=100) list_output = [ {"type": "text", "text": "a" * 500}, {"type": "text", "text": "short"}, {"type": "text", "text": "b" * 500}, ] messages = [create_tool_result_msg(list_output)] asyncio.run(op.call(messages=messages)) output = messages[0].content[0]["output"] assert TRUNCATION_NOTICE_MARKER in output[0]["text"] assert output[1]["text"] == "short" # unchanged assert TRUNCATION_NOTICE_MARKER in output[2]["text"] assert len(list(Path(tmpdir).glob("*.txt"))) == 2 def test_list_output_mixed_block_types(self): """Test that non-text blocks in list output are unchanged.""" with tempfile.TemporaryDirectory() as tmpdir: op = ToolResultCompactor(tool_result_dir=tmpdir, recent_max_bytes=100) list_output = [ {"type": "text", "text": "c" * 500}, {"type": "image", "source": {"type": "url", "url": "http://example.com/img.png"}}, ] messages = [create_tool_result_msg(list_output)] asyncio.run(op.call(messages=messages)) output = messages[0].content[0]["output"] assert TRUNCATION_NOTICE_MARKER in output[0]["text"] assert output[1] == {"type": "image", "source": {"type": "url", "url": "http://example.com/img.png"}} assert len(list(Path(tmpdir).glob("*.txt"))) == 1 def test_cleanup_expired_files(self): """Test cleanup of expired files.""" with tempfile.TemporaryDirectory() as tmpdir: op = ToolResultCompactor(tool_result_dir=tmpdir, recent_max_bytes=100, retention_days=1) # Create an old file old_time = (datetime.now() - timedelta(days=2)).isoformat() old_file = Path(tmpdir) / "old_file.txt" old_file.write_text(f"# tool_name: test\n# created_at: {old_time}\n# ---\ncontent") # Create a new file new_time = datetime.now().isoformat() new_file = Path(tmpdir) / "new_file.txt" new_file.write_text(f"# tool_name: test\n# created_at: {new_time}\n# ---\ncontent") deleted = op.cleanup_expired_files() assert deleted == 1 assert not old_file.exists() assert new_file.exists() def test_string_content_msg_unchanged(self): """Test that messages with string content are unchanged.""" with tempfile.TemporaryDirectory() as tmpdir: op = ToolResultCompactor(tool_result_dir=tmpdir, recent_max_bytes=100) messages = [Msg(name="user", role="user", content="hello world")] asyncio.run(op.call(messages=messages)) assert messages[0].content == "hello world" if __name__ == "__main__": import pytest pytest.main([__file__, "-v"])