Files
Cyrene/debug/cache/test_cdp_e2e_v3.py
T
AskaEth b15e1c9541 fix: 第二轮深度调试修复 - useSpeechSynthesis守卫+NPE防护+E2E测试完善
- P0: useSpeechSynthesis.ts cancel()增加isSupported守卫
- P0: iot_provider.go 添加loader nil检查防止NPE panic
- 新增CDP E2E v4测试脚本 14项全绿通过
- 生成第二轮修复报告 docs/debug/2026-05-21-round2-fixes.md
2026-05-22 00:10:37 +08:00

355 lines
13 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 综合测试 v3 — 完整端到端验证(登录后测试所有功能)"""
import json, time, urllib.request, sys
from websocket import create_connection
# 配置
BASE_URL = "http://localhost:8080/api/v1"
FRONTEND_URL = "http://localhost:5199"
CDP_URL = "http://127.0.0.1:9225"
CREDENTIALS = {"username": "yeij0942", "password": "Jiang1143218570"}
passed = 0
failed = 0
def check(name, condition, detail=""):
global passed, failed
if condition:
passed += 1
print(f"{name}")
else:
failed += 1
print(f"{name} {detail}")
# ============================================================
# Part A: Backend API Tests
# ============================================================
print("=" * 60)
print("Part A: Backend API 测试")
print("=" * 60)
# A1: Health checks
print("\n--- A1: 微服务健康检查 ---")
for svc, port in [("gateway", 8080), ("ai-core", 8081), ("memory-service", 8091), ("tool-engine", 8092), ("iot-debug", 8083)]:
try:
req = urllib.request.Request(f"http://localhost:{port}/api/v1/health", method="GET")
resp = urllib.request.urlopen(req, timeout=5)
check(f"{svc}:{port} 健康检查", resp.status == 200, f"status={resp.status}")
except Exception as e:
check(f"{svc}:{port} 健康检查", False, str(e)[:60])
# A2: Login
print("\n--- A2: 用户登录 ---")
try:
req = urllib.request.Request(
f"{BASE_URL}/auth/login",
data=json.dumps(CREDENTIALS).encode(),
headers={"Content-Type": "application/json"},
method="POST"
)
resp = urllib.request.urlopen(req, timeout=10)
login_data = json.loads(resp.read())
access_token = login_data.get("data", {}).get("token", "")
refresh_token_val = login_data.get("data", {}).get("refresh_token", "")
user_id = login_data.get("data", {}).get("user_id", "")
check("登录成功", bool(access_token), f"token={access_token[:20]}...")
check("获取 refresh_token", bool(refresh_token_val))
check("获取 user_id", bool(user_id), f"user_id={user_id}")
except Exception as e:
check("登录", False, str(e)[:80])
access_token = ""
refresh_token_val = ""
AUTH_HEADER = {"Authorization": f"Bearer {access_token}", "Content-Type": "application/json"}
# A3: Token Refresh
if refresh_token_val:
print("\n--- A3: Token 刷新 ---")
try:
req = urllib.request.Request(
f"{BASE_URL}/auth/refresh",
data=json.dumps({"refresh_token": refresh_token_val}).encode(),
headers={"Content-Type": "application/json"},
method="POST"
)
resp = urllib.request.urlopen(req, timeout=10)
refresh_data = json.loads(resp.read())
new_token = refresh_data.get("data", {}).get("token", "")
check("Token 刷新成功", bool(new_token), f"new_token={new_token[:20]}...")
if new_token:
access_token = new_token
AUTH_HEADER = {"Authorization": f"Bearer {access_token}", "Content-Type": "application/json"}
except Exception as e:
check("Token 刷新", False, str(e)[:80])
# A4: Session CRUD
print("\n--- A4: 会话管理 ---")
session_id = ""
try:
# Create session
req = urllib.request.Request(
f"{BASE_URL}/sessions",
data=json.dumps({"user_id": user_id, "title": "CDP E2E Test"}).encode(),
headers=AUTH_HEADER,
method="POST"
)
resp = urllib.request.urlopen(req, timeout=10)
session_data = json.loads(resp.read())
session_id = session_data.get("data", {}).get("id", "")
check("创建会话", bool(session_id), f"session_id={session_id}")
# List sessions
req = urllib.request.Request(f"{BASE_URL}/sessions?user_id={user_id}", headers=AUTH_HEADER)
resp = urllib.request.urlopen(req, timeout=10)
sessions = json.loads(resp.read())
check("列出会话", isinstance(sessions.get("data"), list), f"count={len(sessions.get('data', []))}")
except Exception as e:
check("会话管理", False, str(e)[:80])
# A5: Memory Search
print("\n--- A5: 记忆搜索 ---")
try:
req = urllib.request.Request(
f"{BASE_URL}/memory/search",
data=json.dumps({"user_id": user_id, "query": "test"}).encode(),
headers=AUTH_HEADER,
method="POST"
)
resp = urllib.request.urlopen(req, timeout=10)
mem_data = json.loads(resp.read())
check("记忆搜索API正常", resp.status in [200, 404], f"status={resp.status}")
except Exception as e:
check("记忆搜索", False, str(e)[:60])
# A6: IoT Devices
print("\n--- A6: IoT 设备 ---")
try:
req = urllib.request.Request(f"{BASE_URL}/iot/devices", headers=AUTH_HEADER)
resp = urllib.request.urlopen(req, timeout=10)
iot_data = json.loads(resp.read())
devices = iot_data.get("data", {}).get("devices", iot_data.get("data", []))
check("IoT设备列表API正常", resp.status in [200, 404], f"status={resp.status}, devices={len(devices) if isinstance(devices, list) else 'N/A'}")
except Exception as e:
check("IoT设备列表", False, str(e)[:60])
print(f"\nBackend 测试结果: {passed} 通过, {failed} 失败")
# ============================================================
# Part B: Frontend CDP E2E Tests
# ============================================================
print("\n" + "=" * 60)
print("Part B: Frontend CDP E2E 测试")
print("=" * 60)
# B1: 创建新的浏览器页面 (about:blank 确保没有 Service Worker 缓存)
print("\n--- B1: 创建新页面 ---")
try:
req = urllib.request.Request(f"{CDP_URL}/json/new?url=about:blank", method="PUT")
resp = urllib.request.urlopen(req, timeout=10)
page = json.loads(resp.read())
ws_url = page["webSocketDebuggerUrl"]
page_id = page.get("id", "")
print(f" Page ID: {page_id}")
check("创建空白页面", bool(ws_url))
except Exception as e:
print(f" ❌ 创建页面失败: {e}")
sys.exit(1)
ws = create_connection(ws_url, timeout=15)
def cdp(method, params=None, msg_id=1):
ws.send(json.dumps({"id": msg_id, "method": method, "params": params or {}}))
def recv_all(timeout=3):
ws.settimeout(timeout)
msgs = []
try:
while True:
msgs.append(ws.recv())
except:
pass
return msgs
# Enable domains
cdp("Page.enable", {}, 1)
cdp("Network.enable", {}, 2)
cdp("Runtime.enable", {}, 3)
cdp("Log.enable", {}, 4)
time.sleep(0.5)
recv_all(0.5)
# B2: 导航到前端页面
print("\n--- B2: 导航到前端 ---")
cdp("Page.navigate", {"url": FRONTEND_URL + "/"}, 10)
time.sleep(4)
msgs = recv_all(3)
# 检查 console 日志和错误
has_js_error = False
console_logs = []
for m in msgs:
d = json.loads(m)
method = d.get("method", "")
params = d.get("params", {})
if method == "Runtime.exceptionThrown":
has_js_error = True
exc = params.get("exceptionDetails", {})
print(f" [JS ERROR] {exc.get('text', '')}")
elif method == "Runtime.consoleAPICalled":
args = params.get("args", [])
texts = [str(a.get("value", "") or a.get("description", ""))[:120] for a in args]
console_logs.append(" ".join(texts))
elif method == "Log.entryAdded":
entry = params.get("entry", {})
console_logs.append(f"[{entry.get('level','log')}] {entry.get('text','')[:120]}")
check("页面加载无 JS 异常", not has_js_error)
# B3: 检查 localStorage(应该为空或没有 test-token-cyrene
print("\n--- B3: localStorage 检查 ---")
cdp("Runtime.evaluate", {
"expression": "JSON.stringify({token: localStorage.getItem('token'), user_id: localStorage.getItem('user_id'), cyrene_store_version: localStorage.getItem('cyrene_store_version'), allKeys: Object.keys(localStorage)})"
}, 20)
time.sleep(0.5)
msgs = recv_all(1)
ls_data = {}
for m in msgs:
d = json.loads(m)
if d.get("id") == 20:
val = d.get("result", {}).get("result", {}).get("value", "")
ls_data = json.loads(val) if val else {}
print(f" localStorage: {ls_data}")
has_test_token = ls_data.get("token") == "test-token-cyrene"
check("无 test-token-cyrene", not has_test_token, "发现硬编码token!" if has_test_token else "")
# B4: 检查 DOM 渲染(登录页面应出现)
print("\n--- B4: DOM 检查 ---")
cdp("Runtime.evaluate", {
"expression": "document.querySelector('input') ? 'has_input' : 'no_input'"
}, 30)
time.sleep(0.5)
msgs = recv_all(1)
for m in msgs:
d = json.loads(m)
if d.get("id") == 30:
val = d.get("result", {}).get("result", {}).get("value", "")
check("登录表单已渲染", val == "'has_input'", f"got: {val}")
# 截图
cdp("Page.captureScreenshot", {"format": "png"}, 31)
time.sleep(1)
msgs = recv_all(2)
for m in msgs:
d = json.loads(m)
if d.get("id") == 31:
img_data = d.get("result", {}).get("data", "")
if img_data:
import base64
with open("/tmp/cdp_screenshot_before_login.png", "wb") as f:
f.write(base64.b64decode(img_data))
print(" 截图已保存到 /tmp/cdp_screenshot_before_login.png")
# B5: 通过 API 直接在页面上设置 token(绕过登录UI)
print("\n--- B5: 注入 token 并模拟已登录状态 ---")
if access_token:
cdp("Runtime.evaluate", {
"expression": f"""
localStorage.setItem('token', '{access_token}');
localStorage.setItem('user_id', '{user_id}');
localStorage.setItem('cyrene_store_version', '1');
'token_injected'
"""
}, 40)
time.sleep(0.5)
recv_all(0.5)
# 重新加载页面
cdp("Page.reload", {}, 41)
time.sleep(4)
msgs = recv_all(3)
# 检查网络请求是否使用正确的 token
api_calls = []
console_errors = []
for m in msgs:
d = json.loads(m)
method = d.get("method", "")
params = d.get("params", {})
if method == "Network.requestWillBeSent":
req = params.get("request", {})
url = req.get("url", "")
headers = req.get("headers", {})
auth = headers.get("Authorization", "") or headers.get("authorization", "")
if "/api/" in url:
api_calls.append(f"{req.get('method','?')} {url[:100]} Auth={auth[:30]}...")
elif method == "Runtime.consoleAPICalled":
args = params.get("args", [])
if params.get("type") == "error":
texts = [str(a.get("value", "") or a.get("description", ""))[:150] for a in args]
console_errors.append(" ".join(texts))
elif method == "Runtime.exceptionThrown":
exc = params.get("exceptionDetails", {})
console_errors.append(f"JS Exception: {exc.get('text', '')}")
print(f" API 调用数: {len(api_calls)}")
for c in api_calls[:5]:
print(f" {c}")
check("API 调用使用正确 token", all("test-token-cyrene" not in c for c in api_calls), "仍使用旧token!" if any("test-token-cyrene" in c for c in api_calls) else "")
# 检查 WebSocket 连接状态
ws_connected = any("ws_connect" in c.lower() or "WebSocket" in c for c in console_logs)
has_auth_error = any("401" in e or "认证" in e or "无效" in e for e in console_errors)
check("无认证错误", not has_auth_error, str(console_errors[:2]) if console_errors else "")
# 截图 after login
cdp("Page.captureScreenshot", {"format": "png"}, 42)
time.sleep(1)
msgs = recv_all(2)
for m in msgs:
d = json.loads(m)
if d.get("id") == 42:
img_data = d.get("result", {}).get("data", "")
if img_data:
import base64
with open("/tmp/cdp_screenshot_after_login.png", "wb") as f:
f.write(base64.b64decode(img_data))
print(" 截图已保存到 /tmp/cdp_screenshot_after_login.png")
# B6: DOM 状态检查 - 应该看到侧边栏和聊天界面
print("\n--- B6: 登录后 UI 状态 ---")
checks_js = """
(() => {
const hasSidebar = !!document.querySelector('aside, [class*="sidebar"], [class*="Sidebar"]');
const hasChatInput = !!document.querySelector('textarea, [contenteditable="true"], input[type="text"]');
const hasHeader = !!document.querySelector('header, [class*="header"], [class*="Header"]');
const bodyText = document.body.innerText.substring(0, 200);
return JSON.stringify({hasSidebar, hasChatInput, hasHeader, bodyText});
})()
"""
cdp("Runtime.evaluate", {"expression": checks_js}, 50)
time.sleep(0.5)
msgs = recv_all(1)
for m in msgs:
d = json.loads(m)
if d.get("id") == 50:
val = d.get("result", {}).get("result", {}).get("value", "")
try:
ui = json.loads(val)
print(f" UI状态: sidebar={ui.get('hasSidebar')}, chatInput={ui.get('hasChatInput')}, header={ui.get('hasHeader')}")
print(f" Body文本: {ui.get('bodyText', '')[:100]}")
check("侧边栏已渲染", ui.get("hasSidebar", False))
check("聊天输入框存在", ui.get("hasChatInput", False))
except:
pass
ws.close()
# ============================================================
# Summary
# ============================================================
print("\n" + "=" * 60)
print(f"测试完成: {passed} 通过, {failed} 失败, 总计 {passed+failed}")
print("=" * 60)