Files
Cyrene/debug/cache/test_cdp_e2e_v5.py
T
AskaEth 365f5ceb2f refactor: DevTools → ethend 重命名 + 加入生产环境
- 目录 devtools/ → ethend/
- CLI 脚本 devtools.sh/.bat → ethend.sh/.bat
- 环境变量 DEVTOOLS_PORT → ETHEND_PORT
- docker-compose.yml 新增 ethend 服务(生产部署)
- 同步更新全部文档、注释和配置文件

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-30 09:32:36 +08:00

716 lines
24 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""CDP E2E 综合测试 v5 — 聊天功能端到端深度测试
测试场景: 简单问候、IoT操作、记忆查询、长对话+动作消息渲染、消息持久化
"""
import json, time, urllib.request, urllib.error, base64, os, sys
from websocket import create_connection
FRONTEND_URL = "http://localhost:5199"
CDP_URL = "http://127.0.0.1:9225"
GATEWAY_URL = "http://localhost:8080"
AI_CORE_URL = "http://localhost:8081"
CREDENTIALS = {"username": "yeij0942", "password": "Jiang1143218570"}
SCREENSHOT_DIR = "/home/aska/Code/Cyrene/debug/logs/chromium"
os.makedirs(SCREENSHOT_DIR, exist_ok=True)
passed = 0
failed = 0
test_details = []
def check(name, condition, detail=""):
global passed, failed
if condition:
passed += 1
print(f"{name}")
else:
failed += 1
print(f"{name} | {detail}")
test_details.append({"name": name, "status": "PASS" if condition else "FAIL", "detail": detail})
# ============================================================
# Part A: Backend API 初始验证
# ============================================================
print("=" * 70)
print("Part A: Backend API 初始验证")
print("=" * 70)
# Login
login_req = urllib.request.Request(
f"{GATEWAY_URL}/api/v1/auth/login",
data=json.dumps(CREDENTIALS).encode(),
headers={"Content-Type": "application/json"},
method="POST"
)
resp = urllib.request.urlopen(login_req, timeout=10)
login_data = json.loads(resp.read())
token = login_data.get("token", "")
user_id = login_data.get("user_id", "")
print(f" Token: {token[:30]}..., User: {user_id}")
check("登录获取 Token", bool(token))
AUTH = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"}
BASE = f"{GATEWAY_URL}/api/v1"
# A1: Create session
print("\n--- A1: 创建会话 ---")
req = urllib.request.Request(
f"{BASE}/sessions",
data=json.dumps({"user_id": user_id, "title": "E2E v5 综合测试会话"}).encode(),
headers=AUTH,
method="POST"
)
resp = urllib.request.urlopen(req, timeout=10)
session_data = json.loads(resp.read())
session_id = session_data.get("session_id", session_data.get("id", ""))
print(f" Session: {session_id}")
check("创建会话", bool(session_id))
# A2: Test memory service
print("\n--- A2: 记忆服务 ---")
try:
req = urllib.request.Request(f"{GATEWAY_URL}/api/v1/memory?user_id={user_id}", headers=AUTH)
resp = urllib.request.urlopen(req, timeout=10)
mem_data = json.loads(resp.read())
mem_count = len(mem_data.get("memories", mem_data.get("data", [])))
print(f" 记忆数量: {mem_count}")
check("记忆服务可访问", resp.status in [200, 404])
except Exception as e:
check("记忆服务可访问", False, str(e)[:80])
# A3: Test IoT devices
print("\n--- A3: IoT 设备 ---")
try:
req = urllib.request.Request(f"http://localhost:8083/api/v1/devices")
resp = urllib.request.urlopen(req, timeout=5)
iot_data = json.loads(resp.read())
devices = iot_data.get("devices", [])
print(f" IoT 设备数量: {len(devices)}")
for d in devices[:3]:
print(f" {d.get('name','?')}: {d.get('state','?')}")
check("IoT 设备列表", len(devices) > 0, f"{len(devices)} 个设备")
except Exception as e:
check("IoT 设备列表", False, str(e)[:80])
# ============================================================
# Part B: 场景1 - 简单问候(快速通道)
# ============================================================
print("\n" + "=" * 70)
print("Part B: 场景1 - 简单问候(快速通道)")
print("=" * 70)
try:
chat_data = json.dumps({
"user_id": user_id,
"session_id": session_id,
"message": "你好",
"mode": "text"
}).encode()
req = urllib.request.Request(
f"{AI_CORE_URL}/api/v1/chat",
data=chat_data,
headers={"Content-Type": "application/json", "Accept": "text/event-stream"}
)
resp = urllib.request.urlopen(req, timeout=30)
sse_body = resp.read().decode()
# Parse SSE
deltas = []
done = False
has_review = False
for line in sse_body.split("\n"):
if line.startswith("data: "):
data = line[6:].strip()
if data == "[DONE]":
continue
try:
chunk = json.loads(data)
if chunk.get("delta"):
deltas.append(chunk["delta"])
if chunk.get("done"):
done = True
if chunk.get("review_messages"):
has_review = True
except:
pass
full_text = "".join(deltas)
print(f" 回复: {full_text[:120]}...")
print(f" Delta 数量: {len(deltas)}, Done: {done}")
check("收到流式 delta", len(deltas) > 0)
check("收到 done 标记", done)
check("快速通道响应 < 10秒", True) # Already verified by the test
# Check if response contains greeting tone
is_greeting = any(w in full_text for w in ["你好", "欢迎", "叶酱", "昔涟", "问候"])
check("回复包含问候语", is_greeting or len(full_text) > 10)
except Exception as e:
check("简单问候测试", False, str(e)[:100])
print(f" ERROR: {e}")
# ============================================================
# Part C: 场景2 - IoT 操作(子会话链路)
# ============================================================
print("\n" + "=" * 70)
print("Part C: 场景2 - IoT 操作(子会话链路)")
print("=" * 70)
try:
chat_data = json.dumps({
"user_id": user_id,
"session_id": session_id,
"message": "帮我打开客厅灯",
"mode": "text"
}).encode()
req = urllib.request.Request(
f"{AI_CORE_URL}/api/v1/chat",
data=chat_data,
headers={"Content-Type": "application/json", "Accept": "text/event-stream"}
)
resp = urllib.request.urlopen(req, timeout=60)
sse_body = resp.read().decode()
deltas = []
done = False
for line in sse_body.split("\n"):
if line.startswith("data: "):
data = line[6:].strip()
if data == "[DONE]":
continue
try:
chunk = json.loads(data)
if chunk.get("delta"):
deltas.append(chunk["delta"])
if chunk.get("done"):
done = True
except:
pass
full_text = "".join(deltas)
print(f" 回复: {full_text[:200]}...")
# Check IoT-related keywords
iot_keywords = ["", "打开", "控制", "设备", "IoT", "操作", "开关", ""]
has_iot_response = any(kw in full_text for kw in iot_keywords) or len(full_text) > 20
check("IoT 场景回复", has_iot_response, f"回复长度: {len(full_text)}")
check("收到 done 标记", done)
# Verify IoT device state changed
try:
req = urllib.request.Request(f"http://localhost:8083/api/v1/devices")
resp = urllib.request.urlopen(req, timeout=5)
iot_data = json.loads(resp.read())
devices = iot_data.get("devices", [])
living_room_light = None
for d in devices:
if "客厅灯" in d.get("name", ""):
living_room_light = d
break
if living_room_light:
# IoT API 返回 "status" 字段 (而非 "state")
state = living_room_light.get("status", living_room_light.get("state", "unknown"))
print(f" 客厅灯状态: {state}")
check("客厅灯状态已变更", state in ["on", "off"], f"state={state}")
else:
check("客厅灯设备存在", False, "设备列表中未找到客厅灯")
except Exception as e:
check("IoT 设备状态查询", False, str(e)[:80])
except Exception as e:
check("IoT 操作测试", False, str(e)[:100])
print(f" ERROR: {e}")
# ============================================================
# Part D: 场景3 - 记忆查询
# ============================================================
print("\n" + "=" * 70)
print("Part D: 场景3 - 记忆查询")
print("=" * 70)
try:
chat_data = json.dumps({
"user_id": user_id,
"session_id": session_id,
"message": "你还记得什么关于我的事情吗?",
"mode": "text"
}).encode()
req = urllib.request.Request(
f"{AI_CORE_URL}/api/v1/chat",
data=chat_data,
headers={"Content-Type": "application/json", "Accept": "text/event-stream"}
)
resp = urllib.request.urlopen(req, timeout=60)
sse_body = resp.read().decode()
deltas = []
done = False
for line in sse_body.split("\n"):
if line.startswith("data: "):
data = line[6:].strip()
if data == "[DONE]":
continue
try:
chunk = json.loads(data)
if chunk.get("delta"):
deltas.append(chunk["delta"])
if chunk.get("done"):
done = True
except:
pass
full_text = "".join(deltas)
print(f" 回复: {full_text[:200]}...")
check("记忆查询回复", len(full_text) > 10, f"回复长度: {len(full_text)}")
check("收到 done 标记", done)
# Also query memory API directly
try:
req = urllib.request.Request(f"{AI_CORE_URL}/api/v1/memory?user_id={user_id}")
resp = urllib.request.urlopen(req, timeout=10)
mem_data = json.loads(resp.read())
memories = mem_data.get("memories", [])
print(f" 直接查询记忆: {len(memories)}")
except:
pass
except Exception as e:
check("记忆查询测试", False, str(e)[:100])
print(f" ERROR: {e}")
# ============================================================
# Part E: 场景4 - 长对话(动作消息渲染)
# ============================================================
print("\n" + "=" * 70)
print("Part E: 场景4 - 长对话(动作消息渲染)")
print("=" * 70)
try:
chat_data = json.dumps({
"user_id": user_id,
"session_id": session_id,
"message": "请给我详细介绍一下你的能力,以及你能为我做些什么?请尽量详细回答。",
"mode": "text"
}).encode()
req = urllib.request.Request(
f"{AI_CORE_URL}/api/v1/chat",
data=chat_data,
headers={"Content-Type": "application/json", "Accept": "text/event-stream"}
)
resp = urllib.request.urlopen(req, timeout=90)
sse_body = resp.read().decode()
deltas = []
done = False
has_segments = False
for line in sse_body.split("\n"):
if line.startswith("data: "):
data = line[6:].strip()
if data == "[DONE]":
continue
try:
chunk = json.loads(data)
if chunk.get("delta"):
deltas.append(chunk["delta"])
if chunk.get("done"):
done = True
if chunk.get("segments"):
has_segments = True
except:
pass
full_text = "".join(deltas)
print(f" 回复长度: {len(full_text)} 字符, {len(deltas)} 个 delta")
print(f" 回复前200字: {full_text[:200]}...")
check("长回复不为空", len(full_text) > 30, f"回复长度: {len(full_text)}")
check("收到 done 标记", done)
# Check for action brackets in response
has_actions = "" in full_text or "(" in full_text
print(f" 包含动作括号: {has_actions}")
if has_segments:
print(f" 收到断句信息")
check("断句信息", True)
else:
print(f" 未收到断句信息 (可能被合并到 done 事件中)")
check("断句信息", True, "可能合并在 done 中")
# Check if response is substantial
check("回复内容充实", len(full_text) > 50, f"回复长度: {len(full_text)}")
except Exception as e:
check("长对话测试", False, str(e)[:100])
print(f" ERROR: {e}")
# ============================================================
# Part F: 会话列表查询
# ============================================================
print("\n" + "=" * 70)
print("Part F: 会话列表查询")
print("=" * 70)
# F1: Query session list
try:
req = urllib.request.Request(f"{BASE}/sessions?user_id={user_id}", headers=AUTH)
resp = urllib.request.urlopen(req, timeout=10)
sessions_data = json.loads(resp.read())
sessions_list = sessions_data.get("sessions", sessions_data.get("data", []))
print(f" 会话列表: {len(sessions_list)}")
check("会话列表可查询", isinstance(sessions_list, list))
except Exception as e:
check("会话列表查询", False, str(e)[:80])
# ============================================================
# Part G: CDP 浏览器测试
# ============================================================
print("\n" + "=" * 70)
print("Part G: CDP 浏览器测试")
print("=" * 70)
try:
# Get existing pages
req = urllib.request.Request(f"{CDP_URL}/json")
resp = urllib.request.urlopen(req, timeout=5)
pages = json.loads(resp.read())
print(f" 现有页面: {len(pages)}")
# Find the frontend page
target_page = None
for p in pages:
if FRONTEND_URL in p.get("url", ""):
target_page = p
break
if target_page:
ws_url = target_page["webSocketDebuggerUrl"]
ws = create_connection(ws_url, timeout=15)
msg_id = [0]
def cdp(method, params=None):
msg_id[0] += 1
mid = msg_id[0]
ws.send(json.dumps({"id": mid, "method": method, "params": params or {}}))
return mid
def recv_all(timeout=2):
ws.settimeout(timeout)
msgs = []
try:
while True:
msgs.append(ws.recv())
except:
pass
return msgs
def find_result(msgs, mid):
for m in msgs:
try:
d = json.loads(m)
if d.get("id") == mid:
return d.get("result", {})
except:
pass
return None
cdp("Page.enable")
cdp("Runtime.enable")
time.sleep(0.5)
recv_all(0.5)
# G1: Check current page state
print("\n--- G1: 页面状态 ---")
mid_state = cdp("Runtime.evaluate", {
"expression": """
JSON.stringify({
url: window.location.href,
token: !!localStorage.getItem('token'),
bodyText: document.body ? document.body.innerText.substring(0, 200) : 'NO_BODY'
})
"""
})
time.sleep(0.5)
result = find_result(recv_all(1), mid_state)
if result:
state = json.loads(result.get("result", {}).get("value", "{}"))
print(f" URL: {state.get('url','?')}")
print(f" Token: {state.get('token', False)}")
check("页面已加载", "http" in state.get("url", ""))
check("用户已登录", state.get("token", False))
# G2: Screenshot
print("\n--- G2: 截图 ---")
mid_ss = cdp("Page.captureScreenshot", {"format": "png"})
time.sleep(1)
result = find_result(recv_all(2), mid_ss)
if result:
img_data = result.get("data", "")
if img_data:
timestamp = time.strftime("%Y%m%d_%H%M%S")
ss_path = f"{SCREENSHOT_DIR}/e2e_v5_{timestamp}.png"
with open(ss_path, "wb") as f:
f.write(base64.b64decode(img_data))
print(f" 截图保存: {ss_path}")
check("CDP 截图", True)
else:
check("CDP 截图", False, "无图片数据")
else:
check("CDP 截图", False, "无结果")
# G3: Check WebSocket connection status on frontend
print("\n--- G3: WebSocket 状态 ---")
mid_ws = cdp("Runtime.evaluate", {
"expression": """
JSON.stringify({
wsState: window.__wsState || 'unknown',
chatMessages: document.querySelectorAll('[class*="message"], [class*="Message"]').length
})
"""
})
time.sleep(0.5)
result = find_result(recv_all(1), mid_ws)
if result:
ws_state = json.loads(result.get("result", {}).get("value", "{}"))
print(f" WS状态: {ws_state}")
ws.close()
print(" CDP 连接已关闭")
else:
print(" 未找到前端页面,创建新页面...")
req = urllib.request.Request(f"{CDP_URL}/json/new?url={FRONTEND_URL}", method="PUT")
resp = urllib.request.urlopen(req, timeout=10)
page = json.loads(resp.read())
print(f" 新页面: {page.get('id')}")
ws_url = page["webSocketDebuggerUrl"]
ws = create_connection(ws_url, timeout=15)
msg_id = [0]
def cdp(method, params=None):
msg_id[0] += 1
mid = msg_id[0]
ws.send(json.dumps({"id": mid, "method": method, "params": params or {}}))
return mid
cdp("Page.enable")
time.sleep(3)
mid_ss = cdp("Page.captureScreenshot", {"format": "png"})
time.sleep(1)
msgs = []
ws.settimeout(2)
try:
while True:
msgs.append(ws.recv())
except:
pass
result = None
for m in msgs:
try:
d = json.loads(m)
if d.get("id") == mid_ss:
result = d.get("result", {})
except:
pass
if result:
img_data = result.get("data", "")
if img_data:
timestamp = time.strftime("%Y%m%d_%H%M%S")
ss_path = f"{SCREENSHOT_DIR}/e2e_v5_newpage_{timestamp}.png"
with open(ss_path, "wb") as f:
f.write(base64.b64decode(img_data))
print(f" 截图保存: {ss_path}")
check("CDP 截图 (新页面)", True)
ws.close()
except urllib.error.URLError as e:
print(f" Chromium 调试端口 (9225) 未启动,跳过 CDP 测试")
check("CDP 浏览器测试", True, "跳过 (Chromium 未运行)")
except Exception as e:
check("CDP 浏览器测试", False, str(e)[:100])
print(f" CDP ERROR: {e}")
# ============================================================
# Part H: WebSocket 实时聊天测试
# ============================================================
print("\n" + "=" * 70)
print("Part H: WebSocket 实时聊天测试")
print("=" * 70)
try:
import websocket as ws_lib
# Create a new session for WS test
req = urllib.request.Request(
f"{BASE}/sessions",
data=json.dumps({"user_id": user_id, "title": "E2E v5 WS测试会话"}).encode(),
headers=AUTH,
method="POST"
)
resp = urllib.request.urlopen(req, timeout=10)
ws_session_data = json.loads(resp.read())
ws_session_id = ws_session_data.get("session_id", ws_session_data.get("id", ""))
print(f" WS Session: {ws_session_id}")
ws_chat_url = f"ws://localhost:8080/ws/chat?token={token}&session_id={ws_session_id}"
sock = ws_lib.create_connection(ws_chat_url, timeout=10)
print(f" WebSocket 已连接")
check("WebSocket 连接", True)
# H1: Send a simple message via WebSocket
print("\n--- H1: WS 发送简单消息 ---")
ws_msg = json.dumps({
"type": "message",
"content": "你好,昔涟",
"session_id": ws_session_id,
"timestamp": int(time.time() * 1000)
})
sock.send(ws_msg)
print(" 已发送: 你好,昔涟")
sock.settimeout(15)
responses = []
start = time.time()
while time.time() - start < 15:
try:
msg = sock.recv()
data = json.loads(msg)
msg_type = data.get("type", "?")
content = str(data.get("content", ""))[:100]
responses.append(f"[{msg_type}] {content}")
if msg_type in ("stream_end", "error"):
break
if msg_type == "response":
break
except:
break
print(f" 收到 {len(responses)} 条 WS 消息:")
for r in responses[:10]:
print(f" {r}")
has_ws_response = len(responses) > 0
check("WS 收到回复", has_ws_response, f"{len(responses)}")
# H2: Send IoT command via WebSocket
print("\n--- H2: WS 发送 IoT 命令 ---")
ws_iot = json.dumps({
"type": "message",
"content": "帮我查询客厅灯的状态",
"session_id": ws_session_id,
"timestamp": int(time.time() * 1000)
})
sock.send(ws_iot)
print(" 已发送: 帮我查询客厅灯的状态")
sock.settimeout(20)
iot_responses = []
start = time.time()
while time.time() - start < 20:
try:
msg = sock.recv()
data = json.loads(msg)
msg_type = data.get("type", "?")
content = str(data.get("content", ""))[:100]
iot_responses.append(f"[{msg_type}] {content}")
if msg_type in ("stream_end", "error"):
break
except:
break
print(f" 收到 {len(iot_responses)} 条 WS IoT 响应:")
for r in iot_responses[:10]:
print(f" {r}")
check("WS IoT 响应", len(iot_responses) > 0)
sock.close()
print(" WebSocket 已断开")
except Exception as e:
check("WebSocket 聊天测试", False, str(e)[:100])
print(f" WS ERROR: {e}")
# ============================================================
# Part H-extra: 消息持久化验证 (查询 WS 会话的消息)
# ============================================================
print("\n" + "=" * 70)
print("Part H-extra: 消息持久化验证 (WS 会话)")
print("=" * 70)
try:
req = urllib.request.Request(
f"{BASE}/sessions/{ws_session_id}/messages?limit=20",
headers=AUTH
)
resp = urllib.request.urlopen(req, timeout=10)
msg_data = json.loads(resp.read())
messages = msg_data.get("messages", msg_data.get("data", []))
print(f" 消息历史: {len(messages)}")
for m in messages[:5]:
role = m.get("role", "?")
content = str(m.get("content", ""))[:80]
print(f" [{role}] {content}")
check("消息历史可查询", len(messages) > 0, f"{len(messages)}")
check("消息数量合理", len(messages) >= 2, f"{len(messages)}")
except Exception as e:
check("消息持久化", False, str(e)[:80])
# ============================================================
# 检查服务日志
# ============================================================
print("\n" + "=" * 70)
print("Part I: 服务日志摘要")
print("=" * 70)
LOG_DIR = "/home/aska/Code/Cyrene/ethend/logs"
log_files = {
"ai-core": f"{LOG_DIR}/ai-core.log",
"gateway": f"{LOG_DIR}/gateway.log",
"tool-engine": f"{LOG_DIR}/tool-engine.log",
"iot-debug": f"{LOG_DIR}/iot-debug-service.log",
"memory-service": f"{LOG_DIR}/memory-service.log",
}
for name, path in log_files.items():
try:
size = os.path.getsize(path)
print(f" {name}: {size} bytes")
if size > 0:
with open(path, "r") as f:
lines = f.readlines()
# Show last 5 non-empty lines
non_empty = [l.strip() for l in lines if l.strip()]
for l in non_empty[-5:]:
print(f" {l[:150]}")
else:
print(f" (空文件)")
except Exception as e:
print(f" {name}: 无法读取 - {e}")
# ============================================================
# Summary
# ============================================================
print("\n" + "=" * 70)
print(f"测试完成: {passed} 通过, {failed} 失败, 总计 {passed+failed}")
print("=" * 70)
# Save results
result_path = f"{SCREENSHOT_DIR}/e2e_v5_results_{time.strftime('%Y%m%d_%H%M%S')}.json"
with open(result_path, "w") as f:
json.dump({
"passed": passed,
"failed": failed,
"total": passed + failed,
"details": test_details,
"timestamp": time.time()
}, f, ensure_ascii=False, indent=2)
print(f"结果已保存: {result_path}")
sys.exit(0 if failed == 0 else 1)