#!/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/devtools/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)