Files
xiaonuan/tests/test_tool_loop.py
T
snowy-yang 6981c6df4d 完善测试:pytest 套件(路由器/工具循环/WS全链路/投递/PG集成)
- tests/conftest.py: 内存版 FakeDB 复刻 db.py 全部签名,绕过 lifespan 的测试应用
- test_chat_flow.py: WebSocket 全链路(回声/路由信号落库/档案/慢信/连续天数)
- test_delivery.py: 事件/每日任务/关怀投递(在线与离线)、天气工具城市链、system prompt 注入
- test_router.py: 小模型路由清洗(去重/丢过去事件/非法作息/500优雅降级/无模型空判定)
- test_tool_loop.py: 原生工具调用循环(喂结果/超轮次强制收尾/坏参数)
- test_pure.py: 纯逻辑(连续天数/哈希槽位/时间间隔/模板/向量)
- test_db_pg.py: PG 集成测试,无 PG_DSN 自动跳过,配好即真实跑库
- 旧散装脚本移除;tools_live/ws_client/tui_smoke 保留为手动脚本
2026-09-23 20:20:37 +08:00

103 lines
3.9 KiB
Python

"""原生工具调用循环的离线测试(mock 模型服务)"""
import asyncio
import json
import httpx
from app import connector
def test_tool_loop_executes_and_feeds_back(monkeypatch):
calls = {"model": 0}
captured = {}
def handler(request: httpx.Request) -> httpx.Response:
body = json.loads(request.content)
calls["model"] += 1
if calls["model"] == 1:
assert body.get("tools"), "第一轮应带上工具声明"
return httpx.Response(200, json={"choices": [{"message": {
"role": "assistant", "content": "",
"tool_calls": [{"id": "call-1", "type": "function",
"function": {
"name": "get_weather",
"arguments": json.dumps({"city": "杭州"})}}]}}]})
tool_msg = body["messages"][-1]
captured["tool_msg"] = tool_msg
assert tool_msg["role"] == "tool"
assert tool_msg["tool_call_id"] == "call-1"
return httpx.Response(200, json={"choices": [{"message": {
"role": "assistant",
"content": "杭州今天有小雨,出门记得带伞哦~"}}]})
monkeypatch.setattr(connector, "_TEST_TRANSPORT",
httpx.MockTransport(handler))
async def exec_(name, args):
captured["exec"] = (name, args)
return "杭州 小雨 15~22°C 降水概率70%"
out = asyncio.run(connector.run_model_tools(
[{"role": "user", "content": "今天出门要带伞吗"}], exec_))
assert out == "杭州今天有小雨,出门记得带伞哦~"
assert captured["exec"] == ("get_weather", {"city": "杭州"})
assert calls["model"] == 2
def test_tool_loop_gives_up_after_max_rounds(monkeypatch):
calls = {"model": 0}
def handler(request: httpx.Request) -> httpx.Response:
body = json.loads(request.content)
calls["model"] += 1
if "tool_calls" not in json.dumps(body["messages"][-1]) and \
calls["model"] <= 3:
return httpx.Response(200, json={"choices": [{"message": {
"role": "assistant", "content": "",
"tool_calls": [{"id": f"c{calls['model']}", "type": "function",
"function": {"name": "web_search",
"arguments": "{}"}}]}}]})
# 第 4 次(超过 max_rounds 的强制收尾)不带 tools
assert "tools" not in body
return httpx.Response(200, json={"choices": [{"message": {
"role": "assistant", "content": "我直接说吧"}}]})
monkeypatch.setattr(connector, "_TEST_TRANSPORT",
httpx.MockTransport(handler))
async def exec_(name, args):
return "结果"
out = asyncio.run(connector.run_model_tools(
[{"role": "user", "content": "搜一下"}], exec_, max_rounds=3))
assert out == "我直接说吧"
assert calls["model"] == 4
def test_tool_loop_bad_arguments_become_empty_dict(monkeypatch):
calls = {"model": 0}
def handler(request: httpx.Request) -> httpx.Response:
calls["model"] += 1
if calls["model"] == 1:
return httpx.Response(200, json={"choices": [{"message": {
"role": "assistant", "content": "",
"tool_calls": [{"id": "c1", "type": "function",
"function": {"name": "get_weather",
"arguments": "{坏了"}}]}}]})
return httpx.Response(200, json={"choices": [{"message": {
"role": "assistant", "content": "好"}}]})
monkeypatch.setattr(connector, "_TEST_TRANSPORT",
httpx.MockTransport(handler))
seen = {}
async def exec_(name, args):
seen["args"] = args
return "ok"
out = asyncio.run(connector.run_model_tools(
[{"role": "user", "content": "天气"}], exec_))
assert out == "好" and seen["args"] == {}