Files
Hy-Memory/tests/coding/test_preproc.py
T
2026-10-16 16:26:27 +08:00

206 lines
7.8 KiB
Python

# -*- coding: utf-8 -*-
"""preproc 工具测试"""
import pytest
from hy_memory.coding.preproc import (
has_any_tool_message,
strip_tool_messages,
truncate_tool_result_text,
truncate_tool_message,
truncate_messages,
prepare_extractor_messages,
extract_files,
extract_tool_summary,
DEFAULT_MAX_TOOL_RESULT_BYTES,
)
from hy_memory.pipelines.base import ChatMessage, ToolCall
class TestHasAnyToolMessage:
def test_no_tools(self):
msgs = [ChatMessage(role="user", content="hi"),
ChatMessage(role="assistant", content="ok")]
assert not has_any_tool_message(msgs)
def test_role_tool(self):
msgs = [ChatMessage(role="user", content="hi"),
ChatMessage(role="tool", content="r", tool_call_id="x")]
assert has_any_tool_message(msgs)
def test_assistant_tool_calls(self):
msgs = [ChatMessage(role="assistant", content="",
tool_calls=[ToolCall(id="x", name="Read", arguments={})])]
assert has_any_tool_message(msgs)
def test_empty(self):
assert not has_any_tool_message([])
class TestStripToolMessages:
def test_drops_tool_role_messages(self):
msgs = [
ChatMessage(role="user", content="hi"),
ChatMessage(role="tool", content="r", tool_call_id="x"),
ChatMessage(role="assistant", content="bye"),
]
out = strip_tool_messages(msgs)
assert len(out) == 2
assert out[0].role == "user" and out[1].role == "assistant"
assert all(not m.is_tool_message() for m in out)
def test_strips_tool_calls_keep_text(self):
msgs = [
ChatMessage(role="assistant", content="text only"),
ChatMessage(role="assistant", content="some text",
tool_calls=[ToolCall(id="x", name="Read", arguments={})]),
ChatMessage(role="assistant", content="",
tool_calls=[ToolCall(id="x", name="Read", arguments={})]),
]
out = strip_tool_messages(msgs)
# 第 1 条保留,第 2 条保留(content 非空,但 tool_calls drop),第 3 条整条丢
assert len(out) == 2
assert all(not m.has_tool_calls() for m in out)
assert out[0].content == "text only"
assert out[1].content == "some text"
def test_unchanged_for_pure_chat(self):
msgs = [ChatMessage(role="user", content="a"),
ChatMessage(role="assistant", content="b")]
out = strip_tool_messages(msgs)
assert len(out) == 2
assert out[0].content == "a" and out[1].content == "b"
class TestTruncate:
def test_prepare_keeps_all_turns_and_truncates_tool_and_assistant(self):
msgs = []
for i in range(10):
msgs.extend([
ChatMessage(role="user", content=f"user-{i}"),
ChatMessage(role="assistant", content="A" * 8000),
ChatMessage(role="tool", content="T" * 5000, tool_name="Read"),
])
out = prepare_extractor_messages(msgs)
assert len(out) == len(msgs)
assert [m.content for m in out if m.role == "user"] == [
f"user-{i}" for i in range(10)
]
assert all("chars omitted" in m.content for m in out if m.role == "assistant")
assert all("bytes omitted" in m.content for m in out if m.role == "tool")
def test_text_short_unchanged(self):
s = "hello"
assert truncate_tool_result_text(s) == "hello"
def test_text_long_truncated(self):
s = "x" * 10000
out = truncate_tool_result_text(s, max_bytes=2048, head_bytes=1024, tail_bytes=512)
assert len(out) < 10000
assert "omitted" in out
assert out.startswith("x" * 100)
assert out.endswith("x" * 100)
def test_utf8_limit_is_real_bytes(self):
text = "中" * 1000 # 3000 UTF-8 bytes
out = truncate_tool_result_text(
text, max_bytes=1200, head_bytes=600, tail_bytes=300,
)
assert "bytes omitted" in out
assert len(out.encode("utf-8")) < len(text.encode("utf-8"))
def test_small_configured_limit_still_truncates(self):
text = "x" * 2000
out = truncate_tool_result_text(text, max_bytes=512)
assert "bytes omitted" in out
assert len(out.encode("utf-8")) <= 512
def test_tool_message_truncated(self):
big = ChatMessage(role="tool", content="x" * 5000)
out = truncate_tool_message(big)
assert len(out.content) < 5000
# 原对象不变
assert len(big.content) == 5000
def test_non_tool_message_unchanged(self):
m = ChatMessage(role="user", content="x" * 5000)
assert truncate_tool_message(m) is m
def test_truncate_messages_iterates(self):
msgs = [
ChatMessage(role="user", content="abc"),
ChatMessage(role="tool", content="x" * 5000),
]
out = truncate_messages(msgs)
assert out[0].content == "abc"
assert "omitted" in out[1].content
def test_default_threshold(self):
small = "x" * (DEFAULT_MAX_TOOL_RESULT_BYTES - 1)
m = ChatMessage(role="tool", content=small)
assert truncate_tool_message(m).content == small
class TestExtractFiles:
def test_extract_path_keys(self):
msgs = [
ChatMessage(role="assistant", content="", tool_calls=[
ToolCall(id="1", name="Read", arguments={"path": "src/a.py"}),
ToolCall(id="2", name="Edit", arguments={"file_path": "src/b.py"}),
ToolCall(id="3", name="Bash", arguments={"command": "ls"}), # 无 path 键
]),
]
files = extract_files(msgs)
assert files == ["src/a.py", "src/b.py"]
def test_dedup_and_sorted(self):
msgs = [
ChatMessage(role="assistant", content="", tool_calls=[
ToolCall(id="1", name="Read", arguments={"path": "z.py"}),
ToolCall(id="2", name="Read", arguments={"path": "a.py"}),
ToolCall(id="3", name="Read", arguments={"path": "z.py"}),
]),
]
assert extract_files(msgs) == ["a.py", "z.py"]
def test_empty(self):
assert extract_files([]) == []
assert extract_files([ChatMessage(role="user", content="hi")]) == []
class TestExtractToolSummary:
def test_per_turn_grouping(self):
msgs = [
ChatMessage(role="user", content="重构 auth"),
ChatMessage(role="assistant", content="", tool_calls=[
ToolCall(id="1", name="Read", arguments={"path": "a"}),
]),
ChatMessage(role="tool", content="ok", tool_call_id="1", tool_name="Read"),
ChatMessage(role="assistant", content="", tool_calls=[
ToolCall(id="2", name="Edit", arguments={"path": "a"}),
]),
ChatMessage(role="tool", content="ok", tool_call_id="2", tool_name="Edit"),
ChatMessage(role="user", content="问个闲话"),
ChatMessage(role="assistant", content="OK"),
]
summary = extract_tool_summary(msgs)
assert len(summary) == 2
assert summary[0]["user"] == "重构 auth"
assert sorted(summary[0]["tools"]) == ["Edit", "Read"]
assert summary[0]["has_tool_result"] is True
assert summary[1]["user"] == "问个闲话"
assert summary[1]["tools"] == []
assert summary[1]["has_tool_result"] is False
def test_no_user_anchor_first(self):
# tool 消息出现在 user 之前
msgs = [
ChatMessage(role="assistant", content="", tool_calls=[
ToolCall(id="1", name="Read", arguments={}),
]),
]
summary = extract_tool_summary(msgs)
assert len(summary) == 1
assert summary[0]["user"] == ""
assert summary[0]["tools"] == ["Read"]