mirror of
https://github.com/snowy-yang/xiaonuan.git
synced 2026-10-21 08:04:48 +00:00
完善测试: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 保留为手动脚本
This commit is contained in:
1 parent
fe96acbda2
commit
6981c6df4d
13 files changed
+1107
-192
No files matched your search
@@ -0,0 +1,158 @@
|
||||
"""投递与工具执行测试(FakeDB + stub 工具)"""
|
||||
import json
|
||||
import time
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from app import main
|
||||
|
||||
|
||||
class _State:
|
||||
name = "CONNECTED"
|
||||
|
||||
|
||||
class FakeWS:
|
||||
client_state = _State()
|
||||
|
||||
def __init__(self):
|
||||
self.sent = []
|
||||
|
||||
async def send_text(self, payload):
|
||||
self.sent.append(payload)
|
||||
|
||||
def last_text(self):
|
||||
"""最后一条主动消息的纯文本(服务端 JSON 转义了中文,要解一下)"""
|
||||
return json.loads(self.sent[-1])["text"]
|
||||
|
||||
|
||||
async def test_one_shot_event_offline_stays_pending(fdb, offline):
|
||||
ev = {"id": 1, "session": "s1", "kind": "alarm",
|
||||
"fire_at": time.time(), "text": "起床", "repeat": "once"}
|
||||
ok = await main.deliver_event(ev)
|
||||
assert ok is False
|
||||
assert fdb.events == [] # 没投递就不写历史、不标完成
|
||||
|
||||
|
||||
async def test_one_shot_event_online_delivers(fdb, offline):
|
||||
ws = FakeWS()
|
||||
main.connected.setdefault("s1", set()).add(ws)
|
||||
try:
|
||||
ev = {"id": 2, "session": "s1", "kind": "reminder",
|
||||
"fire_at": time.time(), "text": "喝水", "repeat": "once"}
|
||||
ok = await main.deliver_event(ev)
|
||||
assert ok is True
|
||||
assert "喝水" in ws.last_text()
|
||||
# 主动消息写进了历史
|
||||
assert [h["role"] for h in fdb.history] == ["assistant"]
|
||||
finally:
|
||||
main.connected.get("s1", set()).discard(ws)
|
||||
|
||||
|
||||
async def test_daily_event_marks_fired_not_done(fdb, offline):
|
||||
ws = FakeWS()
|
||||
main.connected.setdefault("s1", set()).add(ws)
|
||||
try:
|
||||
await fdb.add_event("s1", "reminder", time.time() + 3600, "吃药",
|
||||
"daily", 22 * 60)
|
||||
ev = fdb.events[0]
|
||||
ok = await main.deliver_daily_event(ev)
|
||||
assert ok is True
|
||||
assert "吃药" in ws.last_text()
|
||||
assert ev["last_fired"] == datetime.now().strftime("%Y-%m-%d")
|
||||
assert ev["status"] == "pending" # 每天还要再响
|
||||
finally:
|
||||
main.connected.get("s1", set()).discard(ws)
|
||||
|
||||
|
||||
async def test_care_delivery(fdb, offline):
|
||||
cid = await fdb.add_care("s1", "emotion", "昨天心情不好",
|
||||
time.time() - 10)
|
||||
ok = await main.deliver_care({"id": cid, "session": "s1",
|
||||
"kind": "emotion", "note": "昨天心情不好"})
|
||||
assert ok is False # 没人在线,保持 pending
|
||||
|
||||
ws = FakeWS()
|
||||
main.connected.setdefault("s1", set()).add(ws)
|
||||
try:
|
||||
ok = await main.deliver_care({"id": cid, "session": "s1",
|
||||
"kind": "emotion", "note": "昨天心情不好"})
|
||||
assert ok is True
|
||||
assert "心情" in ws.last_text()
|
||||
assert fdb.care[0]["status"] == "done"
|
||||
finally:
|
||||
main.connected.get("s1", set()).discard(ws)
|
||||
|
||||
|
||||
async def test_routine_echo_greeting(fdb, offline):
|
||||
r = {"session": "s1", "label": "到校", "minute": 7 * 60 + 40,
|
||||
"kind": "inferred", "evidence": 3}
|
||||
text = await main.routine_message(r)
|
||||
assert "到学校" in text
|
||||
|
||||
|
||||
async def test_weather_tool_city_chain(fdb, offline, monkeypatch):
|
||||
calls = []
|
||||
|
||||
async def fake_weather(city):
|
||||
calls.append(city)
|
||||
return f"{city} 晴 25°C"
|
||||
|
||||
monkeypatch.setattr(main.tools, "get_weather", fake_weather)
|
||||
|
||||
# 三层都推不出 -> UNKNOWN_CITY,且不假装有天气
|
||||
out = await main.execute_tool("s1", "get_weather", {})
|
||||
assert out.startswith("UNKNOWN_CITY") and calls == []
|
||||
|
||||
# 路由器的城市提示 -> 查询并记住
|
||||
out = await main.execute_tool("s1", "get_weather", {}, city_hint="杭州")
|
||||
assert "杭州" in out and calls == ["杭州"]
|
||||
assert await fdb.get_meta("s1", "city") == "杭州"
|
||||
|
||||
# 模型显式给了城市 -> 优先用,并覆盖记住的城市
|
||||
out = await main.execute_tool("s1", "get_weather", {"city": "北京"})
|
||||
assert calls[-1] == "北京"
|
||||
assert await fdb.get_meta("s1", "city") == "北京"
|
||||
|
||||
# 之后不带城市 -> 用最近一次记住的
|
||||
out = await main.execute_tool("s1", "get_weather", {})
|
||||
assert calls[-1] == "北京"
|
||||
|
||||
|
||||
async def test_tool_executor_other_tools(fdb, offline, monkeypatch):
|
||||
async def fake_news(q):
|
||||
return f"新闻:{q}"
|
||||
|
||||
async def fake_search(q):
|
||||
return f"搜索:{q}"
|
||||
|
||||
monkeypatch.setattr(main.tools, "get_news", fake_news)
|
||||
monkeypatch.setattr(main.tools, "web_search", fake_search)
|
||||
assert await main.execute_tool("s1", "get_news", {}) == "新闻:今日热点"
|
||||
assert await main.execute_tool("s1", "web_search",
|
||||
{"query": "量子"}) == "搜索:量子"
|
||||
assert await main.execute_tool("s1", "web_search", {}) == "缺少搜索词。"
|
||||
assert "未知工具" in await main.execute_tool("s1", "nope", {})
|
||||
|
||||
|
||||
async def test_system_prompt_includes_lore_and_relevant(fdb, offline):
|
||||
await fdb.upsert_lore("s1", "诞生", "小暖是用户亲手造的")
|
||||
prompt = await main.system_prompt(
|
||||
"s1", [{"text": "用户喜欢打篮球"}])
|
||||
assert "既定事实" in prompt
|
||||
assert "小暖是用户亲手造的" in prompt
|
||||
assert "用户喜欢打篮球" in prompt
|
||||
|
||||
|
||||
async def test_compose_views(fdb, offline):
|
||||
# 记忆
|
||||
await fdb.add_memories("s1", ["用户叫小明"], None)
|
||||
text = await main.compose_view("s1", "memories")
|
||||
assert "用户叫小明" in text
|
||||
# 空记忆的说法
|
||||
text = await main.compose_view("s2", "memories")
|
||||
assert "还不太认识你" in text
|
||||
# 身世
|
||||
text = await main.compose_view("s2", "lore")
|
||||
assert "一个字都不会忘" in text
|
||||
# 时间表空表
|
||||
text = await main.compose_view("s2", "timetable")
|
||||
assert "时间表还空着" in text
|
||||
Reference in new issue
Block a user