首页
/ LibreChat E2E 测试体系实战:Mock 假模型、Redis 流传输分片与 Bombadil 属性化浏览器探索

LibreChat E2E 测试体系实战:Mock 假模型、Redis 流传输分片与 Bombadil 属性化浏览器探索

2026-09-05 19:44:50作者:贡沫苏Truman

本文以 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 在加载阶段就完成了大量准备工作,从源码可以看到其关键机制:

  1. 生成运行时配置writeRuntimeMockConfig() 把模板 e2e/config/librechat.e2e.yaml 复制到被 git 忽略的 e2e/.generated/librechat.e2e.yaml,并替换模板中的占位符(如动态 MCP 服务器、模型录制 Provider 块)。运行时 CONFIG_PATH 指向这份生成副本,模板本身保持"零凭据"状态。
  2. 注入假模型:环境变量 LIBRECHAT_TEST_RUN_HOOK 指向 e2e/setup/fake-model.js。该钩子由 @librechat/apicreateRun 在进程内加载,把每次运行的模型替换为 agents 包自带的 FakeChatModel,因此走的是真实的 Run.create → graph → 工具节点流水线,只是把模型层换成了确定性假实现。
  3. 凭据清洗neutralizeCredentialEnv()neutralizeDotenvSecrets() 用正则 /(API_KEY|SECRET|TOKEN|PASSWORD|CREDENTIALS|CLIENT_ID|_KEY)$/i 把本地 .env 及进程环境中所有凭据形态的变量置空,避免开发机上的真实 Key 泄漏到测试服务器。
  4. 自动拉起配套服务webServer 数组会依序启动假 MCP HTTP 服务器(8765)、假活动标签服务器(8889)、假 Assistants 服务器(8890),最后由 e2e/setup/start-server.js 启动 LibreChat 本体(支持 1 副本或 E2E_REPLICAS=2 双副本 + 测试代理的拓扑)。
  5. 用户认证与清理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:xxxE2E_THINK_REPLY:xxxE2E_SLOW_REPLY:xxxE2E_STEER_TOOL_REPLY:xxxE2E_FORCED_ERROR:xxxE2E_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 分别开启 activityLabelactivityPhaseLabel,用独立端点隔离"子级活动标签"和"父级活动阶段"两种配置的渲染行为,避免相互干扰。
  • interface.schedules.use: true + minIntervalMinutes: 1:定时聊天是实验性功能默认关闭,harness 必须显式开启,并把最小间隔从生产 60 分钟降到 1 分钟,spec 才能快速创建并触发调度。
  • memory.personalize: true + tokenLimit: 10000:开启记忆功能,让 memory.spec.ts 能切换内联的 set/delete memory 工具。
  • endpoints.agents.toolApprovalmode: bypassask 列表里单独挂了一个 approval_probe_mcp_e2e-memory 工具,配合钩子 e2e/setup/tool-approval-hook.js,只让专用的审批探测 spec 走真实 HITL 暂停/恢复流程,而不阻塞其余共享 spec。
  • mcpServers 声明了 stdio 型 e2e-memorye2e/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-spece2e-skill-scopee2e-soft-defaulte2e-brandede2e-starterse2e-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=falseUSE_REDIS_STREAMS=falseE2E_REQUIRE_REDIS_STREAMS=false,显式禁用 Redis
redis USE_REDIS=trueUSE_REDIS_STREAMS=trueE2E_REQUIRE_REDIS_STREAMS=trueREDIS_URI 默认 redis://127.0.0.1:6379/15(数据库 15),REDIS_KEY_PREFIX 默认取自 E2E_REDIS_KEY_PREFIX,缺省为 LibreChatE2E
redis-cluster 同上但 USE_REDIS_CLUSTER=trueREDIS_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.tsworkers: 1 保证每个 shard 只有一个 worker,从而不与同一 shard 内的其他测试争抢同一个认证用户和数据库;CI 下 retries: 2forbidOnly 生效。

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.tstestDir: '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 支持 90s5m2h 这类 数字+ 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 分钟宽泛探索(非阻塞)。属性失败时:

  1. 下载 bombadil-reproduction-* 工件到 e2e/.generated/bombadil-output/,本地执行上面的复现命令;
  2. 配套的 bombadil-diagnostics-* 工件包含 CI 日志、Playwright HTML 报告与测试结果;
  3. 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 的源码,这条命令的实际行为是:

  1. npm run e2e:prepare 构建应用;
  2. E2E_BASE_URL(npm 脚本固定为 http://localhost:3333,避免与 3080 上的常规开发服务器冲突)未就绪,spawn e2e/setup/start-server.js 启动带进程内假 LLM 的测试服务器;
  3. writeStorageState() 驱动 headless Chromium 完成注册(失败则回退登录),把认证态写入 e2e/storageState.json
  4. --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 步标准:

  1. 启动 npm run e2e:record
  2. 让 LLM 用 Computer Use 操作该 headed 浏览器;
  3. 捕获完工作流后停止 codegen;
  4. e2e/recordings/ 中有价值的部分搬进 e2e/specs/mock/ 下的已提交 spec;
  5. 用 role、label、文本或 data-testid 定位器替换脆弱的生成选择器(codegen 本身就以 --test-id-attribute=data-testid 启动,优先产出 testid 定位器);
  6. 添加证明行为成立的断言,而不仅是断言被点击的路径;
  7. 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 泳道即可。

登录后查看全文
热门项目推荐
相关项目推荐