# -*- 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()