b15e1c9541
- 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
355 lines
13 KiB
Python
355 lines
13 KiB
Python
#!/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)
|