LibreChat E2E 测试体系实战:Mock 假模型、Redis 流传输分片与 Bombadil 属性化浏览器探索
本文以 LibreChat 仓库的 e2e/README.md 为核心,系统讲解其 Playwright 端到端测试体系的三层能力:免真实凭据的 Mock 测试画像(Profile)、内存/Redis 双流存储模式与 CI 分片策略、基于 Bombadil 的属性化浏览器探索测试,以及用 Playwright codegen 录制并落地为可维护规格(spec)的完整工作流。读完本文,你可以直接在本仓库运行完整的 mock e2e 套件、切换 Redis 传输验证流式保真度、复现并归档 Bombadil 发现的缺陷,并掌握从浏览器录制到提交级测试的转化方法。
一、Mock E2E 画像:免真实 LLM 凭据的安全默认
Mock e2e 画像是生成式测试最安全的默认选择:它使用 e2e/config/librechat.e2e.yaml 启动 LibreChat,通过 LIBRECHAT_TEST_RUN_HOOK 环境变量注入一个进程内假 LLM,自动创建一个已认证的 e2e 用户,全程不触碰任何真实 Provider 凭据。
1.1 从 npm 脚本到测试服务器的启动链
根目录 package.json 中定义的入口脚本为:
npm run e2e:mock
该脚本实际展开为两步:
npm run e2e:prepare # 等价于 npm run frontend,构建 data-provider、data-schemas、api、client
playwright test --config=e2e/playwright.config.mock.ts
playwright.config.mock.ts 在加载阶段就完成了大量准备工作,从源码可以看到其关键机制:
- 生成运行时配置:
writeRuntimeMockConfig()把模板 e2e/config/librechat.e2e.yaml 复制到被 git 忽略的e2e/.generated/librechat.e2e.yaml,并替换模板中的占位符(如动态 MCP 服务器、模型录制 Provider 块)。运行时CONFIG_PATH指向这份生成副本,模板本身保持"零凭据"状态。 - 注入假模型:环境变量
LIBRECHAT_TEST_RUN_HOOK指向 e2e/setup/fake-model.js。该钩子由@librechat/api的createRun在进程内加载,把每次运行的模型替换为 agents 包自带的FakeChatModel,因此走的是真实的Run.create→ graph → 工具节点流水线,只是把模型层换成了确定性假实现。 - 凭据清洗:
neutralizeCredentialEnv()和neutralizeDotenvSecrets()用正则/(API_KEY|SECRET|TOKEN|PASSWORD|CREDENTIALS|CLIENT_ID|_KEY)$/i把本地.env及进程环境中所有凭据形态的变量置空,避免开发机上的真实 Key 泄漏到测试服务器。 - 自动拉起配套服务:
webServer数组会依序启动假 MCP HTTP 服务器(8765)、假活动标签服务器(8889)、假 Assistants 服务器(8890),最后由 e2e/setup/start-server.js 启动 LibreChat 本体(支持 1 副本或E2E_REPLICAS=2双副本 + 测试代理的拓扑)。 - 用户认证与清理:
globalSetup指向 e2e/setup/global-setup.ts,通过 e2e/setup/user.ts 得到默认用户(testuser@example.com,可用E2E_USER_EMAIL/E2E_USER_PASSWORD覆盖)完成注册/登录;globalTeardown指向 e2e/setup/global-teardown.mock.ts,运行结束后删除主/副两个 e2e 用户,保证数据不残留。
1.2 假模型的"标记驱动"响应协议
e2e/setup/fake-model.js 不是简单返回固定文本,它实现了一套标记(marker)驱动的响应脚本协议:用户在会话中发送 E2E_REPLY:xxx、E2E_THINK_REPLY:xxx、E2E_SLOW_REPLY:xxx、E2E_STEER_TOOL_REPLY:xxx、E2E_FORCED_ERROR:xxx、E2E_MERMAID_ARTIFACT_REPLY 等标记,假模型就分别产出普通回复、带 think 标签的推理回复、慢速分块流、工具调用、流中断错误或 Mermaid/HTML Artifact 内容。其内部类 UsageEmittingFakeChatModel 还会在末尾追加一个携带 usage_metadata 的空 chunk,让 token 用量 SSE 事件在 mock 运行中端到端流动(配合配置中的 contextCost: true 验证上下文用量表的成本行)。
这套协议让浏览器测试可以精确控制"模型说了什么、何时说、是否调用工具",是 Mock 画像能够覆盖流式保真、Steering、HITL、子代理活动等复杂场景的基础。
1.3 零凭据配置模板里预埋的测试开关
e2e/config/librechat.e2e.yaml 虽然不含任何真实凭据,但预埋了大量功能开关供不同 spec 使用:
endpoints.custom定义了 Mock Provider A–F 六个占位端点,baseURL指向本地的假标签服务器(8889),fetch: false表示不拉取模型列表;由于假模型在请求发出前就替换了 graph,这些占位 URL 永远不会被真正调用。- Provider E/F 分别开启
activityLabel与activityPhaseLabel,用独立端点隔离"子级活动标签"和"父级活动阶段"两种配置的渲染行为,避免相互干扰。 interface.schedules.use: true+minIntervalMinutes: 1:定时聊天是实验性功能默认关闭,harness 必须显式开启,并把最小间隔从生产 60 分钟降到 1 分钟,spec 才能快速创建并触发调度。memory.personalize: true+tokenLimit: 10000:开启记忆功能,让memory.spec.ts能切换内联的 set/delete memory 工具。endpoints.agents.toolApproval:mode: bypass但ask列表里单独挂了一个approval_probe_mcp_e2e-memory工具,配合钩子 e2e/setup/tool-approval-hook.js,只让专用的审批探测 spec 走真实 HITL 暂停/恢复流程,而不阻塞其余共享 spec。mcpServers声明了 stdio 型e2e-memory(e2e/setup/fake-mcp-server.js)与 streamable-http 型e2e-http(8765);allowedDomains刻意不含 127.0.0.1,使e2e-http在启动时被白名单拦截(存为inspectionFailed),供mcp-allowlist-override.spec.ts验证"管理员面板覆盖配置后服务器能重新初始化"。modelSpecs定义了e2e-icon-spec、e2e-skill-scope、e2e-soft-default、e2e-branded、e2e-starters、e2e-hitl等命名规格,分别驱动模型图标、技能作用域、软默认、品牌化落地页、会话起始提示词和 HITL 场景。
二、流存储双模式与 CI 分片:Memory 与 Redis
2.1 为什么需要两条流存储泳道
生成式对话的每个流事件最终都来自"生成流存储"(generation stream store)。Mock 画像默认使用内存存储;而 Redis job store + pub/sub 传输是生产部署中的真实路径。为了用同一批浏览器场景验证两条路径的一致性,仓库提供了:
npm run e2e:mock:redis
从 package.json 可见,该命令等价于 cross-env E2E_STREAM_STORE=redis playwright test --config=e2e/playwright.config.mock.ts。
2.2 环境变量如何决定存储模式
e2e/setup/env.ts 中的 getStreamStoreEnv() 是模式分发的核心,支持三种取值:
E2E_STREAM_STORE |
行为 |
|---|---|
memory(默认) |
USE_REDIS=false、USE_REDIS_STREAMS=false、E2E_REQUIRE_REDIS_STREAMS=false,显式禁用 Redis |
redis |
USE_REDIS=true、USE_REDIS_STREAMS=true、E2E_REQUIRE_REDIS_STREAMS=true,REDIS_URI 默认 redis://127.0.0.1:6379/15(数据库 15),REDIS_KEY_PREFIX 默认取自 E2E_REDIS_KEY_PREFIX,缺省为 LibreChatE2E |
redis-cluster |
同上但 USE_REDIS_CLUSTER=true,REDIS_URI 默认为 7001/7002/7003 三节点 |
E2E_REQUIRE_REDIS_STREAMS=true 就是文档所说的"fail closed"机制:e2e/setup/start-server.js 启动时会 ping Redis(超时 10 秒),并验证生成 job 管理器没有静默回退到内存,否则测试直接失败而不是带病运行。
2.3 CI 分片与 Redis 传输专项套件
Pull Request CI 的策略是:完整 mock 套件跑内存模式、分 3 片,外加一条聚焦的 Redis 传输套件:
npx playwright test --config=e2e/playwright.config.mock.ts --shard=1/3
npm run e2e:mock:redis:transport
分片安全性的依据来自 playwright.config.mock.ts:workers: 1 保证每个 shard 只有一个 worker,从而不与同一 shard 内的其他测试争抢同一个认证用户和数据库;CI 下 retries: 2 且 forbidOnly 生效。
e2e:mock:redis:transport 使用派生配置 e2e/playwright.config.redis.ts,它展开 mock 配置后只保留 10 个"跨越流存储边界"的场景,与 README 所列覆盖范围一一对应:
testMatch: [
/completion\.spec\.ts/, // 完成
/deferred-tools-hitl\.spec\.ts/, // HITL 审批
/model-spec-icons\.spec\.ts/, // 模型图标
/steering\.spec\.ts/, // steering(运行中转向)
/steering-escalation\.spec\.ts/, // steering 升级
/streaming\.spec\.ts/, // 流式保真
/subagent-activity\.spec\.ts/,
/thread-fold\.spec\.ts/, // 线程折叠
/tool-approvals\.spec\.ts/, // 工具审批
/usage\.spec\.ts/, // 用量
]
Redis 泳道还放宽了断言预算:expect.timeout 从 10 秒提到 20 秒、CI 重试 3 次——因为每个流事件都要走真实 Redis 往返,暂停/恢复与重新水合(整 job 重放)路径最接近超时预算。夜间调度和手动 workflow 则会在两种流模式下各跑完整套件、每种模式 2 片。
三、Bombadil 属性化浏览器测试:让随机探索替你找 bug
Bombadil 是 e2e/bombadil/ 目录实现的属性化(property-based)浏览器测试框架,它对核心聊天回路、消息分支、并行多会话响应、模型切换、页面重载和侧边栏会话生命周期操作进行随机序列探索,而不是沿着固定脚本点击。
3.1 基础探索命令
npm run e2e:bombadil
底层由 e2e/playwright.config.bombadil.ts(testDir: 'bombadil'、testMatch: 'harness.spec.ts')驱动 e2e/bombadil/harness.spec.ts,该 harness 会:
- 把 e2e/bombadil/specification.ts 复制为运行时副本,并把登录占位符
__BOMBADIL_E2E_USER_EMAIL__/__BOMBADIL_E2E_USER_PASSWORD__替换为实际 e2e 用户凭据; - spawn
node_modules/.bin/bombadil二进制,执行browser test --headless --output-path e2e/.generated/bombadil-output --instrument-javascript inline --exit-on-violation --time-limit 90s; - 对新运行前,若
e2e/.generated/bombadil-output已存在,先归档到e2e/.generated/bombadil-history/(带时间戳)。
调长探索时间:设置 BOMBADIL_TIME_LIMIT(harness 支持 90s、5m、2h 这类 数字+ s/m/h/d 格式),本地或定时任务可放心用更长的值。
3.2 失败复现与归档闭环
失败会留下可复现 trace:
BOMBADIL_REPRODUCE=e2e/.generated/bombadil-output npm run e2e:bombadil:run
复现时 harness 会向 bombadil 传入 --reproduce <path> 而不是 --time-limit,并把 Playwright 超时放宽到 30 分钟。按设计,复现出一个真实违反(invariant violation)时测试应当失败;但如果流式时序变化导致行为发散,复现结果可能偏离,Bombadil 会明确报告这一点。
3.3 CI 集成:非阻塞探索 + 工件
CI 在 Bombadil Property Exploration workflow 中做 5 分钟宽泛探索(非阻塞)。属性失败时:
- 下载
bombadil-reproduction-*工件到e2e/.generated/bombadil-output/,本地执行上面的复现命令; - 配套的
bombadil-diagnostics-*工件包含 CI 日志、Playwright HTML 报告与测试结果; - Bombadil 失败只产生 workflow 警告,不阻塞合并。
3.4 JavaScript 插桩范围
默认只对 inline JS 做插桩,因为对 LibreChat 完整 Vite bundle 插桩会在有状态长运行中超过 Bombadil 的 driver 超时(harness.spec.ts 中的注释明确说明了这一点)。需要做更短周期的覆盖率引导实验时,可以设置:
BOMBADIL_INSTRUMENT_JAVASCRIPT=files,inline npm run e2e:bombadil
3.5 五个可独立运行的生命周期属性
分支重载、fork 提交、模型/会话、HITL 暂停/恢复、运行中 steering 的生命周期属性可以单独跑:
npm run e2e:bombadil:branch-reload
npm run e2e:bombadil:fork-lifecycle
npm run e2e:bombadil:model-lifecycle
npm run e2e:bombadil:hitl
npm run e2e:bombadil:steering
package.json 中每条命令都通过 BOMBADIL_SPECIFICATION 指定对应的 specification 文件(如 hitl-lifecycle.specification.ts),并内置时间上限(30s/30s/30s/45s/75s 分别对应 branch-reload/fork/model/hitl/steering)。这些聚焦命令是诊断性属性:复现出产品不变量违反时以非零码退出。对应的 trace 输出目录按 specification 区分(如 e2e/.generated/bombadil-output-hitl),复现命令也要匹配目录与 :run 脚本:
BOMBADIL_REPRODUCE=e2e/.generated/bombadil-output-hitl npm run e2e:bombadil:hitl:run
各属性的语义(摘自 e2e/README.md):
- HITL:驱动一次真实的
ask_user_question检查点走 answer/resume 控制器——问题暂停时重载页面、只回答一次、再重载已完成会话; - Steering:在一次慢速 MCP 支撑的运行中提交 in-flight steering,断言它恰好从 composer 锚点移动到响应中的工具边界一次,并重载已应用状态;
- Model lifecycle:作为"应通过的对照(passing control)";
- Branch reload 与 fork:保留其最小失败 trace 作为回归锚点。
由于 harness 使用的是免凭据 mock-LLM 画像,探索过程永远不会发出计费的 Provider 请求。
四、录制测试:把探索性浏览器会话变成草稿 spec
4.1 一键录制(Mock 画像)
npm run e2e:record
对照 e2e/setup/record.js 的源码,这条命令的实际行为是:
npm run e2e:prepare构建应用;- 若
E2E_BASE_URL(npm 脚本固定为http://localhost:3333,避免与 3080 上的常规开发服务器冲突)未就绪,spawn e2e/setup/start-server.js 启动带进程内假 LLM 的测试服务器; writeStorageState()驱动 headless Chromium 完成注册(失败则回退登录),把认证态写入e2e/storageState.json;- 以
--target=playwright-test --test-id-attribute=data-testid --load-storage e2e/storageState.json打开 Playwright codegen,起始页/c/new。
原始录制写入 e2e/recordings/,该目录被 git 忽略。
4.2 本地真实配置画像
要对真实的本地 LibreChat 配置(而非 mock 画像)录制:
npm run e2e:record:local
4.3 record.js 的完整参数
node e2e/setup/record.js --url=http://localhost:3080/c/new
node e2e/setup/record.js --profile=local --no-output
node e2e/setup/record.js --auth-only
node e2e/setup/record.js --output=e2e/recordings/settings-draft.spec.ts
结合源码中的 parseArgs() 与 printHelp(),完整参数语义为:
| 参数 | 默认值 | 说明 |
|---|---|---|
--profile mock|local |
mock |
mock 画像会生成运行时配置、清洗凭据并注入假模型钩子;local 直接使用本地环境 |
--url <url> |
<baseURL>/c/new |
codegen 打开的起始 URL |
--output <path> |
e2e/recordings/recording-<时间戳>.spec.ts |
原始录制输出路径 |
--storage <path> |
e2e/storageState.json |
认证存储态路径 |
--no-output |
— | 只在 codegen 面板展示生成代码,不落盘 |
--auth-only |
— | 启动服务器、写入存储态后直接退出(不打开 codegen) |
五、LLM 辅助循环:从录制草稿到可提交的测试
仓库给"让 LLM 用 Computer Use 操作 headed Playwright 浏览器来录制"的完整工作流定了 7 步标准:
- 启动
npm run e2e:record; - 让 LLM 用 Computer Use 操作该 headed 浏览器;
- 捕获完工作流后停止 codegen;
- 把
e2e/recordings/中有价值的部分搬进e2e/specs/mock/下的已提交 spec; - 用 role、label、文本或
data-testid定位器替换脆弱的生成选择器(codegen 本身就以--test-id-attribute=data-testid启动,优先产出 testid 定位器); - 添加证明行为成立的断言,而不仅是断言被点击的路径;
- 用
npm run e2e:mock -- <spec name>运行完成的 spec 验证。
关键原则:生成的录制是草稿,不是最终测试。提交版本应尽可能复用 e2e/specs/mock/helpers.ts 中的共享辅助函数,等待网络或可见 UI 状态而不是固定 sleep,并保持测试数据确定性——这与 mock 画像"每个 spec 使用全新注册用户 + 单 worker"的隔离设计是一致的:确定性数据配合 e2e/recordings/ 之外的受控输入(fake-model 的标记协议),才能被 CI 稳定复现。
六、关键文件地图与延伸阅读
| 关注点 | 文件 |
|---|---|
| 本文主体文档 | e2e/README.md |
| Mock 画像主配置 | e2e/playwright.config.mock.ts |
| Redis 传输专项配置 | e2e/playwright.config.redis.ts |
| 零凭据配置模板 | e2e/config/librechat.e2e.yaml |
| 流存储环境变量分发 | e2e/setup/env.ts |
| 进程内假 LLM(标记协议) | e2e/setup/fake-model.js |
| 测试服务器启动(fail-closed Redis 检查) | e2e/setup/start-server.js |
| 录制脚本 | e2e/setup/record.js |
| Bombadil harness | e2e/bombadil/harness.spec.ts |
| Mock spec 目录(约 60 个场景) | e2e/specs/mock/ |
| npm 脚本入口 | package.json |
适用前提与限制小结:本地运行需先构建前端(e2e:prepare),MongoDB 默认连 mongodb://127.0.0.1:27017/LibreChat-e2e(可用 MONGO_URI 覆盖,或依赖 E2E_USE_MEMORY_MONGO 自动内存模式);Redis 泳道要求 6379 端口有可用 Redis;模型 fixture 录制模式(E2E_MODEL_FIXTURES=record)是唯一会接触真实 Provider 的路径,且被 playwright.config.mock.ts 中的多重防护严格限制在指定 spec 与命名白名单内,普通开发者日常只需面对零凭据的 mock 泳道即可。
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 StartedRust0623
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