From 9a823e80a684a741d6e6f8ba68e17491ccd5b7dc Mon Sep 17 00:00:00 2001 From: Hermes CI Fix Date: Thu, 3 Sep 2026 18:19:58 +0800 Subject: [PATCH] =?UTF-8?q?test(bot-tier):=20UI=E5=AE=9E=E8=AF=81=E8=84=9A?= =?UTF-8?q?=E6=9C=AC=20SuggestionCenter=20=E9=BB=98=E8=AE=A4=E6=9C=AA?= =?UTF-8?q?=E5=BA=94=E7=94=A8+=E7=B1=BB=E5=9E=8B=E8=BF=87=E6=BB=A4=20(P0-?= =?UTF-8?q?=E2=91=A1)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CDP登录→/ai-suggestions→待批列表默认unapplied且可见Bot建议卡+过滤芯片DOM全命中 临时建议SUG_ID=300已清理(回查0) --- .../verify_suggestioncenter_unapplied_ui.py | 133 ++++++++++++++++++ docs/bot-write-tiering-20260903.md | 2 + 2 files changed, 135 insertions(+) create mode 100644 backend/scripts/verify_suggestioncenter_unapplied_ui.py diff --git a/backend/scripts/verify_suggestioncenter_unapplied_ui.py b/backend/scripts/verify_suggestioncenter_unapplied_ui.py new file mode 100644 index 00000000..64f32e62 --- /dev/null +++ b/backend/scripts/verify_suggestioncenter_unapplied_ui.py @@ -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() diff --git a/docs/bot-write-tiering-20260903.md b/docs/bot-write-tiering-20260903.md index 8fbef4fc..cba35f1a 100644 --- a/docs/bot-write-tiering-20260903.md +++ b/docs/bot-write-tiering-20260903.md @@ -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。