feat: chat integration, tenant admin spec, backend chat service, miniprogram updates, DEMO moved to tmp, XCX-TEST removed, migrations & docs
This commit is contained in:
225
scripts/ops/test_chat_e2e.py
Normal file
225
scripts/ops/test_chat_e2e.py
Normal file
@@ -0,0 +1,225 @@
|
||||
"""
|
||||
RNS1.4 CHAT 模块端到端测试脚本
|
||||
用法: python scripts/ops/test_chat_e2e.py
|
||||
|
||||
前置条件:
|
||||
- 后端服务已启动 (uvicorn app.main:app)
|
||||
- .env 中配置了 TEST_USER_TOKEN 和 APP_DB_DSN
|
||||
- test_zqyy_app 数据库可访问
|
||||
|
||||
环境变量:
|
||||
BACKEND_URL — 后端地址,默认 http://localhost:8000
|
||||
TEST_USER_TOKEN — 测试用户 JWT token
|
||||
APP_DB_DSN — 业务数据库连接串(指向 test_zqyy_app)
|
||||
"""
|
||||
# CHANGE 2026-03-20 | RNS1.4 T13.1: CHAT 端到端测试脚本
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
# 加载根 .env
|
||||
from dotenv import load_dotenv
|
||||
load_dotenv(Path(__file__).resolve().parents[2] / ".env")
|
||||
|
||||
BACKEND_URL = os.environ.get("BACKEND_URL", "http://localhost:8000").rstrip("/")
|
||||
TOKEN = os.environ.get("TEST_USER_TOKEN", "")
|
||||
DB_DSN = os.environ.get("APP_DB_DSN", "")
|
||||
|
||||
if not TOKEN:
|
||||
print("❌ 缺少 TEST_USER_TOKEN 环境变量")
|
||||
sys.exit(1)
|
||||
if not DB_DSN:
|
||||
print("❌ 缺少 APP_DB_DSN 环境变量")
|
||||
sys.exit(1)
|
||||
|
||||
import httpx
|
||||
import psycopg2
|
||||
|
||||
HEADERS = {"Authorization": f"Bearer {TOKEN}", "Content-Type": "application/json"}
|
||||
results: list[dict] = []
|
||||
|
||||
|
||||
def record(name: str, passed: bool, detail: str = ""):
|
||||
status = "✅ PASS" if passed else "❌ FAIL"
|
||||
print(f" {status} — {name}" + (f" ({detail})" if detail else ""))
|
||||
results.append({"test": name, "passed": passed, "detail": detail})
|
||||
|
||||
|
||||
def main():
|
||||
print(f"\n🔗 后端: {BACKEND_URL}")
|
||||
print(f"🗄️ 数据库: {DB_DSN[:40]}...\n")
|
||||
|
||||
chat_id = None
|
||||
|
||||
# ── CHAT-1: 对话历史列表 ──
|
||||
print("── CHAT-1: GET /api/xcx/chat/history ──")
|
||||
try:
|
||||
r = httpx.get(f"{BACKEND_URL}/api/xcx/chat/history", headers=HEADERS, timeout=10)
|
||||
record("CHAT-1 状态码 200", r.status_code == 200, f"got {r.status_code}")
|
||||
if r.status_code == 200:
|
||||
data = r.json()
|
||||
record("CHAT-1 返回 items 数组", isinstance(data.get("items"), list))
|
||||
except Exception as e:
|
||||
record("CHAT-1 请求", False, str(e))
|
||||
|
||||
# ── 创建/获取对话(通过 general 入口)──
|
||||
print("\n── CHAT-2b: GET /api/xcx/chat/messages?contextType=general ──")
|
||||
try:
|
||||
r = httpx.get(
|
||||
f"{BACKEND_URL}/api/xcx/chat/messages",
|
||||
params={"contextType": "general", "contextId": ""},
|
||||
headers=HEADERS, timeout=10,
|
||||
)
|
||||
record("CHAT-2b 状态码 200", r.status_code == 200, f"got {r.status_code}")
|
||||
if r.status_code == 200:
|
||||
data = r.json()
|
||||
chat_id = data.get("chatId")
|
||||
record("CHAT-2b 返回 chatId", chat_id is not None, f"chatId={chat_id}")
|
||||
except Exception as e:
|
||||
record("CHAT-2b 请求", False, str(e))
|
||||
|
||||
if not chat_id:
|
||||
print("\n⚠️ 无法获取 chatId,跳过后续测试")
|
||||
_print_summary()
|
||||
return
|
||||
|
||||
# ── CHAT-2a: 通过 chatId 查询消息 ──
|
||||
print(f"\n── CHAT-2a: GET /api/xcx/chat/{chat_id}/messages ──")
|
||||
try:
|
||||
r = httpx.get(
|
||||
f"{BACKEND_URL}/api/xcx/chat/{chat_id}/messages",
|
||||
headers=HEADERS, timeout=10,
|
||||
)
|
||||
record("CHAT-2a 状态码 200", r.status_code == 200, f"got {r.status_code}")
|
||||
except Exception as e:
|
||||
record("CHAT-2a 请求", False, str(e))
|
||||
|
||||
# ── CHAT-3: 发送消息(同步) ──
|
||||
print(f"\n── CHAT-3: POST /api/xcx/chat/{chat_id}/messages ──")
|
||||
test_content = "你好,这是一条端到端测试消息,请简短回复。"
|
||||
ai_reply_id = None
|
||||
try:
|
||||
r = httpx.post(
|
||||
f"{BACKEND_URL}/api/xcx/chat/{chat_id}/messages",
|
||||
json={"content": test_content},
|
||||
headers=HEADERS, timeout=30,
|
||||
)
|
||||
record("CHAT-3 状态码 200", r.status_code == 200, f"got {r.status_code}")
|
||||
if r.status_code == 200:
|
||||
data = r.json()
|
||||
user_msg = data.get("userMessage", {})
|
||||
ai_msg = data.get("aiReply", {})
|
||||
record("CHAT-3 用户消息已返回", bool(user_msg.get("id")))
|
||||
record("CHAT-3 AI 回复已返回", bool(ai_msg.get("id")))
|
||||
record("CHAT-3 AI 回复非空", bool(ai_msg.get("content")))
|
||||
ai_reply_id = ai_msg.get("id")
|
||||
if ai_msg.get("content"):
|
||||
print(f" AI 回复: {ai_msg['content'][:80]}...")
|
||||
except Exception as e:
|
||||
record("CHAT-3 请求", False, str(e))
|
||||
|
||||
# ── CHAT-4: SSE 流式 ──
|
||||
print(f"\n── CHAT-4: POST /api/xcx/chat/stream (SSE) ──")
|
||||
sse_content = "请用一句话介绍台球运动。"
|
||||
sse_tokens: list[str] = []
|
||||
sse_done = False
|
||||
sse_message_id = None
|
||||
try:
|
||||
with httpx.stream(
|
||||
"POST",
|
||||
f"{BACKEND_URL}/api/xcx/chat/stream",
|
||||
json={"chatId": int(chat_id), "content": sse_content},
|
||||
headers=HEADERS,
|
||||
timeout=60,
|
||||
) as resp:
|
||||
record("CHAT-4 状态码 200", resp.status_code == 200, f"got {resp.status_code}")
|
||||
current_event = ""
|
||||
for line in resp.iter_lines():
|
||||
if line.startswith("event:"):
|
||||
current_event = line[6:].strip()
|
||||
elif line.startswith("data:"):
|
||||
raw = line[5:].strip()
|
||||
try:
|
||||
d = json.loads(raw)
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
if current_event == "message" and "token" in d:
|
||||
sse_tokens.append(d["token"])
|
||||
elif current_event == "done":
|
||||
sse_done = True
|
||||
sse_message_id = d.get("messageId")
|
||||
elif current_event == "error":
|
||||
record("CHAT-4 收到 error 事件", False, d.get("message", ""))
|
||||
|
||||
full_reply = "".join(sse_tokens)
|
||||
record("CHAT-4 收到 token 事件", len(sse_tokens) > 0, f"{len(sse_tokens)} tokens")
|
||||
record("CHAT-4 收到 done 事件", sse_done)
|
||||
record("CHAT-4 回复语义通顺", len(full_reply) > 5, f"len={len(full_reply)}")
|
||||
if full_reply:
|
||||
print(f" SSE 回复: {full_reply[:80]}...")
|
||||
except Exception as e:
|
||||
record("CHAT-4 请求", False, str(e))
|
||||
|
||||
# ── 数据库验证 ──
|
||||
print("\n── 数据库持久化验证 ──")
|
||||
try:
|
||||
conn = psycopg2.connect(DB_DSN)
|
||||
cur = conn.cursor()
|
||||
|
||||
# 验证 ai_messages 包含用户消息和 AI 回复
|
||||
cur.execute(
|
||||
"SELECT id, role, tokens_used FROM biz.ai_messages "
|
||||
"WHERE conversation_id = %s ORDER BY created_at DESC LIMIT 4",
|
||||
(int(chat_id),),
|
||||
)
|
||||
rows = cur.fetchall()
|
||||
roles = [r[1] for r in rows]
|
||||
record("DB: ai_messages 有 user 消息", "user" in roles)
|
||||
record("DB: ai_messages 有 assistant 回复", "assistant" in roles)
|
||||
|
||||
# 验证 tokens_used
|
||||
assistant_rows = [r for r in rows if r[1] == "assistant"]
|
||||
if assistant_rows:
|
||||
tokens = assistant_rows[0][2]
|
||||
record("DB: tokens_used 已记录", tokens is not None and tokens > 0,
|
||||
f"tokens_used={tokens}")
|
||||
else:
|
||||
record("DB: tokens_used 已记录", False, "无 assistant 行")
|
||||
|
||||
# 验证 ai_conversations 元数据更新
|
||||
cur.execute(
|
||||
"SELECT last_message, last_message_at FROM biz.ai_conversations WHERE id = %s",
|
||||
(int(chat_id),),
|
||||
)
|
||||
conv = cur.fetchone()
|
||||
if conv:
|
||||
record("DB: last_message 已更新", bool(conv[0]))
|
||||
record("DB: last_message_at 已更新", conv[1] is not None)
|
||||
else:
|
||||
record("DB: ai_conversations 记录存在", False)
|
||||
|
||||
cur.close()
|
||||
conn.close()
|
||||
except Exception as e:
|
||||
record("DB 验证", False, str(e))
|
||||
|
||||
_print_summary()
|
||||
|
||||
|
||||
def _print_summary():
|
||||
print("\n" + "=" * 50)
|
||||
passed = sum(1 for r in results if r["passed"])
|
||||
failed = sum(1 for r in results if not r["passed"])
|
||||
print(f"总计: {len(results)} 项 | ✅ {passed} 通过 | ❌ {failed} 失败")
|
||||
|
||||
# 输出 JSON 报告
|
||||
report_path = Path(__file__).parent / "chat_e2e_report.json"
|
||||
report_path.write_text(json.dumps(results, ensure_ascii=False, indent=2), encoding="utf-8")
|
||||
print(f"📄 报告已保存: {report_path}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user