mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 09:20:22 +08:00
379 lines
12 KiB
Python
379 lines
12 KiB
Python
|
|
"""
|
|||
|
|
ThinkingRectifier 单元测试
|
|||
|
|
|
|||
|
|
测试 Thinking 整流器的核心功能:
|
|||
|
|
- 移除 thinking 和 redacted_thinking 块
|
|||
|
|
- 移除非 thinking 块上的 signature 字段
|
|||
|
|
- 条件删除顶层 thinking 参数
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
import copy
|
|||
|
|
|
|||
|
|
import pytest
|
|||
|
|
|
|||
|
|
from src.services.message.thinking_rectifier import ThinkingRectifier
|
|||
|
|
|
|||
|
|
|
|||
|
|
class TestRectifyBasic:
|
|||
|
|
"""测试基本整流功能"""
|
|||
|
|
|
|||
|
|
def test_empty_request_body(self) -> None:
|
|||
|
|
"""空请求体应返回原值"""
|
|||
|
|
result, modified = ThinkingRectifier.rectify({})
|
|||
|
|
assert result == {}
|
|||
|
|
assert modified is False
|
|||
|
|
|
|||
|
|
def test_none_request_body(self) -> None:
|
|||
|
|
"""None 请求体应返回原值"""
|
|||
|
|
result, modified = ThinkingRectifier.rectify(None) # type: ignore
|
|||
|
|
assert result is None
|
|||
|
|
assert modified is False
|
|||
|
|
|
|||
|
|
def test_no_messages(self) -> None:
|
|||
|
|
"""无 messages 字段应返回原值"""
|
|||
|
|
body = {"model": "claude-3-opus"}
|
|||
|
|
result, modified = ThinkingRectifier.rectify(body)
|
|||
|
|
assert result == body
|
|||
|
|
assert modified is False
|
|||
|
|
|
|||
|
|
def test_empty_messages(self) -> None:
|
|||
|
|
"""空 messages 列表应返回原值"""
|
|||
|
|
body = {"model": "claude-3-opus", "messages": []}
|
|||
|
|
result, modified = ThinkingRectifier.rectify(body)
|
|||
|
|
assert result["messages"] == []
|
|||
|
|
assert modified is False
|
|||
|
|
|
|||
|
|
|
|||
|
|
class TestRemoveThinkingBlocks:
|
|||
|
|
"""测试移除 thinking 块"""
|
|||
|
|
|
|||
|
|
def test_remove_thinking_block(self) -> None:
|
|||
|
|
"""应移除 thinking 块"""
|
|||
|
|
body = {
|
|||
|
|
"messages": [
|
|||
|
|
{
|
|||
|
|
"role": "assistant",
|
|||
|
|
"content": [
|
|||
|
|
{"type": "thinking", "thinking": "...", "signature": "abc"},
|
|||
|
|
{"type": "text", "text": "Hello"},
|
|||
|
|
],
|
|||
|
|
}
|
|||
|
|
]
|
|||
|
|
}
|
|||
|
|
result, modified = ThinkingRectifier.rectify(body)
|
|||
|
|
|
|||
|
|
assert modified is True
|
|||
|
|
content = result["messages"][0]["content"]
|
|||
|
|
assert len(content) == 1
|
|||
|
|
assert content[0]["type"] == "text"
|
|||
|
|
|
|||
|
|
def test_remove_redacted_thinking_block(self) -> None:
|
|||
|
|
"""应移除 redacted_thinking 块"""
|
|||
|
|
body = {
|
|||
|
|
"messages": [
|
|||
|
|
{
|
|||
|
|
"role": "assistant",
|
|||
|
|
"content": [
|
|||
|
|
{"type": "redacted_thinking", "data": "..."},
|
|||
|
|
{"type": "text", "text": "Hello"},
|
|||
|
|
],
|
|||
|
|
}
|
|||
|
|
]
|
|||
|
|
}
|
|||
|
|
result, modified = ThinkingRectifier.rectify(body)
|
|||
|
|
|
|||
|
|
assert modified is True
|
|||
|
|
content = result["messages"][0]["content"]
|
|||
|
|
assert len(content) == 1
|
|||
|
|
assert content[0]["type"] == "text"
|
|||
|
|
|
|||
|
|
def test_remove_multiple_thinking_blocks(self) -> None:
|
|||
|
|
"""应移除多个 thinking 块"""
|
|||
|
|
body = {
|
|||
|
|
"messages": [
|
|||
|
|
{
|
|||
|
|
"role": "assistant",
|
|||
|
|
"content": [
|
|||
|
|
{"type": "thinking", "thinking": "thought 1"},
|
|||
|
|
{"type": "text", "text": "response 1"},
|
|||
|
|
],
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"role": "assistant",
|
|||
|
|
"content": [
|
|||
|
|
{"type": "redacted_thinking", "data": "..."},
|
|||
|
|
{"type": "text", "text": "response 2"},
|
|||
|
|
],
|
|||
|
|
},
|
|||
|
|
]
|
|||
|
|
}
|
|||
|
|
result, modified = ThinkingRectifier.rectify(body)
|
|||
|
|
|
|||
|
|
assert modified is True
|
|||
|
|
assert len(result["messages"][0]["content"]) == 1
|
|||
|
|
assert len(result["messages"][1]["content"]) == 1
|
|||
|
|
|
|||
|
|
|
|||
|
|
class TestRemoveSignatureField:
|
|||
|
|
"""测试移除 signature 字段"""
|
|||
|
|
|
|||
|
|
def test_remove_signature_from_text_block(self) -> None:
|
|||
|
|
"""应从 text 块移除 signature 字段"""
|
|||
|
|
body = {
|
|||
|
|
"messages": [
|
|||
|
|
{
|
|||
|
|
"role": "assistant",
|
|||
|
|
"content": [
|
|||
|
|
{"type": "text", "text": "Hello", "signature": "should_remove"},
|
|||
|
|
],
|
|||
|
|
}
|
|||
|
|
]
|
|||
|
|
}
|
|||
|
|
result, modified = ThinkingRectifier.rectify(body)
|
|||
|
|
|
|||
|
|
assert modified is True
|
|||
|
|
block = result["messages"][0]["content"][0]
|
|||
|
|
assert "signature" not in block
|
|||
|
|
assert block["text"] == "Hello"
|
|||
|
|
|
|||
|
|
def test_remove_signature_from_tool_use_block(self) -> None:
|
|||
|
|
"""应从 tool_use 块移除 signature 字段"""
|
|||
|
|
body = {
|
|||
|
|
"messages": [
|
|||
|
|
{
|
|||
|
|
"role": "assistant",
|
|||
|
|
"content": [
|
|||
|
|
{
|
|||
|
|
"type": "tool_use",
|
|||
|
|
"id": "tool_1",
|
|||
|
|
"name": "search",
|
|||
|
|
"input": {},
|
|||
|
|
"signature": "should_remove",
|
|||
|
|
},
|
|||
|
|
],
|
|||
|
|
}
|
|||
|
|
]
|
|||
|
|
}
|
|||
|
|
result, modified = ThinkingRectifier.rectify(body)
|
|||
|
|
|
|||
|
|
assert modified is True
|
|||
|
|
block = result["messages"][0]["content"][0]
|
|||
|
|
assert "signature" not in block
|
|||
|
|
assert block["id"] == "tool_1"
|
|||
|
|
|
|||
|
|
|
|||
|
|
class TestTopLevelThinkingParam:
|
|||
|
|
"""测试顶层 thinking 参数处理"""
|
|||
|
|
|
|||
|
|
def test_remove_thinking_param_when_tool_use_without_thinking_prefix(self) -> None:
|
|||
|
|
"""整流后有 tool_use 但无 thinking 前缀时应移除 thinking 参数"""
|
|||
|
|
body = {
|
|||
|
|
"thinking": {"type": "enabled", "budget_tokens": 10000},
|
|||
|
|
"messages": [
|
|||
|
|
{
|
|||
|
|
"role": "assistant",
|
|||
|
|
"content": [
|
|||
|
|
{"type": "thinking", "thinking": "..."},
|
|||
|
|
{"type": "tool_use", "id": "t1", "name": "search", "input": {}},
|
|||
|
|
],
|
|||
|
|
}
|
|||
|
|
],
|
|||
|
|
}
|
|||
|
|
result, modified = ThinkingRectifier.rectify(body)
|
|||
|
|
|
|||
|
|
assert modified is True
|
|||
|
|
assert "thinking" not in result
|
|||
|
|
# tool_use 应保留
|
|||
|
|
assert result["messages"][0]["content"][0]["type"] == "tool_use"
|
|||
|
|
|
|||
|
|
def test_keep_thinking_param_when_no_tool_use(self) -> None:
|
|||
|
|
"""整流后无 tool_use 时应保留 thinking 参数"""
|
|||
|
|
body = {
|
|||
|
|
"thinking": {"type": "enabled", "budget_tokens": 10000},
|
|||
|
|
"messages": [
|
|||
|
|
{
|
|||
|
|
"role": "assistant",
|
|||
|
|
"content": [
|
|||
|
|
{"type": "thinking", "thinking": "..."},
|
|||
|
|
{"type": "text", "text": "Hello"},
|
|||
|
|
],
|
|||
|
|
}
|
|||
|
|
],
|
|||
|
|
}
|
|||
|
|
result, modified = ThinkingRectifier.rectify(body)
|
|||
|
|
|
|||
|
|
assert modified is True
|
|||
|
|
# thinking 参数应保留(只移除了 thinking 块)
|
|||
|
|
assert "thinking" in result
|
|||
|
|
assert result["thinking"]["type"] == "enabled"
|
|||
|
|
|
|||
|
|
def test_keep_thinking_param_when_disabled(self) -> None:
|
|||
|
|
"""thinking 参数未启用时应保留"""
|
|||
|
|
body = {
|
|||
|
|
"thinking": {"type": "disabled"},
|
|||
|
|
"messages": [
|
|||
|
|
{
|
|||
|
|
"role": "assistant",
|
|||
|
|
"content": [
|
|||
|
|
{"type": "tool_use", "id": "t1", "name": "search", "input": {}},
|
|||
|
|
],
|
|||
|
|
}
|
|||
|
|
],
|
|||
|
|
}
|
|||
|
|
result, modified = ThinkingRectifier.rectify(body)
|
|||
|
|
|
|||
|
|
# 无 thinking 块需要移除,thinking 参数也不需要移除
|
|||
|
|
assert modified is False
|
|||
|
|
assert result["thinking"]["type"] == "disabled"
|
|||
|
|
|
|||
|
|
|
|||
|
|
class TestEdgeCases:
|
|||
|
|
"""测试边界情况"""
|
|||
|
|
|
|||
|
|
def test_non_dict_message_preserved(self) -> None:
|
|||
|
|
"""非 dict 消息应保留"""
|
|||
|
|
body = {
|
|||
|
|
"messages": [
|
|||
|
|
"string message", # 非 dict
|
|||
|
|
{"role": "user", "content": "Hello"},
|
|||
|
|
]
|
|||
|
|
}
|
|||
|
|
result, modified = ThinkingRectifier.rectify(body)
|
|||
|
|
|
|||
|
|
assert result["messages"][0] == "string message"
|
|||
|
|
assert modified is False
|
|||
|
|
|
|||
|
|
def test_string_content_preserved(self) -> None:
|
|||
|
|
"""字符串 content 应保留"""
|
|||
|
|
body = {
|
|||
|
|
"messages": [
|
|||
|
|
{"role": "user", "content": "Hello"},
|
|||
|
|
{"role": "assistant", "content": "Hi there"},
|
|||
|
|
]
|
|||
|
|
}
|
|||
|
|
result, modified = ThinkingRectifier.rectify(body)
|
|||
|
|
|
|||
|
|
assert result["messages"][1]["content"] == "Hi there"
|
|||
|
|
assert modified is False
|
|||
|
|
|
|||
|
|
def test_non_dict_block_in_content_preserved(self) -> None:
|
|||
|
|
"""content 中的非 dict 块应保留"""
|
|||
|
|
body = {
|
|||
|
|
"messages": [
|
|||
|
|
{
|
|||
|
|
"role": "assistant",
|
|||
|
|
"content": [
|
|||
|
|
"string block", # 非 dict
|
|||
|
|
{"type": "text", "text": "Hello"},
|
|||
|
|
],
|
|||
|
|
}
|
|||
|
|
]
|
|||
|
|
}
|
|||
|
|
result, modified = ThinkingRectifier.rectify(body)
|
|||
|
|
|
|||
|
|
assert result["messages"][0]["content"][0] == "string block"
|
|||
|
|
assert modified is False
|
|||
|
|
|
|||
|
|
def test_empty_content_after_rectify_logs_warning(self) -> None:
|
|||
|
|
"""整流后 assistant 消息 content 为空时应记录警告(不抛异常)"""
|
|||
|
|
body = {
|
|||
|
|
"messages": [
|
|||
|
|
{
|
|||
|
|
"role": "assistant",
|
|||
|
|
"content": [
|
|||
|
|
{"type": "thinking", "thinking": "only thinking"},
|
|||
|
|
],
|
|||
|
|
}
|
|||
|
|
]
|
|||
|
|
}
|
|||
|
|
# 应该不抛异常
|
|||
|
|
result, modified = ThinkingRectifier.rectify(body)
|
|||
|
|
|
|||
|
|
assert modified is True
|
|||
|
|
# content 应为空列表
|
|||
|
|
assert result["messages"][0]["content"] == []
|
|||
|
|
|
|||
|
|
def test_deep_copy_does_not_modify_original(self) -> None:
|
|||
|
|
"""深拷贝应保护原始数据不被修改"""
|
|||
|
|
original = {
|
|||
|
|
"messages": [
|
|||
|
|
{
|
|||
|
|
"role": "assistant",
|
|||
|
|
"content": [
|
|||
|
|
{"type": "thinking", "thinking": "..."},
|
|||
|
|
{"type": "text", "text": "Hello"},
|
|||
|
|
],
|
|||
|
|
}
|
|||
|
|
]
|
|||
|
|
}
|
|||
|
|
original_copy = copy.deepcopy(original)
|
|||
|
|
|
|||
|
|
ThinkingRectifier.rectify(original)
|
|||
|
|
|
|||
|
|
# 原始数据不应被修改
|
|||
|
|
assert original == original_copy
|
|||
|
|
|
|||
|
|
|
|||
|
|
class TestLastAssistantCheck:
|
|||
|
|
"""测试只检查最后一条 assistant 消息的逻辑"""
|
|||
|
|
|
|||
|
|
def test_only_last_assistant_matters_for_thinking_removal(self) -> None:
|
|||
|
|
"""只有最后一条 assistant 消息决定是否移除 thinking 参数"""
|
|||
|
|
body = {
|
|||
|
|
"thinking": {"type": "enabled", "budget_tokens": 10000},
|
|||
|
|
"messages": [
|
|||
|
|
# 第一条 assistant 有 tool_use
|
|||
|
|
{
|
|||
|
|
"role": "assistant",
|
|||
|
|
"content": [
|
|||
|
|
{"type": "thinking", "thinking": "..."},
|
|||
|
|
{"type": "tool_use", "id": "t1", "name": "search", "input": {}},
|
|||
|
|
],
|
|||
|
|
},
|
|||
|
|
{"role": "user", "content": "result"},
|
|||
|
|
# 最后一条 assistant 无 tool_use
|
|||
|
|
{
|
|||
|
|
"role": "assistant",
|
|||
|
|
"content": [
|
|||
|
|
{"type": "thinking", "thinking": "..."},
|
|||
|
|
{"type": "text", "text": "Final answer"},
|
|||
|
|
],
|
|||
|
|
},
|
|||
|
|
],
|
|||
|
|
}
|
|||
|
|
result, modified = ThinkingRectifier.rectify(body)
|
|||
|
|
|
|||
|
|
assert modified is True
|
|||
|
|
# thinking 参数应保留(最后一条 assistant 无 tool_use)
|
|||
|
|
assert "thinking" in result
|
|||
|
|
|
|||
|
|
def test_last_assistant_with_tool_use_removes_thinking(self) -> None:
|
|||
|
|
"""最后一条 assistant 有 tool_use 时应移除 thinking 参数"""
|
|||
|
|
body = {
|
|||
|
|
"thinking": {"type": "enabled", "budget_tokens": 10000},
|
|||
|
|
"messages": [
|
|||
|
|
# 第一条 assistant 无 tool_use
|
|||
|
|
{
|
|||
|
|
"role": "assistant",
|
|||
|
|
"content": [
|
|||
|
|
{"type": "thinking", "thinking": "..."},
|
|||
|
|
{"type": "text", "text": "thinking..."},
|
|||
|
|
],
|
|||
|
|
},
|
|||
|
|
{"role": "user", "content": "continue"},
|
|||
|
|
# 最后一条 assistant 有 tool_use
|
|||
|
|
{
|
|||
|
|
"role": "assistant",
|
|||
|
|
"content": [
|
|||
|
|
{"type": "thinking", "thinking": "..."},
|
|||
|
|
{"type": "tool_use", "id": "t1", "name": "search", "input": {}},
|
|||
|
|
],
|
|||
|
|
},
|
|||
|
|
],
|
|||
|
|
}
|
|||
|
|
result, modified = ThinkingRectifier.rectify(body)
|
|||
|
|
|
|||
|
|
assert modified is True
|
|||
|
|
# thinking 参数应被移除
|
|||
|
|
assert "thinking" not in result
|