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,199 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""中途切会话回归:agent 流式进行中切走 → 切回 → 时间线必须完整
|
||||
(用户报告:切走再切回,正文/思考那一段显示不出来)
|
||||
运行: python tests/smoke_midswitch.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)
|
||||
window = None
|
||||
results = []
|
||||
S = {} # 测试状态
|
||||
|
||||
|
||||
def check(name, ok, detail=""):
|
||||
print(f" {'PASS' if ok else 'FAIL'} {name}"
|
||||
+ (f" [{detail}]" if detail and not ok else ""))
|
||||
results.append(ok)
|
||||
|
||||
|
||||
CHECK_JS = r"""
|
||||
(function() {
|
||||
var mid = window.__msMid;
|
||||
var w = document.getElementById(mid);
|
||||
if (!w) return JSON.stringify({error: 'wrapper missing'});
|
||||
var tl = w.querySelector('.reply-content');
|
||||
var blocks = Array.prototype.map.call(tl.children, function(el) {
|
||||
return el.className.split(' ')[0];
|
||||
});
|
||||
var texts = Array.prototype.map.call(
|
||||
w.querySelectorAll('.md-segment'),
|
||||
function(s) { return s.textContent || ''; });
|
||||
var thinks = Array.prototype.map.call(
|
||||
w.querySelectorAll('.think-content'),
|
||||
function(s) { return s.textContent || ''; });
|
||||
return JSON.stringify({blocks: blocks, texts: texts, thinks: thinks,
|
||||
streaming: w.classList.contains('streaming')});
|
||||
})()
|
||||
"""
|
||||
|
||||
|
||||
def fire(session, fn, *args):
|
||||
"""模拟 worker 信号到达(UI 线程直接调 handler)"""
|
||||
getattr(window, fn)(*([session] + list(args)))
|
||||
|
||||
|
||||
def step1():
|
||||
global window
|
||||
window = MainWindow()
|
||||
window.show()
|
||||
QTimer.singleShot(3000, step2)
|
||||
|
||||
|
||||
def step2():
|
||||
db = window.db
|
||||
sessA = db.create_session("切走测试A")
|
||||
sessB = db.create_session("切走测试B")
|
||||
A, B = sessA["id"], sessB["id"]
|
||||
db.add_message(B, "user", "B 的问题", None)
|
||||
|
||||
# ---- 会话 A 开始流式(手动模拟事件到达)----
|
||||
user_row = db.add_message(A, "user", "A 的问题", None)
|
||||
mid = "msg-midswitch"
|
||||
window.current_session_id = A
|
||||
window.chat_bridge.create_message(mid, "assistant", "", "SW")
|
||||
window._active_streams[A] = {
|
||||
"msg_id": mid, "content": "", "reasoning": "",
|
||||
"timeline": [], "tl_kind": None,
|
||||
"parent_id": user_row["id"], "branch_info": None, "worker": None,
|
||||
"previous_leaf_id": user_row["id"],
|
||||
}
|
||||
S.update(A=A, B=B, mid=mid)
|
||||
window.browser.page().runJavaScript(f"window.__msMid = '{mid}';")
|
||||
|
||||
# 阶段 1(A 前台):think1 + text1 + 工具
|
||||
fire(A, "on_reasoning_received", "思考第一")
|
||||
fire(A, "on_reasoning_received", "段内容")
|
||||
fire(A, "on_chunk_received", "工具前正文")
|
||||
fire(A, "_on_tool_started", "call-ms", "bash", '{"command":"echo ms"}')
|
||||
|
||||
QTimer.singleShot(800, switch_away)
|
||||
|
||||
|
||||
def switch_away():
|
||||
"""切到 B:A 的流继续在后台跑"""
|
||||
print("[test] 切换到 B ...", flush=True)
|
||||
window.load_messages_to_web(S["B"])
|
||||
# 阶段 2(A 后台):think2 + text2(只应累积,不进 JS)
|
||||
QTimer.singleShot(600, lambda: (
|
||||
fire(S["A"], "on_reasoning_received", "思考第二"),
|
||||
fire(S["A"], "on_chunk_received", "工具后正文"),
|
||||
print("[test] 后台 token 已累积", flush=True),
|
||||
QTimer.singleShot(600, switch_back)
|
||||
))
|
||||
|
||||
|
||||
def switch_back():
|
||||
"""切回 A:应重建 wrapper + 恢复时间线 + 后续无缝续流"""
|
||||
print("[test] 切回 A ...", flush=True)
|
||||
window.load_messages_to_web(S["A"])
|
||||
# ★ 切回后立即检查:恢复的正文/思考必须已同步上屏(不等 rAF/timer)
|
||||
def check_right_away():
|
||||
window.browser.page().runJavaScript(CHECK_JS, lambda res: (
|
||||
print("[切回后立即]", str(res), flush=True),
|
||||
check("切回后立即: 后台正文已同步上屏",
|
||||
"工具后正文" in json.loads(str(res)).get("texts", [""] + [json.loads(str(res))["texts"][0] if json.loads(str(res)).get("texts") else ""])[0] or "工具后正文" in "".join(json.loads(str(res)).get("texts", []))),
|
||||
QTimer.singleShot(300, phase3)
|
||||
))
|
||||
def phase3():
|
||||
fire(S["A"], "on_chunk_received", " 续流正文")
|
||||
QTimer.singleShot(600, finish_it)
|
||||
QTimer.singleShot(400, check_right_away)
|
||||
|
||||
|
||||
def finish_it():
|
||||
window.on_reply_finished(S["A"])
|
||||
QTimer.singleShot(400, step_check)
|
||||
|
||||
|
||||
DIAG_JS = r"""
|
||||
(function() {
|
||||
var wrappers = Array.prototype.map.call(
|
||||
document.querySelectorAll('.message-wrapper'),
|
||||
function(w) { return w.id + ':' + w.className.split(' ')[1]; });
|
||||
return JSON.stringify({wrappers: wrappers,
|
||||
bufKeys: Object.keys(window.messageBuffer || {})});
|
||||
})()
|
||||
"""
|
||||
|
||||
def step_check():
|
||||
def diag(res):
|
||||
print("[diag]", str(res), flush=True)
|
||||
window.browser.page().runJavaScript(CHECK_JS, on_check)
|
||||
window.browser.page().runJavaScript(DIAG_JS, diag)
|
||||
|
||||
|
||||
def on_check(res):
|
||||
d = json.loads(str(res))
|
||||
if "error" in d:
|
||||
check("切回后 wrapper 存在", False, d["error"])
|
||||
done()
|
||||
return
|
||||
blocks = d["blocks"]
|
||||
texts = "".join(d["texts"])
|
||||
thinks = "".join(d["thinks"])
|
||||
print(f" blocks = {blocks}")
|
||||
print(f" texts = {d['texts']}")
|
||||
print(f" thinks = {d['thinks']}")
|
||||
check("切回后 wrapper 存在", True)
|
||||
check("块顺序与事件顺序一致: think→text→chip→think→text",
|
||||
[b for b in blocks if b in
|
||||
("think-block", "tool-chip", "md-segment")] ==
|
||||
["think-block", "md-segment", "tool-chip", "think-block", "md-segment"],
|
||||
str(blocks))
|
||||
check("正文完整(切走前+后台+切回后)",
|
||||
"工具前正文" in texts and "工具后正文" in texts
|
||||
and "续流正文" in texts, str(d["texts"]))
|
||||
check("思考完整(切走前+后台)",
|
||||
"思考第一段内容" in thinks and "思考第二" in thinks,
|
||||
str(d["thinks"]))
|
||||
check("streaming 已收尾", not d["streaming"])
|
||||
# 入库验证
|
||||
row = window.db.get_message_chain(S["A"])
|
||||
asst = [m for m in row if m["role"] == "assistant"]
|
||||
ok_db = False
|
||||
if asst:
|
||||
tl = asst[-1].get("timeline")
|
||||
if tl:
|
||||
tl_list = json.loads(tl)
|
||||
joined = "".join(e.get("text", "") for e in tl_list if e["t"] == "text")
|
||||
ok_db = ("工具前正文" in joined and "工具后正文" in joined
|
||||
and "续流正文" in joined)
|
||||
check("DB 时间线完整(三段正文都在)", ok_db)
|
||||
done()
|
||||
|
||||
|
||||
def done():
|
||||
try:
|
||||
window.db.delete_session(S["A"])
|
||||
window.db.delete_session(S["B"])
|
||||
except Exception:
|
||||
pass
|
||||
print(f"\n===== {'ALL PASS' if all(results) else 'HAS FAILURES'}: "
|
||||
f"{sum(results)}/{len(results)} =====")
|
||||
app.quit()
|
||||
|
||||
|
||||
QTimer.singleShot(500, step1)
|
||||
app.exec()
|
||||
Reference in New Issue
Block a user