chore: import original project baseline
Import the pre-repair source tree as the history baseline. Runtime data (data/), virtualenvs, bytecode caches and logs are gitignored so local secrets and user state stay out of the repo.
This commit is contained in:
@@ -0,0 +1,151 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""公式渲染端到端验证:真实会话消息 → 真实前端管线 → DOM 断言
|
||||
|
||||
运行: PYTHONIOENCODING=utf-8 QT_QPA_PLATFORM=offscreen python tests/verify_math_render.py
|
||||
|
||||
铁律:不污染真实 DB —— 先把 data/chat_history.db 复制到临时文件,再指向副本。
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import time
|
||||
import shutil
|
||||
import tempfile
|
||||
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
|
||||
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
|
||||
os.environ.setdefault("HAOCODE_RENDER", "software")
|
||||
os.environ["QTWEBENGINE_CHROMIUM_FLAGS"] = "--disable-gpu"
|
||||
|
||||
_REAL_DB = os.path.join(os.path.dirname(__file__), "..", "data", "chat_history.db")
|
||||
_TMP_DB = os.path.join(tempfile.gettempdir(), f"haocode_verify_math_{os.getpid()}.db")
|
||||
shutil.copyfile(_REAL_DB, _TMP_DB)
|
||||
|
||||
import core.db_manager as _dbm # noqa: E402
|
||||
_dbm._DEFAULT_DB = _TMP_DB
|
||||
|
||||
from PyQt6.QtWidgets import QApplication # noqa: E402
|
||||
from PyQt6.QtCore import QTimer # noqa: E402
|
||||
from ui.views.main_window import MainWindow # noqa: E402
|
||||
|
||||
app = QApplication(sys.argv)
|
||||
results = []
|
||||
win = {"w": None, "sid": None}
|
||||
|
||||
JS_QUERY = r"""(function(){
|
||||
var scope = document.getElementById('chat-container') || document.body;
|
||||
var q = function(s){ return scope.querySelectorAll(s).length; };
|
||||
// 只取助手消息(user 消息按设计是纯文本,本来就不渲染公式)
|
||||
var asst = scope.querySelectorAll('.message-wrapper.assistant');
|
||||
var asstScope = asst.length ? asst[asst.length - 1] : scope;
|
||||
// 可见文本:剔除 katex-mathml(CSS 视觉隐藏的 MathML 层,内部保留原始 tex 属正常)
|
||||
var clone = asstScope.cloneNode(true);
|
||||
clone.querySelectorAll('.katex-mathml').forEach(function(e){ e.parentNode.removeChild(e); });
|
||||
var visTxt = clone.textContent || '';
|
||||
var codeTxt = '';
|
||||
asstScope.querySelectorAll('pre, code').forEach(function(e){ codeTxt += e.textContent + '\n'; });
|
||||
return {
|
||||
katex: q('.katex'),
|
||||
display: q('.katex-display'),
|
||||
mathml: q('.katex-mathml'),
|
||||
vislayer: q('.katex-html'),
|
||||
leakedPlaceholder: visTxt.indexOf('@@K') !== -1,
|
||||
rawBackslash: visTxt.indexOf('\\operatorname') !== -1,
|
||||
bareBracketFormula: visTxt.indexOf('P_4=\\operatorname') !== -1,
|
||||
codeHasDollar: codeTxt.indexOf('$x + y$') !== -1,
|
||||
codeHasArr: codeTxt.indexOf('arr[0]') !== -1
|
||||
};
|
||||
})()"""
|
||||
|
||||
|
||||
def check(name, cond, extra=""):
|
||||
print((" PASS " if cond else " FAIL ") + name + ((" | " + str(extra)) if extra else ""))
|
||||
results.append(bool(cond))
|
||||
|
||||
|
||||
def run_js(js, timeout_s=15):
|
||||
r = {"v": None, "d": False}
|
||||
|
||||
def cb(val):
|
||||
if isinstance(val, str):
|
||||
try:
|
||||
val = json.loads(val)
|
||||
except Exception:
|
||||
pass
|
||||
r["v"] = val
|
||||
r["d"] = True
|
||||
|
||||
b = win["w"].browser
|
||||
if hasattr(b, "execute_js_async"):
|
||||
b.execute_js_async(js, cb)
|
||||
else:
|
||||
b.page().runJavaScript(js, cb)
|
||||
t0 = time.time()
|
||||
while not r["d"] and time.time() - t0 < timeout_s:
|
||||
app.processEvents()
|
||||
time.sleep(0.05)
|
||||
return r["v"]
|
||||
|
||||
|
||||
def boot():
|
||||
w = MainWindow()
|
||||
win["w"] = w
|
||||
sid = None
|
||||
for s in w.db.get_all_sessions():
|
||||
if s.get("title") == "公式渲染验收":
|
||||
sid = s["id"]
|
||||
break
|
||||
if not sid:
|
||||
print("FAIL 未找到「公式渲染验收」会话(先跑 tests/inject_math_demo.py)")
|
||||
app.quit()
|
||||
return
|
||||
win["sid"] = sid
|
||||
print(f"会话: {sid}")
|
||||
w.load_messages_to_web(sid)
|
||||
QTimer.singleShot(9000, phase_check)
|
||||
|
||||
|
||||
def phase_check():
|
||||
for _ in range(40):
|
||||
if run_js("window.jsReady === true ? 1 : 0", timeout_s=3) == 1:
|
||||
break
|
||||
time.sleep(0.5)
|
||||
|
||||
r = run_js(JS_QUERY)
|
||||
if not isinstance(r, dict):
|
||||
print(f"FAIL DOM 查询失败: {r!r}")
|
||||
app.quit()
|
||||
return
|
||||
print("\n DOM 统计: " + json.dumps(r, ensure_ascii=False))
|
||||
check("KaTeX 渲染出公式(.katex > 0)", r["katex"] > 0, f"katex={r['katex']}")
|
||||
check("块公式 7 个(.katex-display == 7)", r["display"] == 7, f"display={r['display']}")
|
||||
check("可视层存在(.katex-html > 0)", r["vislayer"] > 0, f"vislayer={r['vislayer']}")
|
||||
check("无占位符泄漏", not r["leakedPlaceholder"])
|
||||
check("可见文本无原始 tex 残留", not r["rawBackslash"])
|
||||
check("无裸括号公式残留", not r["bareBracketFormula"])
|
||||
check("代码块内 $x + y$ 保持原样", r["codeHasDollar"])
|
||||
check("代码块内 arr[0] 保持原样", r["codeHasArr"])
|
||||
|
||||
try:
|
||||
shot_path = os.path.join(os.path.dirname(__file__), "_tmp_math_render.png")
|
||||
win["w"].browser.grab().save(shot_path)
|
||||
print(f" 截图: {shot_path}")
|
||||
except Exception as e:
|
||||
print(f" 截图失败: {e}")
|
||||
|
||||
print(f"\n===== {'ALL PASS' if all(results) else 'HAS FAILURES'}: {sum(results)}/{len(results)} =====")
|
||||
try:
|
||||
win["w"].close()
|
||||
except Exception:
|
||||
pass
|
||||
app.quit()
|
||||
|
||||
|
||||
QTimer.singleShot(400, boot)
|
||||
QTimer.singleShot(120000, app.quit)
|
||||
app.exec()
|
||||
try:
|
||||
os.remove(_TMP_DB)
|
||||
except Exception:
|
||||
pass
|
||||
sys.exit(0 if all(results) else 1)
|
||||
Reference in New Issue
Block a user