test(bot-tier): UI实证脚本 SuggestionCenter 默认未应用+类型过滤 (P0-②)

CDP登录→/ai-suggestions→待批列表默认unapplied且可见Bot建议卡+过滤芯片DOM全命中
临时建议SUG_ID=300已清理(回查0)
This commit is contained in:
Hermes CI Fix
2026-09-03 18:19:58 +08:00
parent be71c2296e
commit 9a823e80a6
2 changed files with 135 additions and 0 deletions
@@ -0,0 +1,133 @@
#!/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()
+2
View File
@@ -70,5 +70,7 @@
- 回归: backend/tests/test_bot_suggestion_channel.py(12条:鉴权/语义/多租户/幂等/审批闭环/审计/收口);test_risk_levels.py 计数更新 L2=6。
- 生产E2E: backend/scripts/verify_bot_write_tiering_e2e.py(可复跑,标记+清理+回查)2026-09-03 全 PASS:
A Bot创建kpi_target建议→unapplied → B 人apply→KPI目标100→888.88 + apply_detail(before/after) + applied_by=管理员/applied_at → C /kpi-result 直写 source_batch=e2e-bot-tier-20260903 落库 → D bot_audit.log 两条 L2 记录(财务BOT / bot-bridge)→ E 清理回查=0。
- UI实证: backend/scripts/verify_suggestioncenter_unapplied_ui.py(CDP,9225)2026-09-03 全 PASS:
登录→/ai-suggestions→待批列表默认 status=unapplied 且可见 Bot建议卡片(E2E-验证,临时建议验证后删除),类型过滤芯片(全部类型/改KPI目标/调预算/建行动方案)与状态radio(未应用/已应用/已忽略)在DOM命中。
- pytest 全量: 726 passed / 4 failed(日历漂移预存:budget roll_forward + ai_suggestions budget_overrun + roadmap budget_headroom×2,非本次回归) / 1 xfailed。
- 后端重启: systemctl restart cma-backend(ActiveEnter 晚于 commit); /health ok。