fischer-agentkit/tests/unit/test_headroom_compressor.py

362 lines
14 KiB
Python

"""HeadroomCompressor 单元测试
所有测试使用 mock headroom 模块,无需安装 headroom-ai。
"""
from unittest.mock import MagicMock, patch
import pytest
from agentkit.core.headroom_compressor import (
HeadroomCompressor,
_is_code_content,
_is_json_content,
)
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
def _make_headroom_compress_mock(return_content="compressed"):
"""创建 mock headroom.compress 函数,返回带有 messages 属性的结果对象"""
mock_result = MagicMock()
mock_result.messages = [{"role": "user", "content": return_content}]
return mock_result
def _long_json_content():
"""生成超过 min_length 的 JSON 内容"""
import json
items = [{"id": i, "name": f"item_{i}", "description": f"description for item {i}"} for i in range(50)]
return json.dumps({"items": items})
def _long_code_content():
"""生成超过 min_length 的代码内容"""
lines = []
for i in range(50):
lines.append(f"def function_{i}():")
lines.append(f" result = process_data({i})")
lines.append(f" return result")
return "\n".join(lines)
def _long_text_content():
"""生成超过 min_length 的纯文本内容"""
return "This is plain text content. " * 100
# ---------------------------------------------------------------------------
# TestHeadroomAvailability
# ---------------------------------------------------------------------------
class TestHeadroomAvailability:
"""测试 headroom-ai 可用性检测"""
def test_is_available_false_when_not_installed(self):
"""_HEADROOM_AVAILABLE=False 时 is_available() 返回 False"""
compressor = HeadroomCompressor({})
with patch("agentkit.core.headroom_compressor._HEADROOM_AVAILABLE", False):
assert compressor.is_available() is False
def test_is_available_true_when_installed(self):
"""_HEADROOM_AVAILABLE=True 时 is_available() 返回 True"""
compressor = HeadroomCompressor({})
with patch("agentkit.core.headroom_compressor._HEADROOM_AVAILABLE", True):
assert compressor.is_available() is True
# ---------------------------------------------------------------------------
# TestContentTypeDetection
# ---------------------------------------------------------------------------
class TestContentTypeDetection:
"""测试内容类型检测函数"""
def test_json_content_detected(self):
"""有效 JSON 对象被正确检测"""
assert _is_json_content('{"key": "value"}') is True
def test_json_array_detected(self):
"""有效 JSON 数组被正确检测"""
assert _is_json_content('[1, 2, 3]') is True
def test_non_json_content(self):
"""普通文本不被识别为 JSON"""
assert _is_json_content("hello world") is False
def test_invalid_json_start(self):
"""{ 开头但无效的 JSON 不被识别"""
assert _is_json_content("{invalid") is False
def test_code_content_detected(self):
"""Python 代码(含 def/class 关键字)被正确检测"""
code = "def hello():\n pass\n\nclass Foo:\n pass\nimport os\nfrom sys import path"
assert _is_code_content(code) is True
def test_non_code_content(self):
"""纯文本不被识别为代码"""
text = "This is just a regular paragraph of text with no code keywords at all."
assert _is_code_content(text) is False
# ---------------------------------------------------------------------------
# TestCompressToolResult
# ---------------------------------------------------------------------------
class TestCompressToolResult:
"""测试 compress_tool_result 方法"""
@pytest.mark.asyncio
async def test_short_content_not_compressed(self):
"""短于 min_length 的内容不压缩"""
compressor = HeadroomCompressor({"min_length": 500})
with patch("agentkit.core.headroom_compressor._HEADROOM_AVAILABLE", True):
result = await compressor.compress_tool_result("test_tool", "short content")
assert result == "short content"
@pytest.mark.asyncio
async def test_json_content_compressed_with_smart_crusher(self):
"""JSON 内容使用 smart_crusher 压缩"""
compressor = HeadroomCompressor({
"min_length": 100,
"compressors": ["smart_crusher", "code_compressor"],
})
json_content = _long_json_content()
mock_fn = MagicMock(return_value=_make_headroom_compress_mock("compressed json"))
with patch("agentkit.core.headroom_compressor._HEADROOM_AVAILABLE", True), \
patch("agentkit.core.headroom_compressor.headroom_compress", mock_fn):
result = await compressor.compress_tool_result("json_tool", json_content)
assert "compressed json" in result
assert "<!-- CCR:hash=" in result
mock_fn.assert_called_once()
@pytest.mark.asyncio
async def test_code_content_compressed_with_code_compressor(self):
"""代码内容使用 code_compressor 压缩"""
compressor = HeadroomCompressor({
"min_length": 100,
"compressors": ["smart_crusher", "code_compressor"],
})
code_content = _long_code_content()
mock_fn = MagicMock(return_value=_make_headroom_compress_mock("compressed code"))
with patch("agentkit.core.headroom_compressor._HEADROOM_AVAILABLE", True), \
patch("agentkit.core.headroom_compressor.headroom_compress", mock_fn):
result = await compressor.compress_tool_result("code_tool", code_content)
assert "compressed code" in result
assert "<!-- CCR:hash=" in result
@pytest.mark.asyncio
async def test_text_content_not_compressed(self):
"""纯文本内容无适用的压缩器,返回原始内容"""
compressor = HeadroomCompressor({
"min_length": 100,
"compressors": ["smart_crusher", "code_compressor"],
})
text_content = _long_text_content()
with patch("agentkit.core.headroom_compressor._HEADROOM_AVAILABLE", True):
result = await compressor.compress_tool_result("text_tool", text_content)
assert result == text_content
@pytest.mark.asyncio
async def test_compression_includes_ccr_hash(self):
"""压缩后的输出包含 CCR hash 标记"""
compressor = HeadroomCompressor({
"min_length": 100,
"compressors": ["smart_crusher"],
})
json_content = _long_json_content()
mock_fn = MagicMock(return_value=_make_headroom_compress_mock("small"))
with patch("agentkit.core.headroom_compressor._HEADROOM_AVAILABLE", True), \
patch("agentkit.core.headroom_compressor.headroom_compress", mock_fn):
result = await compressor.compress_tool_result("tool", json_content)
assert "<!-- CCR:hash=" in result
@pytest.mark.asyncio
async def test_headroom_not_available_returns_original(self):
"""headroom-ai 不可用时返回原始内容"""
compressor = HeadroomCompressor({"min_length": 100})
json_content = _long_json_content()
with patch("agentkit.core.headroom_compressor._HEADROOM_AVAILABLE", False):
result = await compressor.compress_tool_result("tool", json_content)
assert result == json_content
@pytest.mark.asyncio
async def test_compression_failure_falls_back(self):
"""headroom_compress 抛出异常时回退到原始内容"""
compressor = HeadroomCompressor({
"min_length": 100,
"compressors": ["smart_crusher"],
})
json_content = _long_json_content()
mock_fn = MagicMock(side_effect=RuntimeError("headroom error"))
with patch("agentkit.core.headroom_compressor._HEADROOM_AVAILABLE", True), \
patch("agentkit.core.headroom_compressor.headroom_compress", mock_fn):
result = await compressor.compress_tool_result("tool", json_content)
assert result == json_content
# ---------------------------------------------------------------------------
# TestCompress
# ---------------------------------------------------------------------------
class TestCompress:
"""测试 compress 方法(消息列表压缩)"""
@pytest.mark.asyncio
async def test_tool_messages_compressed(self):
"""role=tool 且内容足够长的消息被压缩"""
compressor = HeadroomCompressor({"min_length": 100})
long_content = "x" * 500
messages = [
{"role": "system", "content": "system prompt"},
{"role": "tool", "content": long_content, "tool_call_id": "call_1"},
]
mock_fn = MagicMock(return_value=_make_headroom_compress_mock("compressed"))
with patch("agentkit.core.headroom_compressor._HEADROOM_AVAILABLE", True), \
patch("agentkit.core.headroom_compressor.headroom_compress", mock_fn):
result = await compressor.compress(messages)
assert len(result) == 2
assert result[0]["role"] == "system"
assert result[1]["role"] == "tool"
assert "compressed" in result[1]["content"]
assert "<!-- CCR:hash=" in result[1]["content"]
@pytest.mark.asyncio
async def test_non_tool_messages_preserved(self):
"""system/user/assistant 消息保持不变"""
compressor = HeadroomCompressor({"min_length": 100})
messages = [
{"role": "system", "content": "system prompt"},
{"role": "user", "content": "x" * 500},
{"role": "assistant", "content": "y" * 500},
]
with patch("agentkit.core.headroom_compressor._HEADROOM_AVAILABLE", True):
result = await compressor.compress(messages)
assert result == messages
@pytest.mark.asyncio
async def test_short_tool_messages_not_compressed(self):
"""短于 min_length 的 tool 消息不压缩"""
compressor = HeadroomCompressor({"min_length": 500})
messages = [
{"role": "tool", "content": "short", "tool_call_id": "call_1"},
]
with patch("agentkit.core.headroom_compressor._HEADROOM_AVAILABLE", True):
result = await compressor.compress(messages)
assert result[0]["content"] == "short"
@pytest.mark.asyncio
async def test_headroom_not_available_returns_original(self):
"""headroom-ai 不可用时返回原始消息列表"""
compressor = HeadroomCompressor({"min_length": 100})
messages = [
{"role": "tool", "content": "x" * 500, "tool_call_id": "call_1"},
]
with patch("agentkit.core.headroom_compressor._HEADROOM_AVAILABLE", False):
result = await compressor.compress(messages)
assert result == messages
# ---------------------------------------------------------------------------
# TestCCRRetrieve
# ---------------------------------------------------------------------------
class TestCCRRetrieve:
"""测试 CCR 缓存检索"""
def test_retrieve_by_hash(self):
"""通过 hash 检索存储的原始内容"""
compressor = HeadroomCompressor({})
original = "this is the original content"
ccr_hash = compressor._store_ccr(original)
assert ccr_hash is not None
result = compressor.retrieve(ccr_hash=ccr_hash)
assert result["success"] is True
assert result["content"] == original
assert result["ccr_hash"] == ccr_hash
def test_retrieve_by_query(self):
"""通过关键词搜索 CCR 缓存"""
compressor = HeadroomCompressor({})
compressor._store_ccr("Python is a programming language")
compressor._store_ccr("Rust is a systems programming language")
result = compressor.retrieve(query="Python")
assert result["success"] is True
assert len(result["results"]) >= 1
assert any("Python" in r["content"] for r in result["results"])
def test_retrieve_not_found(self):
"""无效 hash 返回错误"""
compressor = HeadroomCompressor({})
result = compressor.retrieve(ccr_hash="nonexistent_hash")
assert result["success"] is False
assert "error" in result
@pytest.mark.asyncio
async def test_ccr_hash_in_compressed_output(self):
"""compress_tool_result 后内容可通过 CCR hash 检索"""
compressor = HeadroomCompressor({
"min_length": 100,
"compressors": ["smart_crusher"],
})
json_content = _long_json_content()
mock_fn = MagicMock(return_value=_make_headroom_compress_mock("compressed"))
with patch("agentkit.core.headroom_compressor._HEADROOM_AVAILABLE", True), \
patch("agentkit.core.headroom_compressor.headroom_compress", mock_fn):
result = await compressor.compress_tool_result("tool", json_content)
# Extract hash from output
import re
match = re.search(r"CCR:hash=([a-f0-9]+)", result)
assert match is not None
ccr_hash = match.group(1)
# Retrieve original via hash
retrieved = compressor.retrieve(ccr_hash=ccr_hash)
assert retrieved["success"] is True
assert retrieved["content"] == json_content
# ---------------------------------------------------------------------------
# TestHeadroomCompressorConfig
# ---------------------------------------------------------------------------
class TestHeadroomCompressorConfig:
"""测试配置项"""
def test_default_config(self):
"""默认配置值"""
compressor = HeadroomCompressor({})
assert compressor._compressors == ["smart_crusher", "code_compressor"]
assert compressor._ccr_ttl == 300
assert compressor._min_length == 500
assert compressor._model == "default"
def test_custom_config(self):
"""自定义配置值"""
config = {
"compressors": ["smart_crusher"],
"ccr_ttl": 600,
"min_length": 1000,
"model": "gpt-4",
}
compressor = HeadroomCompressor(config)
assert compressor._compressors == ["smart_crusher"]
assert compressor._ccr_ttl == 600
assert compressor._min_length == 1000
assert compressor._model == "gpt-4"