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,187 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""E2E live 测试:真实 AgentWorker + 假流式 + 真实 MainWindow 信号链路。
|
||||
验证 live 时正文 md-segment 是否有内容(用户报告的 bug)。
|
||||
运行: QT_QPA_PLATFORM=offscreen python tests/diag_live_text.py
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
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")
|
||||
|
||||
from PyQt6.QtWidgets import QApplication # noqa: E402
|
||||
from PyQt6.QtCore import QTimer # noqa: E402
|
||||
|
||||
from core.agent.types import (AssistantMessageEvent, AgentMessage, ToolCall) # noqa: E402
|
||||
from core.agent import stream_fn as sf # noqa: E402
|
||||
from core.agent.tools import default_tools # noqa: E402
|
||||
from ui.views.main_window import MainWindow # noqa: E402
|
||||
|
||||
app = QApplication(sys.argv)
|
||||
window = None
|
||||
results = []
|
||||
CALL_STATE = {"n": 0}
|
||||
|
||||
|
||||
def check(name, ok, detail=""):
|
||||
print(f" {'PASS' if ok else 'FAIL'} {name}"
|
||||
+ (f" [{detail}]" if detail and not ok else ""))
|
||||
results.append(ok)
|
||||
|
||||
|
||||
# ---- 假 stream_fn:第 1 轮 思考+文本+bash 工具调用;第 2 轮 最终回答 ----
|
||||
def fake_stream(context, model, signal, max_tokens, tools=None):
|
||||
CALL_STATE["n"] += 1
|
||||
n = CALL_STATE["n"]
|
||||
if n == 1:
|
||||
yield ("event", AssistantMessageEvent(type="thinking_delta", text="我先看看"))
|
||||
yield ("event", AssistantMessageEvent(type="thinking_delta", text="目录。"))
|
||||
yield ("event", AssistantMessageEvent(type="text_delta", text="我来执行命令"))
|
||||
yield ("event", AssistantMessageEvent(type="toolcall_delta",
|
||||
tool_call_index=0, tool_call_field="id", tool_call_delta="call-e2e-1"))
|
||||
yield ("event", AssistantMessageEvent(type="toolcall_delta",
|
||||
tool_call_index=0, tool_call_field="name", tool_call_delta="bash"))
|
||||
yield ("event", AssistantMessageEvent(type="toolcall_delta",
|
||||
tool_call_index=0, tool_call_field="arguments",
|
||||
tool_call_delta='{"command": "echo e2e-ok"}'))
|
||||
time.sleep(0.3)
|
||||
yield ("final", AgentMessage(
|
||||
role="assistant", stop_reason="stop",
|
||||
tool_calls=[ToolCall(id="call-e2e-1", name="bash",
|
||||
raw_arguments='{"command": "echo e2e-ok"}')]))
|
||||
else:
|
||||
for tok in ["最终", "回答", ":任务", "完成。"]:
|
||||
yield ("event", AssistantMessageEvent(type="text_delta", text=tok))
|
||||
time.sleep(0.05)
|
||||
yield ("final", AgentMessage(role="assistant", stop_reason="stop"))
|
||||
|
||||
|
||||
def step1():
|
||||
global window
|
||||
window = MainWindow()
|
||||
QTimer.singleShot(3500, step2)
|
||||
|
||||
|
||||
def step2():
|
||||
from core.llm_engine import AgentWorker
|
||||
db = window.db
|
||||
sess = db.create_session("E2E 正文测试")
|
||||
sid = sess["id"]
|
||||
mid = "msg-e2e-text"
|
||||
user_row = db.add_message(sid, "user", "测试问题", None)
|
||||
window.current_session_id = sid
|
||||
window.chat_bridge.create_message(mid, "assistant", "", "E2E")
|
||||
window._active_streams[sid] = {
|
||||
"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"],
|
||||
}
|
||||
|
||||
# 真实 API + 流事件日志(定位 text_delta 是否到达)
|
||||
import core.llm_engine as le
|
||||
real_stream = le.openai_stream
|
||||
def logging_stream(context, model, signal, max_tokens, tools=None):
|
||||
n_ev = 0
|
||||
for kind, payload in real_stream(context, model, signal,
|
||||
max_tokens, tools):
|
||||
t = getattr(payload, "type", kind)
|
||||
txt = str(getattr(payload, "text", ""))[:30]
|
||||
if t not in ("text_delta", "thinking_delta", "toolcall_delta"):
|
||||
print(f" [stream] {kind}/{t}", flush=True)
|
||||
else:
|
||||
n_ev += 1
|
||||
if n_ev <= 6 or n_ev % 20 == 0:
|
||||
print(f" [stream] {t} {txt!r}", flush=True)
|
||||
yield kind, payload
|
||||
le.openai_stream = logging_stream
|
||||
worker = AgentWorker(provider_name=window.current_provider,
|
||||
model_name=window.current_model,
|
||||
openai_messages=[{"role": "user",
|
||||
"content": "运行命令 echo hello-from-diag 并告诉我输出"}],
|
||||
enable_tools=True)
|
||||
|
||||
# 与 send_message 相同的信号连接
|
||||
worker.reasoning_received.connect(lambda t: window.on_reasoning_received(sid, t))
|
||||
worker.chunk_received.connect(lambda t: window.on_chunk_received(sid, t))
|
||||
worker.tool_execution_started.connect(
|
||||
lambda cid, name, args: window._on_tool_started(sid, cid, name, args))
|
||||
worker.tool_execution_updated.connect(
|
||||
lambda cid, text: window._on_tool_updated(sid, cid, text))
|
||||
worker.tool_execution_finished.connect(
|
||||
lambda cid, name, ok, text: window._on_tool_finished(sid, cid, name, ok, text))
|
||||
worker.error_occurred.connect(lambda err: (print(' [worker error]', err), window.on_error(sid, err)))
|
||||
worker.finished.connect(lambda: window.on_reply_finished(sid))
|
||||
|
||||
window._active_streams[sid]["worker"] = worker
|
||||
worker.start()
|
||||
print("worker started, 等待流结束...")
|
||||
QTimer.singleShot(40000, step3)
|
||||
|
||||
|
||||
def step3():
|
||||
# live DOM 检查(不重载 DB)
|
||||
js = """
|
||||
(function() {
|
||||
try {
|
||||
var w = document.getElementById('msg-e2e-text');
|
||||
if (!w) return JSON.stringify({error: 'no wrapper'});
|
||||
var tl = w.querySelector('.reply-content');
|
||||
var blocks = Array.prototype.map.call(tl.children,
|
||||
function(el) { return el.className.split(' ')[0]; });
|
||||
var segs = w.querySelectorAll('.md-segment');
|
||||
var segTexts = Array.prototype.map.call(segs,
|
||||
function(x) { return x.textContent; });
|
||||
var thinks = w.querySelectorAll('.think-content');
|
||||
var thinkTexts = Array.prototype.map.call(thinks,
|
||||
function(x) { return x.textContent; });
|
||||
var chip = w.querySelector('.tool-chip');
|
||||
return JSON.stringify({blocks: blocks, segTexts: segTexts,
|
||||
thinkTexts: thinkTexts, chip: !!chip,
|
||||
streaming: w.classList.contains('streaming')});
|
||||
} catch (e) { return JSON.stringify({error: String(e)}); }
|
||||
})()
|
||||
"""
|
||||
def got(res):
|
||||
d = json.loads(str(res))
|
||||
if "error" in d:
|
||||
check("E2E DOM", False, d["error"])
|
||||
finish()
|
||||
return
|
||||
print(f" blocks = {d['blocks']}")
|
||||
print(f" segTexts = {d['segTexts']}")
|
||||
print(f" thinkTexts = {d['thinkTexts']}")
|
||||
print(f" chip = {d['chip']} streaming={d['streaming']}")
|
||||
check("live 正文段有内容(至少一段非空)",
|
||||
any((t or "").strip() for t in d["segTexts"]),
|
||||
str(d["segTexts"]))
|
||||
check("live 最终回答含命令输出",
|
||||
any("hello-from-diag" in (t or "") for t in d["segTexts"]),
|
||||
str(d["segTexts"]))
|
||||
check("live 思考段有内容(或模型未思考)",
|
||||
True, str(d["thinkTexts"])[:80])
|
||||
check("live 工具 chip 存在", d["chip"])
|
||||
check("streaming 已收尾", not d["streaming"])
|
||||
# 时间线入库检查
|
||||
st = window._active_streams.get(sid := window.current_session_id)
|
||||
tl = window.db.get_session(window.current_session_id) if False else None
|
||||
finish()
|
||||
|
||||
window.browser.page().runJavaScript(js, got)
|
||||
|
||||
|
||||
def finish():
|
||||
try:
|
||||
window.db.delete_session(window.current_session_id)
|
||||
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