Files
Haocode/tests/smoke_copy_session.py
T
sorrow404null a7412824e0 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.
2026-09-17 16:40:01 +08:00

157 lines
7.0 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.
"""会话复制 · UI 入口端到端(offscreen
链路:弹窗「📋 复制」按钮 → action_triggered("copy") → on_session_action →
DBManager.copy_session → 侧边栏重建 + 自动切到副本
运行: QT_QPA_PLATFORM=offscreen python tests/smoke_copy_session.py
"""
import os
import sys
import tempfile
import shutil
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
os.environ.setdefault("PYTHONIOENCODING", "utf-8")
# 铁律:测试不得污染真实 data/chat_history.db → 默认 DB 路径重定向到临时文件
import core.db_manager as _dbm # noqa: E402
_DB_TMP = os.path.join(tempfile.gettempdir(), f"haocode_test_copy_ui_{os.getpid()}.db")
if os.path.exists(_DB_TMP):
os.remove(_DB_TMP)
_dbm._DEFAULT_DB = _DB_TMP
_FS_TMP = tempfile.mkdtemp(prefix="hocode_copy_ui_files_") # 临时附件根
from PyQt6.QtWidgets import QApplication, QPushButton # noqa: E402
from PyQt6 import QtCore # noqa: E402
from ui.views.main_window import MainWindow, SessionContextPopup # noqa: E402
app = QApplication(sys.argv)
ok = True
def check(name, cond, extra=""):
global ok
print((" PASS " if cond else " FAIL ") + name + ("" if cond else f" {extra}"), flush=True)
if not cond:
ok = False
def chain_sig(sid):
return [(m["role"], m["content"]) for m in window.db.get_message_chain(sid)]
window = MainWindow()
window.db.files_root = _FS_TMP
# ======================================================================
# 1) 准备一个带内容的源会话(直接走 DB 层,不触发真实 API 请求)
# ======================================================================
window.on_new_chat_clicked()
src = window.current_session_id
for role, text in [("user", "帮我看看这个文件"), ("assistant", "好的,我先读一下")]:
window.db.add_message(src, role, text, window.db.get_session_leaf(src))
window.db.mark_session_has_messages(src)
window.rebuild_sidebar()
src_title = [s for s in window.db.get_all_sessions() if s["id"] == src][0]["title"]
check("源会话已就绪(2 条消息)", len(window.db.get_message_chain(src)) == 3,
str(len(window.db.get_message_chain(src))))
# 图片附件(验证 UI 链路里附件也被深拷贝)
os.makedirs(os.path.join(_FS_TMP, "data", "attachments"), exist_ok=True)
png = os.path.join(_FS_TMP, "data", "attachments", "ui_src.png")
with open(png, "wb") as f:
f.write(b"\x89PNG\r\n\x1a\nUI-IMAGE-BYTES")
meta = ('{"user_text":"看图","attachments":[{"type":"image","size_kb":0.1,'
'"local_path":"data/attachments/ui_src.png"}]}')
window.db.add_message(src, "user", "以及这张图", window.db.get_session_leaf(src),
attachment_metadata=meta)
# ======================================================================
# 2) 弹窗里存在「复制」按钮,且点击后发出 ("copy", sid)
# ======================================================================
popup = SessionContextPopup(src, False, window)
btns = popup.findChildren(QPushButton)
texts = [b.text() for b in btns]
check("弹窗含「📋 复制」按钮", any("复制" in t for t in texts), str(texts))
check("按钮顺序 编辑/星标/复制/删除",
len(texts) == 4 and "编辑" in texts[0] and "星标" in texts[1]
and "复制" in texts[2] and "删除" in texts[3], str(texts))
got = []
popup.action_triggered.connect(lambda a, s: got.append((a, s)))
copy_btn = [b for b in btns if "复制" in b.text()][0]
copy_btn.click()
check("点击复制按钮发出 copy 动作", got == [("copy", src)], str(got))
# ======================================================================
# 3) on_session_action("copy") → 真实克隆 + 侧边栏刷新 + 切到副本
# ======================================================================
n_before = len(window.db.get_all_sessions())
window.on_session_action("copy", src)
sessions = window.db.get_all_sessions()
check("会话数 +1", len(sessions) == n_before + 1, f"{n_before}{len(sessions)}")
rows = [s for s in sessions if s["title"] == src_title + " (副本)"]
check("副本标题 = 源标题 + ' (副本)'", len(rows) == 1, str([s["title"] for s in sessions]))
copy_id = rows[0]["id"] if rows else None
if copy_id:
check("副本不是源(ID 不同)", copy_id != src)
check("消息链完全一致", chain_sig(src) == chain_sig(copy_id))
check("已自动切到副本", window.current_session_id == copy_id, str(window.current_session_id))
check("侧边栏含副本项",
any(window.history_list.item(i).data(QtCore.Qt.ItemDataRole.UserRole) == copy_id
for i in range(window.history_list.count())))
check("副本排在列表顶部(sort_order 最小)",
sessions[0]["id"] == copy_id, sessions[0]["title"])
check("副本不带星标", rows[0]["is_starred"] == 0)
check("副本 mode 跟随源(未锁定为 None",
rows[0]["mode"] == window.db.get_session_mode(src))
# 附件深拷贝:新旧文件同时存在,且副本 metadata 指向新文件
import json as _json
c_rows = [m for m in window.db.get_message_chain(copy_id) if m.get("attachment_metadata")]
c_meta = _json.loads(c_rows[-1]["attachment_metadata"])
new_rel = c_meta["attachments"][0]["local_path"]
check("副本附件已改名", new_rel != "data/attachments/ui_src.png", new_rel)
check("新旧图片文件同时存在",
os.path.isfile(png) and os.path.isfile(os.path.join(_FS_TMP, new_rel)))
check("副本图片内容一致",
open(os.path.join(_FS_TMP, new_rel), "rb").read()
== open(png, "rb").read())
# 删副本后源完好(UI 链路下的删除隔离)
window.db.delete_session(copy_id)
check("删副本后源消息链不变", chain_sig(src) == [
("system", "你是一个优秀的助手!"), ("user", "帮我看看这个文件"),
("assistant", "好的,我先读一下"), ("user", "以及这张图")])
check("删副本后源图片仍在", os.path.isfile(png))
# ======================================================================
# 4) 生成中拒绝复制
# ======================================================================
n_before2 = len(window.db.get_all_sessions())
window._active_streams[src] = {"worker": None}
window.on_session_action("copy", src)
check("会话生成中 → 拒绝复制(无新会话)",
len(window.db.get_all_sessions()) == n_before2)
window._active_streams.pop(src, None)
# 5) 源不存在 → 静默失败,不崩
try:
window.on_session_action("copy", "sess_not_exist_zzz")
check("源不存在时不抛异常", True)
except Exception as e:
check("源不存在时不抛异常", False, str(e))
print("\n===== " + ("ALL PASS" if ok else "HAS FAILURES") + " =====", flush=True)
shutil.rmtree(_FS_TMP, ignore_errors=True)
if os.path.exists(_DB_TMP):
os.remove(_DB_TMP)
sys.exit(0 if ok else 1)