Files
Haocode/tests/smoke_live_guard.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

281 lines
12 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.
# -*- 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 看门狗(200msforceRenderNow
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 Btimers 跑过之后): 兜底应已渲染正文 + 收尾 + 其余特性
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()