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,297 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""时间线持久化冒烟测试(真实 DB + 真实 WebEngine 页面):
|
||||
1. 流式事件 → stream_state 时间线累积(思考/文本/工具 按序)
|
||||
2. 入库(timeline 列)→ 切会话重载 → DOM 时间线还原(工具气泡不丢)
|
||||
3. build_api_context 从时间线重建完整 API 链(assistant+tool_calls+tool)
|
||||
4. 切回进行中的会话:restoreStreamingTimeline 续流
|
||||
运行: QT_QPA_PLATFORM=offscreen python tests/smoke_persist.py
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
|
||||
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
|
||||
|
||||
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 = []
|
||||
window = None
|
||||
test = {"sid": None, "mid": None, "done": False}
|
||||
|
||||
|
||||
def check(name, ok, detail=""):
|
||||
print(f" {'PASS' if ok else 'FAIL'} {name}"
|
||||
+ (f" [{detail}]" if detail and not ok else ""))
|
||||
results.append(ok)
|
||||
|
||||
|
||||
def finish():
|
||||
if test["done"]:
|
||||
return
|
||||
test["done"] = True
|
||||
# 清理测试会话
|
||||
try:
|
||||
if test["sid"] and window:
|
||||
window.db.delete_session(test["sid"])
|
||||
except Exception:
|
||||
pass
|
||||
print(f"\n===== {'ALL PASS' if all(results) else 'HAS FAILURES'}: "
|
||||
f"{sum(results)}/{len(results)} =====")
|
||||
app.quit()
|
||||
|
||||
|
||||
# JS: 校验重载后的时间线 DOM
|
||||
JS_VERIFY_RELOAD = r"""
|
||||
(function() {
|
||||
var out = {};
|
||||
try {
|
||||
var mid = '__MID__';
|
||||
var wrapper = document.getElementById(mid);
|
||||
if (!wrapper) return JSON.stringify({error: 'wrapper missing'});
|
||||
var tl = wrapper.querySelector('.reply-content');
|
||||
out.blocks = Array.prototype.map.call(tl.children, function(el) {
|
||||
return el.className.split(' ')[0];
|
||||
});
|
||||
var chip = wrapper.querySelector('.tool-chip');
|
||||
var st = chip ? chip.querySelector('.tool-chip-status') : null;
|
||||
out.chipStatus = st ? st.textContent : null;
|
||||
out.chipOk = st ? st.classList.contains('ok') : false;
|
||||
out.chipCallId = chip ? chip.getAttribute('data-call-id') : null;
|
||||
var segs = wrapper.querySelectorAll('.md-segment');
|
||||
out.lastSegText = segs.length ? segs[segs.length - 1].textContent : '';
|
||||
out.thinkCount = wrapper.querySelectorAll('.think-block').length;
|
||||
out.thinkChevron = !!wrapper.querySelector('.think-block .chev');
|
||||
} catch (e) {
|
||||
out.error = String(e);
|
||||
}
|
||||
return JSON.stringify(out);
|
||||
})()
|
||||
"""
|
||||
|
||||
# JS: 续流测试
|
||||
JS_RESUME_A = r"""
|
||||
(function() {
|
||||
try {
|
||||
var mid = '__MID__';
|
||||
createMessage(mid, 'assistant', '', 'Resume');
|
||||
restoreStreamingTimeline(mid, '__TLJSON__');
|
||||
// 续流:接着最后一段文本写;思考新开一段
|
||||
appendToken(mid, '(续流文本)');
|
||||
appendReasoning(mid, '续思考内容');
|
||||
return 'resumeA-done';
|
||||
} catch (e) {
|
||||
return 'resumeA-err:' + String(e);
|
||||
}
|
||||
})()
|
||||
"""
|
||||
|
||||
JS_RESUME_B = r"""
|
||||
(function() {
|
||||
try {
|
||||
var mid = '__MID__';
|
||||
finishMessage(mid);
|
||||
var wrapper = document.getElementById(mid);
|
||||
var tl = wrapper.querySelector('.reply-content');
|
||||
var out = {};
|
||||
out.blocks = Array.prototype.map.call(tl.children, function(el) {
|
||||
return el.className.split(' ')[0];
|
||||
});
|
||||
var segs = wrapper.querySelectorAll('.md-segment');
|
||||
out.lastSegText = segs.length ? segs[segs.length - 1].textContent : '';
|
||||
out.thinkCount = wrapper.querySelectorAll('.think-block').length;
|
||||
out.chipCount = wrapper.querySelectorAll('.tool-chip').length;
|
||||
wrapper.parentNode.removeChild(wrapper);
|
||||
return JSON.stringify(out);
|
||||
} catch (e) {
|
||||
return 'resumeB-err:' + String(e);
|
||||
}
|
||||
})()
|
||||
"""
|
||||
|
||||
|
||||
|
||||
def run_checks():
|
||||
db = window.db
|
||||
|
||||
# ---------- 建测试会话 + 模拟流式事件累积 ----------
|
||||
sess = db.create_session("持久化测试会话")
|
||||
sid = sess["id"]
|
||||
test["sid"] = sid
|
||||
mid = "msg-persist-test"
|
||||
test["mid"] = mid
|
||||
|
||||
user_row = db.add_message(sid, "user", "测试问题", None)
|
||||
user_id = user_row["id"]
|
||||
|
||||
window.current_session_id = sid
|
||||
window.chat_bridge.create_message(mid, "assistant", "", "PersistTest")
|
||||
|
||||
st = {
|
||||
"msg_id": mid, "content": "", "reasoning": "",
|
||||
"timeline": [], "tl_kind": None,
|
||||
"parent_id": user_id, "branch_info": None, "worker": None,
|
||||
"previous_leaf_id": user_id,
|
||||
}
|
||||
window._active_streams[sid] = st
|
||||
|
||||
# 事件序列:思考 → 文本 → 工具 → 思考 → 文本
|
||||
window.on_reasoning_received(sid, "先看一下")
|
||||
window.on_reasoning_received(sid, "目录结构。")
|
||||
window.on_chunk_received(sid, "我来执行")
|
||||
window._on_tool_started(sid, "call-p1", "bash", '{"command": "echo hi"}')
|
||||
window._on_tool_updated(sid, "call-p1", "hi\n")
|
||||
window._on_tool_finished(sid, "call-p1", "bash", True,
|
||||
"$ echo hi\nhi\n[exit 0]")
|
||||
window.on_reasoning_received(sid, "输出正常。")
|
||||
window.on_chunk_received(sid, "任务完成。")
|
||||
|
||||
tl = st["timeline"]
|
||||
check("时间线累积: 5 段", len(tl) == 5, str(tl))
|
||||
check("时间线类型序 think/text/tool/think/text",
|
||||
[e["t"] for e in tl] == ["think", "text", "tool", "think", "text"],
|
||||
str([e["t"] for e in tl]))
|
||||
tool_e = tl[2] if len(tl) > 2 else {}
|
||||
check("工具条目定格 ok+result",
|
||||
tool_e.get("ok") is True and "[exit 0]" in tool_e.get("result", ""),
|
||||
str(tool_e))
|
||||
check("聚合 content/reasoning 正确",
|
||||
st["content"] == "我来执行任务完成。"
|
||||
and st["reasoning"] == "先看一下目录结构。输出正常。",
|
||||
f"{st['content']!r} / {st['reasoning']!r}")
|
||||
|
||||
tl_json = json.dumps(tl, ensure_ascii=False)
|
||||
|
||||
# ---------- 入库 ----------
|
||||
row = db.add_message(sid, "assistant", st["content"], user_id,
|
||||
reasoning=st["reasoning"], msg_id=mid,
|
||||
timeline=tl_json)
|
||||
check("DB timeline 列写入", row.get("timeline") == tl_json)
|
||||
|
||||
# ---------- 切走再切回(重载) ----------
|
||||
del window._active_streams[sid] # 模拟流已结束(先于重载,避免 live-restore 重复渲染)
|
||||
window.load_messages_to_web(sid, show_loading=False)
|
||||
|
||||
def verify_reload(res):
|
||||
data = json.loads(str(res))
|
||||
if "error" in data:
|
||||
check("重载 DOM 校验", False, data["error"])
|
||||
finish()
|
||||
return
|
||||
expected = ["think-block", "md-segment", "tool-chip",
|
||||
"think-block", "md-segment"]
|
||||
check("重载: 时间线块序还原", data["blocks"] == expected,
|
||||
str(data["blocks"]))
|
||||
check("重载: 工具气泡保留且成功",
|
||||
data["chipOk"] and data["chipStatus"] == "✓ 完成"
|
||||
and data["chipCallId"] == "call-p1",
|
||||
f"{data['chipStatus']} {data['chipCallId']}")
|
||||
check("重载: 思考块 x2 + 末段文本",
|
||||
data["thinkCount"] == 2 and "任务完成。" in data["lastSegText"],
|
||||
f"think={data['thinkCount']} seg={data['lastSegText']!r}")
|
||||
check("重载: SVG 箭头", data["thinkChevron"])
|
||||
|
||||
# ---------- build_api_context 重建 ----------
|
||||
api = window.build_api_context(sid)
|
||||
roles = [m["role"] for m in api]
|
||||
check("API 链: user→assistant→tool→assistant",
|
||||
roles == ["user", "assistant", "tool", "assistant"],
|
||||
str(roles))
|
||||
asst1 = api[1] if len(api) > 1 else {}
|
||||
check("API: 首条 assistant 带 tool_calls",
|
||||
asst1.get("content") == "我来执行"
|
||||
and len(asst1.get("tool_calls", [])) == 1
|
||||
and asst1["tool_calls"][0]["function"]["name"] == "bash",
|
||||
str(asst1)[:200])
|
||||
check("API: tool 消息 tool_call_id 对齐",
|
||||
len(api) > 2 and api[2].get("tool_call_id") == "call-p1"
|
||||
and "[exit 0]" in api[2].get("content", ""),
|
||||
str(api[2] if len(api) > 2 else {})[:200])
|
||||
check("API: 末条 assistant = 最终回答",
|
||||
len(api) > 3 and api[3].get("content") == "任务完成。")
|
||||
check("API: 无 reasoning 字段",
|
||||
all("reasoning" not in m for m in api))
|
||||
|
||||
# ---------- 切回进行中会话: 续流 ----------
|
||||
tljs = json.dumps(tl_json)
|
||||
js_a = (JS_RESUME_A
|
||||
.replace("'__MID__'", "'msg-resume-test'")
|
||||
.replace("restoreStreamingTimeline(mid, '__TLJSON__')",
|
||||
f"restoreStreamingTimeline(mid, {tljs})"))
|
||||
js_b = JS_RESUME_B.replace("'__MID__'", "'msg-resume-test'")
|
||||
|
||||
def verify_resume_data(d2):
|
||||
if "error" in d2:
|
||||
check("续流 DOM 校验", False, d2["error"])
|
||||
finish()
|
||||
return
|
||||
expected2 = ["think-block", "md-segment", "tool-chip",
|
||||
"think-block", "md-segment", "think-block"]
|
||||
check("续流: 块序正确", d2["blocks"] == expected2,
|
||||
str(d2["blocks"]))
|
||||
check("续流: 文本并入末段",
|
||||
"任务完成。(续流文本)" in d2["lastSegText"],
|
||||
d2["lastSegText"])
|
||||
check("续流: 思考 x3 + 工具 x1",
|
||||
d2["thinkCount"] == 3 and d2["chipCount"] == 1,
|
||||
f"think={d2['thinkCount']} chip={d2['chipCount']}")
|
||||
finish()
|
||||
|
||||
def run_b():
|
||||
def got_b(v):
|
||||
vstr = "" if v is None else str(v)
|
||||
print(f" [resume B] {vstr[:120]}")
|
||||
if vstr.startswith("resumeB-err"):
|
||||
check("续流 finishMessage", False, vstr)
|
||||
finish()
|
||||
return
|
||||
try:
|
||||
d2 = json.loads(vstr)
|
||||
except Exception:
|
||||
check("续流 DOM 校验", False, repr(vstr)[:120])
|
||||
finish()
|
||||
return
|
||||
verify_resume_data(d2)
|
||||
window.browser.page().runJavaScript(js_b, got_b)
|
||||
|
||||
def run_a():
|
||||
def got_a(v):
|
||||
vstr = "" if v is None else str(v)
|
||||
print(f" [resume A] {vstr[:120]}")
|
||||
if vstr.startswith("resumeA-err"):
|
||||
check("续流 restore", False, vstr)
|
||||
finish()
|
||||
return
|
||||
QTimer.singleShot(300, run_b)
|
||||
window.browser.page().runJavaScript(js_a, got_a)
|
||||
|
||||
QTimer.singleShot(300, run_a)
|
||||
|
||||
|
||||
|
||||
window.browser.page().runJavaScript(
|
||||
JS_VERIFY_RELOAD.replace("'__MID__'", f"'{mid}'"), verify_reload)
|
||||
|
||||
|
||||
def main():
|
||||
global window
|
||||
try:
|
||||
window = MainWindow()
|
||||
except Exception:
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
app.quit()
|
||||
return
|
||||
QTimer.singleShot(6000, run_checks)
|
||||
QTimer.singleShot(45000, lambda: (check("超时", False, "45s 未完成"),
|
||||
finish()) if not test["done"] else None)
|
||||
app.exec()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user