OpenCode 前端时间线布局连续性压测:Timeline Stability 测试套件全解析
本文以 OpenCode Web 应用(packages/app)的 Timeline Layout Continuity 压测套件为对象,讲清它的运行方式、rAF 采样探针、声明式不变量契约与受控后端夹具的设计,并说明这套 pass/fail 判据能证明什么、不能证明什么。读完后你应能独立运行 bun run test:stability,理解每个视觉契约的底层实现,并能按同样思路为流式会话时间线编写布局稳定性测试。
一、这个套件解决什么问题
OpenCode 的会话时间线是一个持续接收事件流(消息更新、part 增量、工具状态变化)并做虚拟化的动态列表。内容增长、行替换、折叠展开、滚动与重排相互交织时,最容易被用户感知到的缺陷是:可见锚点跳动、相邻行重叠、内容闪烁白屏、焦点/展开状态丢失。
套件说明文档将其定位为"连续性验证":连续性探针在浏览器渲染机会之间采样由 DOM 派生的布局与可见性状态,测试用例则声明显式的契约。文档明确列出了它要验证的七类契约:
- 当用户不在底部时,保持一个可见的语义锚点稳定;
- 当活跃内容增长或新内容出现时,保持末端锚定(贴底);
- 保持相邻可见行有序且无显著重叠;
- 在更新与虚拟化过程中保持用户选择的展开(disclosure)状态;
- 当一个可见表面替换另一个可见表面时,避免采样到空白间隙;
- 在本地状态或焦点依赖行/控件身份的地方,保持逻辑行与控件身份稳定;
- 在重测量过程中保持键盘、滚轮与嵌套滚动的归属一致。
套件驱动的是真实浏览器中的 reducer、projection、组件、虚拟化器、布局、焦点与交互代码;后端与事件生产者则是受控夹具。换言之,被测对象是完整的前端生产构建,被替换掉的只有网络与模型侧。
二、如何运行:test:stability 的实际构成
在 packages/app 下执行:
bun run test:stability
对照 package.json 可以还原这条脚本的完整构成:
bun test ./e2e/performance/unit/visual-stability.test.ts \
&& playwright test --config e2e/performance/timeline-stability/playwright.config.ts
即分两段执行:先跑纯分析器与采样器的单测(对应文档"Test Layers"中的 Oracle contract 层——"pure analyzer and browser sampler calibration tests"),再跑 Playwright 浏览器压测。
timeline-stability 专属的 Playwright 配置继承自 e2e 基础配置,并覆盖了以下关键项:
export default {
...config, // 继承 ../playwright.config 的 webServer/use 等配置
testDir: ".",
testMatch: "**/*.spec.ts",
outputDir: "../../test-results/timeline-stability",
reporter: [["html", { outputFolder: "../../playwright-report/timeline-stability", open: "never" ]], ["line"]],
retries: 0, // 不重试:失败必须如实暴露
workers: 1, // 单 Chromium worker 串行执行
use: {
...config.use,
trace: "retain-on-failure",
screenshot: "only-on-failure",
video: "retain-on-failure",
},
}
三个参数与文档描述一一对应:
workers: 1即"在单个 Chromium worker 中运行生产构建",保证时间线事件流在单浏览器内按序投递,避免并发干扰时序采样;retries: 0意味着任何偶发失败都会留痕而不是被重试掩盖;retain-on-failure三件套(trace / 截图 / 视频)正是诊断证据链的来源。
文档还强调:选定场景在应用就绪后施加确定性 4 倍 CPU 压力,"这是压力剖面(stress profile),而非对某台具体设备的仿真"。这个 4 倍压力在夹具里通过 CDP 精确实现(见第五节)。
三、连续性探针:rAF 驱动的 DOM 采样原理
探针实现位于 probe.ts,它把一段脚本注入页面,挂到 window.__visualStabilityProbe 上,并形成一个自持的采样循环。
3.1 采样循环与时间基线
- 探针启动时记录
performance.now()作为startedAt,并把performance.timeOrigin + startedAt解析后作为 epoch 基线返回给宿主,使采样时间轴与 Playwright trace 对齐; - 核心是
sample()函数:每次采样后通过setTimeout(…, 0)+requestAnimationFrame(sample)排队下一帧(见 probe.ts),从而绑定浏览器的渲染机会; - 启动 Promise 会等到至少一个样本落地才 resolve,确保后续标记(marker)不会发生在空窗口上。
每个样本记录两部分:
- 视口状态:探针定位包含
[data-timeline-row]的.scroll-view__viewport元素,采样其top/bottom/scrollTop/scrollHeight/clientHeight,并推导distanceFromBottom(距底距离)——这是末端锚定判定的直接输入; - 区域(region)状态:测试为每个关注的行/控件声明 CSS 选择器(可附加
closest提升选择器),探针逐个采样:present/visible/inViewport/cssHidden;- 未裁剪布局边界
layoutTop/layoutBottom与祖先裁剪后可见交集top/bottom/width/height:探针沿父链向上,对所有overflowY/overflowX为hidden|clip|scroll|auto的祖先以及视口本身做矩形交集(见 probe.ts)。这正对应文档"analyzer 同时记录未裁剪布局边界与祖先裁剪可见交集"一句; - 不透明度:既支持
opacitySelectors聚合内容节点的不透明度(沿父链逐层相乘),也沿祖先链累计opacity并检测display: none/visibility: hidden; node身份号:用一个WeakMap<Node, number>给首次见到的 DOM 节点分配递增 ID,用于后续判定"是否被重新挂载";label(aria-label)与截断后的text文本快照。
3.2 标记(marker)与事件窗口
测试在投递每个后端事件后调用 markVisualStability(page, label),把带时间戳的标记写入探针。analyzer.ts 的 analyzeVisualTraceByMarker 会把这些标记切分为事件窗口:每个窗口取"标记前最后一个样本 + 窗口内样本"逐段判定,问题消息会带上前缀 marker: issue。此外 motion 类问题默认还会做聚合分析(aggregateMotion),即跨整个 trace 检查位置反转次数。
四、声明式契约:不变量类型与判定规则
测试通过 visualPlan(regions, invariants, options) 声明契约,不变量类型定义在 invariant.ts。文档七条契约与不变量类型的对应关系,以及分析器 analyzer.ts 中的具体判定逻辑如下:
| 不变量类型 | 语义 | 判定细节 |
|---|---|---|
required |
指定区域必须至少渲染过一次 | 全程无 visible 样本则报 "never rendered" |
continuous-any |
一组区域中任一必须连续可见 | 在"首次可见→末次可见"区间内出现全空样本则报 "blanked between visible frames" |
unique |
指定区域不得同时出现多个实例 | 某样本 count > 1 即报重复 |
stable |
指定区域保持同一 DOM 身份 | 可见样本中出现多个不同 node ID 则报 "remounted N times"(守护焦点/本地状态不丢) |
fixed |
指定区域在视口中位置固定(默认容差 1px) | 相对首个可见样本的 top 位移超限即报 |
opacity |
可见期间不透明度不得低于地板(legacy 默认 0.65) | 任何可见样本低于 floor 即报 |
continuity |
present 与 visible 两个层面不得中途断裂 | "present 帧之间消失"或"可见帧之间白屏"(限视口内样本)分别报出 |
motion |
允许运动但限制方向反转次数 | 对 top/bottom/width/height 逐个计算相邻样本差的方向序列,反转次数超过 maxReversals / maxPositionReversals 即报 |
label-stability |
aria-label 不得回退 | 标签序列中出现"变回旧值"即报 "label reverted" |
flow |
按声明顺序的区域不得互相重叠或倒序 | 同时可见且在视口内的相邻两区域,before.bottom - after.top 超过 overlapTolerance(默认 0.5px)或顺序颠倒即报 |
preserve-bottom-anchor |
初始贴底则全程保持贴底 | 首样本 distanceFromBottom <= 4 后,任何样本超过 4px 即报 |
acquire-bottom-anchor |
结束时必须到达贴底 | 末样本 distanceFromBottom > 4 即报 |
其中 preserve-bottom-anchor 与 acquire-bottom-anchor 的 4px 阈值可以直接在 analyzer.ts 中读到。
文档中"滚动条与原始 scrollTop 变化本身不会导致连续性检查失败;用户可见的语义锚点移动才会"这句,在源码层面得到印证:分析器从未直接比较 scrollTop 序列,所有判定都基于区域的可见几何、身份、标签与不透明度——这正是"语义锚点"与"滚动位移"的区分所在。
五、测试层级与确定性夹具
5.1 测试层级
README 将套件划分为六个层级,目录下的 spec 文件与之对应(timeline-stability 目录):
- Projection(投影):被接收的行、分组、标签与最终可见状态——如
file-matrix.spec.ts、transition-matrix.spec.ts; - Local state(本地状态):展开状态、身份、重复投递、虚拟化恢复——如
lifecycle.spec.ts中"用户折叠某行后,后续兄弟重渲染不得改变其aria-expanded"的用例; - Interaction(交互):滚轮、键盘、嵌套滚动、可操作性与焦点——
interaction.spec.ts、scroll-interaction.spec.ts; - Layout continuity(布局连续性):锚定、相邻关系、响应式重排、可见表面交接——
lifecycle.spec.ts中"Thinking 占位被流式 reasoning + text 替换且无白屏回合"的用例就在此列(用continuous-any覆盖 thinking/reasoning/text 三个区域); - Reducer hardening(reducer 加固):形状合法但故意乱序、重复、删除、替换的事件——
adverse.spec.ts; - Oracle contract(判据契约):纯分析器与浏览器采样器的校准测试——
fixture.test.ts(bun 单测)与oracle-browser.spec.ts。
文档同时给出了一条夹具编写纪律:生产生命周期夹具应建模当前生产者实际会发出的状态;不可能或乱序的序列只属于 reducer-hardening 测试,不得被描述为"正常 provider 行为"。这避免了把加固测试的假想故障误标成线上行为。
5.2 夹具如何做到确定性
fixture.ts 是整个套件的确定性核心,setupTimeline(page, input) 的要点:
- 受控后端:
mockOpenCodeServer+installSseTransport在页面内拦截网络,构造确定性项目(proj_timeline_stability)、会话(ses_timeline_stability)与消息序列,事件按测试脚本逐条投递并可精确控制延迟(sendAll); - Schema 级校验:所有夹具消息、part、事件在进入页面之前先经 effect
Schema解码(errors: "all", onExcessProperty: "error"),validateTimelineMessages还额外断言消息/ part ID 唯一、part 归属匹配、user/assistant 角色的 part 类型约束(见 fixture.ts)——保证投递给 reducer 的事件"形状永远合法",从而把"乱序/重复"等故障与"非法数据"清晰分开; - 确定性 4x CPU 压力:
cpuRate参数经 CDPEmulation.setCPUThrottlingRate施加(见 fixture.ts),spec 中普遍传cpuRate: 4,与文档"selected scenarios use deterministic 4x CPU stress after application readiness"一致; - 环境矩阵维度:
viewport(默认 1400x900)、deviceScaleFactor(经Emulation.setDeviceMetricsOverride)、locale、reducedMotion(emulateMedia)、protocol: "v1" | "v2"、seedHistory(预置 18 条历史消息让虚拟化时间线更真实)——分别支撑environment-matrix、context-matrix、shell-matrix等矩阵型 spec; - 稳定的等待手段:
waitForVisualSettle要求指定选择器的矩形连续stableFrames帧签名不变且相邻元素保持有序才放行;settle(frames)等待若干 rAF。注意这与 e2e/AGENTS.md 的规范一致:不用墙钟延迟做同步,而是等待具体的 UI 状态。
一个完整的用例形态(lifecycle.spec.ts)是:以 4x CPU 压力与历史种子启动 → 发送 busy 状态 → 等 part 就位 → 滚到底 → startVisualProbe 声明区域 → sendAll 交错投递空/短/长三个并行 shell 的完成事件与后续文本 → stopVisualProbe 收集 trace → reportVisualStability 按契约(required/unique/stable/opacity/continuity/motion/label-stability/preserve-bottom-anchor/flow,且 perMarker: true)判定。
六、诚实的边界:这套判据不能证明什么
README 用一整节划定证据边界,值得原样吸收为设计准则:
- 不检查每个合成器呈现的像素。
requestAnimationFrame之后取到的样本是 DOM/布局观测,不证明每个被采样状态都真的被显示,也不证明每个被显示的帧都被采样了; - 因此它不覆盖:仅合成器或光栅化层面的瑕疵;颜色、对比度、canvas、WebGL、遮罩、不规则裁剪与任意遮挡;物理屏幕刷新率、操作系统原生缩放与任何具名低端设备;TCP 分段、代理缓冲与真实服务器/provider 的完整链路;
- Playwright 视频、trace、截图与观测 JSON 只是诊断证据,不是像素基线,不参与常规 pass/fail 判定。
这个边界声明的价值在于防止误读:绿色通过说明"在受控事件流与 4x CPU 压力下,DOM 布局契约全部成立",而不是"在任何设备上都视觉无瑕"。
七、诊断证据链
失败时,由于配置了 retain-on-failure,Playwright 配置 会保留:
video.webm——场景全过程录像;trace.zip——Playwright 交互/网络/DOM 时间线;- 失败截图;
- 采样 DOM/布局 trace JSON——即探针收集的
samples + markers观测数据; - 事件标记(
describeEvent生成的type:partID:tool:status摘要)与汇总后的违例信息。
此外还有一个环境变量控制的可选开关:设置 OPENCODE_STABILITY_CAPTURE=1 才会启用 before/violation/after 截图采集。该开关在 capture.ts 中生效(if (process.env.OPENCODE_STABILITY_CAPTURE !== "1") return)。文档解释其为默认关闭的原因:合成器读回(readback)会扰动计时,因此只在需要取证时开启。
八、小结:可复用的方法论
Timeline Stability 套件给出的方法可以概括为四步,且每一步都有源码可对:
- 采样与渲染对齐:探针用 rAF 链绑定渲染机会,同时记录未裁剪几何与祖先裁剪交集,使"可见"有精确定义;
- 契约声明化:把"不闪烁、不重叠、锚点稳定、身份不变"等体验要求转写成可判定不变量,判据实现为纯函数(
analyzeVisualObservations),可用 bun 单测独立校准; - 故障注入与正常路径分层:合法事件走生产生命周期夹具,乱序/重复/删除事件只出现在 reducer-hardening 层,避免证据语义混淆;
- 判据边界显式化:明确声明不覆盖合成器/像素级问题与真实网络链路,诊断产物不参与 pass/fail,保证结论可解释。
对维护 OpenCode 会话时间线(或任何流式、虚拟化列表)的开发者而言,这份套件的价值不仅在于它能拦截回归,更在于它示范了如何用 DOM 级观测把"布局连续性"这类体验属性变成可声明、可判定、可复现的工程契约。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0624
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00