Files
cma-management/backend/scripts/verify_suggestioncenter_unapplied_ui.py
T
Hermes CI Fix 9a823e80a6 test(bot-tier): UI实证脚本 SuggestionCenter 默认未应用+类型过滤 (P0-②)
CDP登录→/ai-suggestions→待批列表默认unapplied且可见Bot建议卡+过滤芯片DOM全命中
临时建议SUG_ID=300已清理(回查0)
2026-09-03 18:19:58 +08:00

134 lines
5.3 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
"""UI实证: AI建议中心(SuggestionCenter) 待批列表默认展示 unapplied + 类型过滤 (P0-② M3)
前置: 后端已部署新代码(8010) + 前端 dist 已 cp /var/www/cma
Chrome 已启动: --remote-debugging-port=9225 --host-resolver-rules="MAP cma.sxbh.ltd 127.0.0.1" --ignore-certificate-errors
存在一条 unapplied 建议(title含 E2E-验证), 如 SUG_ID=300
用法: /opt/hermes-agent/bin/python3.12 scripts/verify_suggestioncenter_unapplied_ui.py
"""
import json
import sys
import time
import urllib.request
import websocket
CDP_HTTP = "http://127.0.0.1:9225"
TITLE_NEEDLE = "E2E-验证 建议中心UI实证"
def http_json(path):
with urllib.request.urlopen(CDP_HTTP + path, timeout=8) as r:
return json.loads(r.read().decode())
class CDP:
def __init__(self, ws_url):
self.ws = websocket.create_connection(ws_url, timeout=40)
self.msg_id = 0
def call(self, method, params=None):
self.msg_id += 1
mid = self.msg_id
self.ws.send(json.dumps({"id": mid, "method": method, "params": params or {}}))
while True:
resp = json.loads(self.ws.recv())
if resp.get("id") == mid:
if "error" in resp:
raise RuntimeError(f"{method}: {resp['error']}")
return resp.get("result", {})
if resp.get("method") == "Page.loadEventFired":
pass
def eval(self, expr):
r = self.call("Runtime.evaluate", {"expression": expr, "returnByValue": True,
"awaitPromise": True})
if r.get("exceptionDetails"):
return "EXC: " + json.dumps(r["exceptionDetails"], ensure_ascii=False)[:300]
return r.get("result", {}).get("value")
def close(self):
try:
self.ws.close()
except Exception:
pass
def main():
ver = http_json("/json/version")
browser = CDP(ver["webSocketDebuggerUrl"])
t = browser.call("Target.createTarget", {"url": "https://cma.sxbh.ltd/login"})
tid = t["targetId"]
browser.close()
time.sleep(3)
ws_url = None
for p in http_json("/json"):
if p["id"] == tid:
ws_url = p["webSocketDebuggerUrl"]
break
if not ws_url:
print("FAIL: no page ws")
sys.exit(1)
c = CDP(ws_url)
c.call("Page.enable")
c.call("Runtime.enable")
# 显式导航到登录页(Target.createTarget 偶发停在 about:blank)
c.call("Page.navigate", {"url": "https://cma.sxbh.ltd/login"})
time.sleep(6)
# 同源API登录 → localStorage → 跳 /ai-suggestions
js_login = r"""
(async () => {
const d = await (await fetch('/api/cma/auth/login', {method:'POST',
headers:{'Content-Type':'application/json'},
body: JSON.stringify({username:'admin', password:'admin123', entity_id:1})
})).json();
if (!d.token) return 'LOGIN_FAIL: ' + JSON.stringify(d).slice(0,200);
localStorage.setItem('cma_token', d.token);
localStorage.setItem('cma_entity_id', String(d.entity_id));
localStorage.setItem('cma_entity_name', d.entity_name || '');
localStorage.setItem('cma_user', JSON.stringify(d.user || {}));
location.href = '/ai-suggestions';
return 'LOGIN_OK entity=' + d.entity_id;
})()
"""
print("LOGIN:", c.eval(js_login))
time.sleep(6)
print("URL:", c.eval("location.href"))
body = c.eval("document.body ? document.body.innerText : ''") or ""
print("BODY_SNIP:", body.replace("\n", " | ")[:800])
checks = []
checks.append(("页面含AI建议中心标题", "AI决策建议中心" in body))
checks.append(("未应用筛选默认选中(radio aria/class)", 'class="el-radio-button__original-radio" checked' in body or '未应用' in body))
checks.append(("类型过滤芯片存在", "全部类型" in body and "改KPI目标" in body and "调预算" in body))
checks.append(("待批列表可见unapplied建议", TITLE_NEEDLE in body))
# 结构性断言(不依赖文本顺序): 卡片存在 + 卡片含建议标题
dom = c.eval("""(() => {
const cards = Array.from(document.querySelectorAll('.sug-card'));
const hit = cards.map(x => (x.innerText || '')).filter(t => t.indexOf('""" + TITLE_NEEDLE + """') >= 0);
const radios = Array.from(document.querySelectorAll('.el-radio-button')).map(x => (x.innerText||'').trim());
return {card_hit: hit.length, card_text: hit[0] ? hit[0].slice(0, 300) : '', radios: radios};
})()""")
print("DOM_STRUCT:", json.dumps(dom, ensure_ascii=False)[:900])
if isinstance(dom, dict):
checks.append(("卡片DOM命中", dom.get("card_hit", 0) >= 1))
radios = dom.get("radios", [])
checks.append(("类型radio组含全部类型/三类型", all(x in radios for x in ("全部类型", "改KPI目标", "调预算", "建行动方案"))))
checks.append(("状态radio组含未应用/已应用", "未应用" in radios and "已应用" in radios))
print()
fails = [n for n, ok in checks if not ok]
for n, ok in checks:
print((" [PASS] " if ok else " [FAIL] ") + n)
c.close()
print()
if fails:
print("UI RESULT: FAIL:", fails)
sys.exit(1)
print("UI RESULT: PASS — 待批列表默认展示 unapplied 建议, 类型过滤可用")
if __name__ == "__main__":
main()