Files
Haocode/tests/smoke_offscreen.py
T
2026-09-17 16:30:02 +08:00

207 lines
7.5 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.
"""offscreen 冒烟测试:主窗口实例化 + 核心链路(不启动真实 LLM)
运行: QT_QPA_PLATFORM=offscreen python tests/smoke_offscreen.py
"""
import os
import sys
import json
import time
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" # 绕过 AMD 核显 context lost # 软渲染,离屏最稳
# 铁律:测试不得污染真实 data/chat_history.db → DBManager 默认路径重定向到临时文件
import tempfile as _tf # noqa: E402
import core.db_manager as _dbm # noqa: E402
_dbm._DEFAULT_DB = os.path.join(_tf.gettempdir(), f"haocode_test_smoke_offscreen_{os.getpid()}.db")
# P1-03QtWebEngine 独立 profile 重定向到临时目录(不占项目 data/webengine/
os.environ.setdefault(
"HAOCODE_WEBENGINE_PROFILE_DIR",
os.path.join(_tf.gettempdir(), f"haocode_test_smoke_offscreen_{os.getpid()}_profile"))
import ctypes # noqa: E402
from PyQt6.QtWidgets import QApplication # noqa: E402
from PyQt6.QtCore import QTimer # noqa: E402
# QtWebEngine 必须在 QApplication 创建前 import
from ui.views.main_window import MainWindow # noqa: E402
app = QApplication(sys.argv)
results = []
def check(name, fn):
try:
fn()
print(f" PASS {name}")
results.append(True)
except Exception as e:
import traceback
traceback.print_exc()
print(f" FAIL {name}: {e}")
results.append(False)
window = None
page_ready = {"ok": False}
def on_page_load_progress(v):
pass
def on_js_console(level, msg, line, src):
pass
def try_load():
global window
try:
window = MainWindow()
page_ready["ok"] = True
except Exception:
import traceback
traceback.print_exc()
finish(False)
return
# MainWindow 构造完成时 HTML/JS 已就绪(loadFinished 可能早于连接)
QTimer.singleShot(6000, run_checks) # 等 JS 引擎 + 历史渲染完成
def run_checks():
# 1) 窗口已创建
check("MainWindow 实例化", lambda: (_ for _ in ()).throw(AssertionError("no window")) if window is None else None)
# 2) DB 链路
def db_chain():
assert window.db is not None
sessions = window.db.get_all_sessions()
assert isinstance(sessions, list)
check("DB 会话列表", db_chain)
# 3) build_api_contextP0 修复验证:不再有 reasoning 字段)
def ctx_build():
sid = window.current_session_id
if not sid:
return
msgs = window.build_api_context(sid)
assert isinstance(msgs, list)
for m in msgs:
assert "reasoning" not in m, f"reasoning 字段仍在 API payload 里: {m.get('role')}"
check("build_api_context 无 reasoning 字段", ctx_build)
# 4) CJK token 估算
def est():
n = window._estimate_token_count([
{"role": "user", "content": "你好,世界!这是一段中文测试。"},
{"role": "assistant", "content": "hello world " * 20},
])
assert isinstance(n, int) and n > 0
check("CJK token 估算", est)
# 5) AgentWorker 可构造 + 信号齐全
def worker():
from core.llm_engine import AgentWorker, TitleWorker
w = AgentWorker(window.current_provider, window.current_model,
[{"role": "user", "content": "x"}])
for sig in ("chunk_received", "reasoning_received", "error_occurred",
"tool_execution_started", "tool_execution_updated",
"tool_execution_finished", "context_compacted"):
assert hasattr(w, sig), sig
t = TitleWorker(window.current_provider, window.current_model,
[{"role": "user", "content": "x"}])
assert hasattr(t, "chunk_received")
check("AgentWorker/TitleWorker 构造", worker)
# 6) bridge 工具方法
def bridge():
b = window.chat_bridge
for m in ("tool_execution_started", "tool_execution_updated",
"tool_execution_finished", "show_note"):
assert hasattr(b, m), m
check("ChatBridge 工具事件方法", bridge)
# 7) 离屏渲染 JS 就绪
check("Web 页面加载完成", lambda: (_ for _ in ()).throw(AssertionError("page not ready")) if not page_ready["ok"] else None)
# 8) 🌟 KaTeX 公式渲染(真实页面上下文:资源加载 + [...] 供应商格式 + 行内 $)
def _run_js(js, timeout_s=10):
result = {"val": None, "done": False}
def on_ret(val):
if isinstance(val, str):
try:
val = json.loads(val)
except Exception:
pass
result["val"] = val
result["done"] = True
if hasattr(window.browser, "execute_js_async"): # WebView2 路径(cb 收 JSON 字符串)
window.browser.execute_js_async(js, on_ret)
else: # QtWebEngine 路径(cb 收 Python 对象)
window.browser.page().runJavaScript(js, on_ret)
t0 = time.time()
while not result["done"] and time.time() - t0 < timeout_s:
app.processEvents()
time.sleep(0.05)
assert result["done"], "JS 执行超时"
return result["val"]
def katex_render():
# 等页面 JS 就绪(WV2 冷启动可能慢;app.js 就绪时置 window.jsReady=true
for _ in range(30):
if _run_js("window.jsReady === true ? 1 : 0", timeout_s=3) == 1:
break
time.sleep(0.5)
else:
raise AssertionError("页面 JS 未就绪(jsReady")
ver = _run_js("typeof katex !== 'undefined' ? katex.version : null")
assert ver, "katex 全局缺失(资源加载失败?)"
js = ("(function(){var s = '结果:" + chr(92) + "n[" + chr(92) + "nP_4=" + chr(92)*2 + "operatorname{BRF}(M_4,M_5)" + chr(92) + "n]" + chr(92) + "n" + chr(92) + "n行内 $E=mc^2$ 结束。';"
"var html = safeHtml(marked.parse(s));return {"
"has: html.indexOf('katex') !== -1,"
"disp: (html.match(/katex-display/g) || []).length,"
'inline: (html.match(/class=\"katex\"/g) || []).length,'
"leaked: html.indexOf('@@K') !== -1,"
"vis: html.indexOf('katex-html') !== -1,"
"tex: html.indexOf('\\operatorname{BRF}') !== -1};})()")
r = _run_js(js)
assert isinstance(r, dict), f"意外返回: {r!r}"
assert r.get("has"), "未产生 KaTeX HTML"
assert r.get("disp", 0) >= 1, "块公式未渲染为 katex-display"
assert r.get("inline", 0) >= 1, "行内公式未渲染"
assert not r.get("leaked"), "占位符泄漏"
assert r.get("vis"), "缺少 katex-html 可视层(KaTeX 未真正渲染)"
assert r.get("tex"), "tex 未正确传入(转义错误)"
print(f" [info] KaTeX {ver} | display={r.get('disp')} inline={r.get('inline')}")
check("KaTeX 公式渲染([...] 供应商格式 + 行内 $", katex_render)
print(f"\n===== {'ALL PASS' if all(results) else 'HAS FAILURES'}: {sum(results)}/{len(results)} =====")
try:
window.close()
except Exception:
pass
app.quit()
def finish(ok=None):
app.quit()
QTimer.singleShot(500, try_load)
QTimer.singleShot(90000, finish) # 总超时
app.exec()
sys.exit(0 if all(results) else 1)