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,280 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""四项 UI 增强回归测试(真实 WebEngine 页面):
|
||||
1. rAF 卡死模拟(AMD 核显/合成器不出帧)→ 40ms 兜底仍实时渲染流式正文
|
||||
2. 深度思考默认收起 + .streaming-think 蓝色动画类;收尾后标签还原
|
||||
3. bash chip 耗时徽章([exit 0] (1.2s))+ 超时徽章(命令超时(>120s))
|
||||
4. 长结果:默认尾部 4000 字 + 展开按钮;点击展开全文/再点收起;
|
||||
收起状态下 finish 后结果仍可见(修复「结束后展开为空」)
|
||||
运行: python tests/smoke_live_guard.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 = []
|
||||
|
||||
|
||||
def check(name, ok, detail=""):
|
||||
print(f" {'PASS' if ok else 'FAIL'} {name}"
|
||||
+ (f" [{detail}]" if detail and not ok else ""))
|
||||
results.append(ok)
|
||||
|
||||
|
||||
# Phase 0: rAF + 所有页面定时器全灭(极端节流)→
|
||||
# 内容必须上屏:同步通道(token 内)+ Qt 看门狗(200ms,forceRenderNow)
|
||||
JS_PHASE_0A = r"""
|
||||
(function() {
|
||||
var out = {};
|
||||
window.__realRAF = window.requestAnimationFrame;
|
||||
window.__realCAF = window.cancelAnimationFrame;
|
||||
window.__realTO = window.setTimeout;
|
||||
window.__realCTO = window.clearTimeout;
|
||||
window.__realSI = window.setInterval;
|
||||
window.__realCSI = window.clearInterval;
|
||||
window.__fakeRafId = 0;
|
||||
window.__fakeTimerId = 1000;
|
||||
window.requestAnimationFrame = function() { return ++window.__fakeRafId; };
|
||||
window.cancelAnimationFrame = function(){};
|
||||
window.setTimeout = function() { return ++window.__fakeTimerId; };
|
||||
window.clearTimeout = function(){};
|
||||
window.setInterval = function() { return ++window.__fakeTimerId; };
|
||||
window.clearInterval = function(){};
|
||||
|
||||
var m0 = 'lg-sync-' + Date.now();
|
||||
window.__lgM0 = m0;
|
||||
createMessage(m0, 'assistant', '', 'LG');
|
||||
appendReasoning(m0, 'sync think ');
|
||||
appendToken(m0, 'sync text one ');
|
||||
appendToken(m0, 'two');
|
||||
var segs = document.querySelectorAll('#' + m0 + ' .md-segment');
|
||||
var t = '';
|
||||
for (var i = 0; i < segs.length; i++) t += segs[i].textContent || '';
|
||||
out.textAtBurst = t;
|
||||
return JSON.stringify(out);
|
||||
})()
|
||||
"""
|
||||
|
||||
JS_PHASE_0B = r"""
|
||||
(function() {
|
||||
var out = {};
|
||||
var m0 = window.__lgM0;
|
||||
var segs = document.querySelectorAll('#' + m0 + ' .md-segment');
|
||||
var t = '';
|
||||
for (var i = 0; i < segs.length; i++) t += segs[i].textContent || '';
|
||||
var tcs = document.querySelectorAll('#' + m0 + ' .think-content');
|
||||
var th = '';
|
||||
for (var i = 0; i < tcs.length; i++) th += tcs[i].textContent || '';
|
||||
out.text = t;
|
||||
out.think = th;
|
||||
// 恢复页面定时器
|
||||
window.requestAnimationFrame = window.__realRAF;
|
||||
window.cancelAnimationFrame = window.__realCAF;
|
||||
window.setTimeout = window.__realTO;
|
||||
window.clearTimeout = window.__realCTO;
|
||||
window.setInterval = window.__realSI;
|
||||
window.clearInterval = window.__realCSI;
|
||||
finishMessage(m0);
|
||||
return JSON.stringify(out);
|
||||
})()
|
||||
"""
|
||||
|
||||
# Phase A: 模拟 rAF 永不触发 + 流式 token;断言此刻 DOM 尚空(卡死生效)
|
||||
JS_PHASE_A = r"""
|
||||
(function() {
|
||||
var out = {};
|
||||
window.__realRAF = window.requestAnimationFrame;
|
||||
window.__realCAF = window.cancelAnimationFrame;
|
||||
// 模拟真实卡死:rAF 返回真实挂起 ID(truthy)但回调永不被调用
|
||||
window.__fakeRafId = 0;
|
||||
window.requestAnimationFrame = function(cb) { window.__rafCalls = (window.__rafCalls||0)+1; return ++window.__fakeRafId; };
|
||||
window.cancelAnimationFrame = function(){};
|
||||
|
||||
var mid = 'lg-' + Date.now();
|
||||
window.__lgMid = mid;
|
||||
createMessage(mid, 'assistant', '', 'LG');
|
||||
appendReasoning(mid, 'deep ');
|
||||
appendReasoning(mid, 'thinking...');
|
||||
appendToken(mid, 'live text one ');
|
||||
appendToken(mid, 'two ');
|
||||
appendToken(mid, 'three');
|
||||
|
||||
var segs = document.querySelectorAll('#' + mid + ' .md-segment');
|
||||
var domLen = 0;
|
||||
for (var i = 0; i < segs.length; i++)
|
||||
domLen += (segs[i].textContent || '').length;
|
||||
out.rafCalls = window.__rafCalls || 0;
|
||||
out.domLenBeforeTimers = domLen;
|
||||
|
||||
// 思考块状态(默认收起 + 蓝色动画类)
|
||||
var block = document.querySelector('#' + mid + ' .think-block');
|
||||
out.thinkOpen = block ? block.open : null;
|
||||
out.thinkStreaming = block ? block.classList.contains('streaming-think') : false;
|
||||
out.thinkLabel = block ? (block.querySelector('.think-label')||{}).textContent : null;
|
||||
return JSON.stringify(out);
|
||||
})()
|
||||
"""
|
||||
|
||||
# Phase B(timers 跑过之后): 兜底应已渲染正文 + 收尾 + 其余特性
|
||||
JS_PHASE_B = r"""
|
||||
(function() {
|
||||
var out = {};
|
||||
var mid = window.__lgMid;
|
||||
var segs = document.querySelectorAll('#' + mid + ' .md-segment');
|
||||
var domText = '';
|
||||
for (var i = 0; i < segs.length; i++) domText += segs[i].textContent || '';
|
||||
out.liveTextRendered = domText.indexOf('live text one two three') !== -1;
|
||||
|
||||
// 收尾:思考块标签还原 + 动画类移除
|
||||
finishMessage(mid);
|
||||
var block = document.querySelector('#' + mid + ' .think-block');
|
||||
out.thinkLabelAfter = block ? (block.querySelector('.think-label')||{}).textContent : null;
|
||||
out.thinkStreamingAfter = block ? block.classList.contains('streaming-think') : false;
|
||||
// 恢复 rAF
|
||||
window.requestAnimationFrame = window.__realRAF;
|
||||
window.cancelAnimationFrame = window.__realCAF;
|
||||
|
||||
// ---- 耗时 / 超时徽章 ----
|
||||
var mid2 = 'lg-timing-' + Date.now();
|
||||
createMessage(mid2, 'assistant', '', 'LG');
|
||||
toolExecutionStarted(mid2, 'call-ok', 'bash', JSON.stringify({command:'echo hi'}));
|
||||
toolExecutionFinished(mid2, 'call-ok', 'bash', true, '$ echo hi\nhi\n[exit 0] (1.2s)');
|
||||
toolExecutionStarted(mid2, 'call-tmo', 'bash', JSON.stringify({command:'sleep 999'}));
|
||||
toolExecutionFinished(mid2, 'call-tmo', 'bash', false, '命令超时(>120s)已终止');
|
||||
var w2 = document.getElementById(mid2);
|
||||
var chipOk = w2.querySelector('[data-call-id="call-ok"]');
|
||||
var chipTmo = w2.querySelector('[data-call-id="call-tmo"]');
|
||||
out.durBadge = chipOk ? (chipOk.querySelector('.tool-chip-time')||{}).textContent : null;
|
||||
out.tmoBadge = chipTmo ? (chipTmo.querySelector('.tool-chip-timeout')||{}).textContent : null;
|
||||
|
||||
// ---- 收起状态下 finish:结果必须已写入(展开可见) ----
|
||||
var livePre = chipOk ? chipOk.querySelector('.tool-chip-live') : null;
|
||||
out.resultWhileCollapsed = livePre ? (livePre.textContent || '').indexOf('hi') !== -1 : false;
|
||||
|
||||
// ---- 长结果:尾部预览 + 展开/收起 ----
|
||||
var longRes = 'L'.repeat(1000) + ' MIDDLE-MARKER ' + 'R'.repeat(5000); // 6015 字,标记在头部(预览窗外)
|
||||
toolExecutionStarted(mid2, 'call-long', 'read', JSON.stringify({path:'/big.txt'}));
|
||||
toolExecutionFinished(mid2, 'call-long', 'read', true, longRes);
|
||||
var chipLong = w2.querySelector('[data-call-id="call-long"]');
|
||||
var pre = chipLong ? chipLong.querySelector('.tool-chip-live') : null;
|
||||
var btn = chipLong ? chipLong.querySelector('.tool-expand-btn') : null;
|
||||
out.longHasBtn = !!btn;
|
||||
out.longPreviewLen = pre ? (pre.textContent || '').length : 0;
|
||||
out.longPreviewIsTail = pre ? (pre.textContent || '').lastIndexOf('R') > (pre.textContent||'').lastIndexOf('MIDDLE-MARKER') : false;
|
||||
out.longPreviewHasMarker = pre ? (pre.textContent || '').indexOf('MIDDLE-MARKER') !== -1 : true;
|
||||
if (btn) {
|
||||
btn.click();
|
||||
out.expandedHasMarker = pre ? (pre.textContent || '').indexOf('MIDDLE-MARKER') !== -1 : false;
|
||||
out.expandedLen = pre ? (pre.textContent || '').length : 0;
|
||||
out.fullClass = pre ? pre.classList.contains('tool-chip-full') : false;
|
||||
btn.click();
|
||||
out.collapseBackLen = pre ? (pre.textContent || '').length : 0;
|
||||
}
|
||||
return JSON.stringify(out);
|
||||
})()
|
||||
"""
|
||||
|
||||
|
||||
def step1():
|
||||
global window
|
||||
print("[step1] creating MainWindow...", flush=True)
|
||||
window = MainWindow()
|
||||
window.show()
|
||||
print("[step1] shown, scheduling step2", flush=True)
|
||||
QTimer.singleShot(3000, step2)
|
||||
|
||||
|
||||
def step2():
|
||||
print("[step2] phase 0: kill all page timers + burst tokens", flush=True)
|
||||
# 注册为当前会话的活跃流 → Qt 看门狗(200ms)会对它 forceRenderNow
|
||||
window.current_session_id = "fake-sync-sid"
|
||||
window._active_streams["fake-sync-sid"] = {
|
||||
"msg_id": None, "content": "pending", "reasoning": "",
|
||||
"timeline": [], "tl_kind": None, "parent_id": None,
|
||||
"branch_info": None, "worker": None, "previous_leaf_id": None,
|
||||
}
|
||||
window.browser.page().runJavaScript(JS_PHASE_0A, on_p0a)
|
||||
|
||||
|
||||
def on_p0a(res):
|
||||
a = json.loads(str(res))
|
||||
print(f" phase0 burst 瞬间: text={a['textAtBurst']!r}")
|
||||
# 把 JS 里生成的 mid 告诉看门狗(msg_id 需一致才会 forceRenderNow)
|
||||
# —— 简化:直接改 JS 端记录的 mid 对应的 buffer 即可,这里用 runJavaScript 对齐
|
||||
window.browser.page().runJavaScript(
|
||||
"window.__lgM0", lambda r: _align_and_wait(str(r)))
|
||||
|
||||
|
||||
def _align_and_wait(mid):
|
||||
mid = mid.strip().strip('"')
|
||||
window._active_streams["fake-sync-sid"]["msg_id"] = mid
|
||||
print("[phase0] 等待 Qt 看门狗渲染 (400ms) ...", flush=True)
|
||||
QTimer.singleShot(400, on_p0b)
|
||||
|
||||
|
||||
def on_p0b():
|
||||
window.browser.page().runJavaScript(JS_PHASE_0B, on_p0)
|
||||
|
||||
|
||||
def on_p0(res):
|
||||
p = json.loads(str(res))
|
||||
print(f" phase0 400ms 后: text={p['text']!r} think={p['think']!r}")
|
||||
check("定时器全灭: burst 内思考同步上屏", "sync think" in p["think"], str(p))
|
||||
check("定时器全灭: 正文经同步通道/看门狗上屏",
|
||||
"sync text one two" in p["text"], str(p))
|
||||
window._active_streams.pop("fake-sync-sid", None)
|
||||
window.browser.page().runJavaScript(JS_PHASE_A, on_a)
|
||||
|
||||
|
||||
def on_a(res):
|
||||
print("[on_a] got result:", str(res)[:120], flush=True)
|
||||
a = json.loads(str(res))
|
||||
print(f" phaseA: rafCalls={a['rafCalls']} domLenBefore={a['domLenBeforeTimers']}")
|
||||
# v7 起:首 token 插入前已同步渲染(:empty 布局失效修复的副作用——首字必现),
|
||||
# 故此刻 DOM 可能已有首 token 内容;本断言只验证 rAF 被调用但从未触发。
|
||||
check("rAF 被卡死(0 帧触发)", a["rafCalls"] > 0, str(a))
|
||||
check("思考块默认收起", a["thinkOpen"] is False)
|
||||
check("思考块带蓝色动画类 streaming-think", a["thinkStreaming"])
|
||||
QTimer.singleShot(300, step3)
|
||||
|
||||
|
||||
def step3():
|
||||
window.browser.page().runJavaScript(JS_PHASE_B, on_b)
|
||||
|
||||
|
||||
def on_b(res):
|
||||
b = json.loads(str(res))
|
||||
check("rAF 卡死时兜底仍实时渲染正文", b["liveTextRendered"], str(b))
|
||||
check("收尾后思考标签还原为『已完成深度思考』",
|
||||
b["thinkLabelAfter"] == "已完成深度思考", str(b["thinkLabelAfter"]))
|
||||
check("收尾后移除 streaming-think 动画类", b["thinkStreamingAfter"] is False)
|
||||
check("耗时徽章 ⏱ 1.2s", b["durBadge"] == "⏱ 1.2s", str(b["durBadge"]))
|
||||
check("超时徽章 ⏱ 超时 120s", b["tmoBadge"] == "⏱ 超时 120s", str(b["tmoBadge"]))
|
||||
check("收起状态下 finish 后结果仍可见(修复展开为空)", b["resultWhileCollapsed"])
|
||||
check("长结果有展开按钮", b["longHasBtn"])
|
||||
check("长结果默认只显示尾部 4002 字", b["longPreviewLen"] == 4002,
|
||||
str(b["longPreviewLen"]))
|
||||
check("尾部预览不含中段标记(标记在头部)", b["longPreviewIsTail"]
|
||||
and b["longPreviewHasMarker"] is False)
|
||||
check("点击展开全文(含中段标记 + full 类)",
|
||||
b["expandedHasMarker"] and b["fullClass"]
|
||||
and b["expandedLen"] > 5000, str(b))
|
||||
check("再点收回到尾部预览", b["collapseBackLen"] == 4002,
|
||||
str(b["collapseBackLen"]))
|
||||
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