"""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") 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_context(P0 修复验证:不再有 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)