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,201 @@
|
||||
"""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)
|
||||
Reference in New Issue
Block a user