ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Dify E2E 测试体系:Cucumber + Playwright 仓库级端到端场景的运行、编排与契约实践

Dify E2E 测试体系:Cucumber + Playwright 仓库级端到端场景的运行、编排与契约实践 Dify E2E 测试体系Cucumber Playwright 仓库级端到端场景的运行、编排与契约实践【免费下载链接】difyBuild Agentic workflows, RAG pipelines, with rich AI model and tool support on one collaborative workspace. Deploy on cloud, VPC, or self-hosted, so teams move from prototype to production without rebuilding the stack.项目地址: https://gitcode.com/GitHub_Trending/di/dify本文以 Dify 仓库中的 e2e/README.md 为入口展开——该文件声明本包的规范化文档位于 e2e/AGENTS.md因此后者构成本文的主体骨架。读完本文你可以掌握 Dify E2E 包的全部运行命令与语义标签体系理解单一编排器拥有服务生命周期的运行时所有权模型并了解浏览器操作与 oRPC 生成契约如何划分 Browser/API 边界以及种子数据、清理注册与失败诊断的完整约定。1. 定位与核心结论README 指向的规范化文档e2e/README.md 全文只有一行有效信息Canonical documentation for this package lives in [AGENTS.md].这确立了 Dify 仓库的一个文档约定包级 README 只做导航真正的架构、运行时、会话、标签语义、种子、协议与清理契约由 e2e/AGENTS.md 承载。该文档开篇即给出包的定位This package contains Difys repository-level Cucumber scenarios with Playwright as the browser layer.即e2e/是 Dify 的仓库级 E2E 测试包采用CucumberGherkin 场景描述 Playwright浏览器驱动的组合。文档同时划清了职责边界本文件AGENTS.md拥有当前包的架构、运行时/会话/标签语义、seed、协议与清理契约编写与评审方法论由仓库本地的e2e-cucumber-playwrightskill 负责而各功能特性的具体事实则放在各自 feature 最近的AGENTS.md中。从源码结构看e2e/目录与该描述完全吻合Gherkin 特性文件位于 features/ 下的accessibility/、agent-v2/、apps/、auth/、smoke/等子目录步骤定义glue code位于 features/step-definitions/按能力域组织共享能力位于 support/ 与 scripts/。2. 运行命令全集从一次性安装到按标签子集执行所有命令必须从仓库根目录执行。先执行一次性安装pnpm install安装依赖与pnpm -C e2e e2e:install安装浏览器及其系统依赖。注意由于各 runner 共享端口、认证状态与日志路径同一时间只能运行一个本地pnpm -C e2e e2e*进程。AGENTS.md 给出的完整命令表如下均已对照 e2e/package.json 中的 scripts 逐一核实存在场景命令对已初始化的实例执行场景pnpm -C e2e e2e独立的 WCAG Level A 无障碍扫描pnpm -C e2e e2e:accessibility:a独立的 WCAG Level AA 无障碍扫描pnpm -C e2e e2e:accessibility:aa单页自动化 WCAG 扫描pnpm -C e2e exec tsx ./scripts/run-cucumber.ts --full -- --tags axe and wcag-a and wcag-page-studio按需替换级别与页面标签重置、初始化并运行确定性场景pnpm -C e2e e2e:full准备并运行依赖共享夹具fixture的场景E2E_START_AGENT_BACKEND1 pnpm -C e2e e2e:prepared按标签运行子集pnpm -C e2e e2e -- --tags smoke有头模式调试pnpm -C e2e e2e:headed -- --tags smoke准备并运行外部运行时场景E2E_START_AGENT_BACKEND1 pnpm -C e2e e2e:external仅对现有中间件做 seed不跑 Cucumberpnpm -C e2e seed -- --profile prepared\|external-runtime\|post-merge重置已持久化的 E2E 状态pnpm -C e2e e2e:reset仅构建生产 Web 产物不启动服务pnpm -C e2e e2e:web:build中间件生命周期管理pnpm -C e2e e2e:middleware:up与pnpm -C e2e e2e:middleware:down限定范围的静态检查vp check e2e对照 e2e/package.json可以看到这些命令的真实映射e2e与e2e:full都调用tsx ./scripts/run-cucumber.ts后者附加--fulle2e:prepared调用run-prepared.tse2e:external调用run-external-runtime.tse2e:middleware:up/down、e2e:reset、e2e:web:build全部收敛到tsx ./scripts/setup.ts subcommand而e2e:install实际执行playwright install --with-deps chromium webkitCI 变体e2e:install:ci使用--only-shell进一步缩减体积。此外还有文档未列出的e2e:post-merge入口run-post-merge.ts用于合并后的场景准备。文档还给出三个关键环境变量E2E_FORCE_WEB_BUILD1runner 默认复用web/.next/BUILD_ID即复用已有的 Next.js 构建产物设置该变量可强制重新构建前端E2E_BROWSERwebkit针对特定浏览器做聚焦的跨浏览器运行E2E_SLOW_MO500与有头命令配合用于本地动作级调试的减速。2.1 标签过滤的默认语义cucumber.config.tsCucumber 的配置文件 e2e/cucumber.config.ts 值得完整解读它定义了没有显式--tags时到底跑什么const hasCliTags process.argv.some((arg) arg --tags || arg.startsWith(--tags)) const defaultNonExternalTags not axe and not prepared and not external-model and not external-tool const selectedTags process.env.E2E_CUCUMBER_TAGS || (hasCliTags ? undefined : defaultNonExternalTags) const tags selectedTags ? (${selectedTags}) and not skip : not skip const config { format: [ progress-bar, summary, html:./cucumber-report/report.html, message:./cucumber-report/report.ndjson, ], import: [./tsx-register.js, features/**/*.ts], paths: [features/**/*.feature], tags, timeout: 60_000, }其语义是标签选择存在三级优先级——环境变量E2E_CUCUMBER_TAGS 命令行--tags 默认的defaultNonExternalTags排除axe、prepared、external-model、external-tool四类外部/可选标签无论哪级来源最终都会再叠加and not skip保证被skip标记的场景在所有 runner 配置下都被排除。配置还固定了特性文件路径为features/**/*.feature场景超时 60 秒报告双写为 HTMLcucumber-report/report.html与 Cucumber Messages NDJSONcucumber-report/report.ndjson。3. 运行时所有权模型每个脚本只拥有一件事AGENTS.md 的 Runtime Ownership 一节是本包架构的核心文件所有权职责scripts/setup.tsreset、中间件、后端、前端的启动scripts/run-cucumber.ts唯一的 E2E 运行时编排器服务生命周期、可选的 seed 执行、Cucumber 调用、teardownscripts/seed-runner.ts针对已运行中的运行时创建并验证 fixture永不启动服务support/web-server.ts前端复用、就绪探测与关闭features/support/hooks.ts共享的认证引导、场景生命周期与诊断features/support/world.tsDifyWorld每场景的行为级BrowserContext及其带认证的 setup/cleanup 客户端features/step-definitions/按能力域组织的 gluecommon/仅留给真正跨能力的步骤阅读 run-cucumber.ts 可以印证上述每一条。其main()流程按序完成--full时先resetState()再startMiddleware()run-cucumber.ts#L122-L127若shouldStartManagedAgentBackend()为真先拉起 shellctl sandbox默认端口E2E_SHELLCTL_PORT || 5004健康检查/healthz再拉起 agent backend默认端口E2E_AGENT_BACKEND_PORT || 5050就绪探针/openapi.json均以startLoggedProcess记录日志到.logs/run-cucumber.ts#L132-L161启动 API server就绪探针${apiURL}/health超时 180 秒与 Celery worker——seed 场景下额外限定队列dataset,priority_dataset,workflow_based_app_executionrun-cucumber.ts#L20startWebServer启动/复用前端超时 300 秒执行runSeed(seed)若请求以npx tsx ./node_modules/cucumber/cucumber/bin/cucumber.js --config ./cucumber.config.ts调用 Cucumber--headed通过CUCUMBER_HEADLESS0/1传递finally中按注册顺序执行清理停 Web、停 Celery、停 API、停 agent backend、停 shellctl sandbox、停中间件run-cucumber.ts#L88-L106。两个值得注意的实现细节未预期进程退出即快速失败waitForManagedProcess将waitForUrl与进程exit事件竞速进程未就绪先退出时会抛出附带日志尾部 20 行的错误run-cucumber.ts#L28-L74避免服务挂了却干等超时双通道终止处理注册SIGINT/SIGTERM触发清理并以非零码退出保证 Ctrl-C 也能完成 teardownrun-cucumber.ts#L108-L119。3.1 认证引导、行为门禁与步骤定义约定文档还规定了几条行为层面的契约惰性初始化与认证复用未初始化的实例在 setup 阶段被惰性安装并认证已初始化的实例直接登录并复用认证状态。全量运行--full通过 setup 阶段本身证明 reset 与 bootstrap 的正确性而不是写一个 Gherkin 场景去证明。行为门禁Cucumber 的退出码是行为门禁同时 runner 还要求报告里至少出现一条testCaseStarted消息——run-cucumber.ts#L223-L226 在退出码为 0 后调用assertCucumberScenariosStarted(messages)防止标签选择为空导致 0 个场景却通过。文档明确禁止用场景数基线或跳过场景白名单替代这一门禁。World 隔离浏览器身份与 API 身份保持分离使未认证/登出旅程不会破坏 fixture 的所有权跨 actor 场景为每个 actor 使用独立的BrowserContext与类型化DifyWorld状态确保诊断与清理覆盖所有 actor。TypeScript 写法约束访问 World 状态的步骤定义必须写成async function (this: DifyWorld, ...)因为箭头函数拿不到 Cucumber 绑定的 World 实例。4. 标签体系与外部运行时哪些场景属于哪条流水线AGENTS.md 的 Tags And External Runtime 一节定义了整个 E2E 的选择语义逐条归纳如下标签语义默认场景使用共享的已认证存储状态storage stateunauthenticated创建干净的上下文用于未认证旅程authenticated仅作意图与选择用不改变运行时行为axe独立自动化 WCAG 扫描被默认功能套件与常规 CI 命令排除wcag-a/wcag-aa限定独立的级别化扫描选择任一级别的命令必须同时选择axewcag-page-slug页面选择器挂在 features/accessibility/ 下对应 Examples 块prepared需要 prepared fixturespost-merge seed profile 包含它们external-model/external-tool场景会调用真实外部运行时确定性命令排除这些标签external 命令是显式选择opt-inmicrophone使用签入仓库的假音频 fixture 与隔离的 Chromium 上下文browser-smoke在 Chromium 与 WebKit 两条 CI 通道运行聚焦的键盘与导航覆盖skip将场景临时排除出所有 runner 配置产品行为恢复后应尽快移除禁止用于永久或环境依赖性的屏蔽agent-backend-runtimeAgent v2 运行时场景要求显式的运行时可用性步骤无障碍工作流的定位值得强调文档将其定义为opt-in 的人工审计而非回归门禁。当修改审计工作流、页面矩阵或就绪契约时PR 作者应在合并前自行运行 AA/all 路径——这与package.json中e2e:accessibility默认转发到e2e:accessibility:aa的实现一致。外部运行时的规则对照 run-cucumber.ts 中的 agent backend 启动逻辑Seed 与 Cucumber 必须共享同一个运行时生命周期。组合命令拥有 reset、中间件、服务、seed、Cucumber 与 teardown 全流程CI 不允许在 workflow YAML 中自行复刻这套生命周期。E2E_START_AGENT_BACKEND1会在 API 之前拉起受管的本机 agent backend且与显式的E2E_AGENT_BACKEND_URL/AGENT_BACKEND_BASE_URL互斥feature 自带的服务使用自己的标签agent v2 运行时场景使用agent-backend-runtime并要求显式的运行时可用性步骤。文档末尾是一条纪律性约束不得用运行时标签暗示无关服务也不得在必需 fixture 缺失时静默跳过行为。5. 浏览器、API 与契约边界什么动作必须由浏览器完成Browser, API, And Contract Boundaries 一节回答了 E2E 中最常见的设计问题用户动作必须由被测浏览器执行API 只能准备 fixture、轮询持久化结果与清理不能替代用户的When动作结果断言优先选择用户在浏览器中可观测的结果除非持久化后端状态本身就是被测契约。API 侧的调用规范同样具体对普通 Console JSON 与可表达的 multipart 操作使用场景级或进程级的生成式 oRPC 客户端并开启请求与响应校验直接调用生成的操作。e2e/package.json 的依赖可以印证这条约束的落点orpc/client、orpc/contract、orpc/openapi-client与 workspace 内的dify/contracts包正是生成契约的来源。禁止清单手写端点 URL、复制 DTO/schema、响应强转cast、一对一转发包装器、可变跨场景客户端、TanStack Query 缓存。仅在 helper 真正拥有某项职责时才保留fixture 构建、多操作编排、清理注册表、不变量、最终一致性轮询、收窄的测试视图或协议适配器SSE、二进制下载、仅重定向流程、外部服务与基础设施就绪检查可以集中到真实 owner 下的适配器。校验失败即契约失败应回溯到后端 schema 的 owner必要时更新 api/controllers/API_SCHEMA_GUIDE.md 中的契约、重新生成dify/contracts并让场景对齐产品真正的状态 owner禁止关闭校验或添加兜底 schema 来让 E2E 通过。6. 种子数据、清理与诊断可复现与可归因的工程约定最后一节 Seeds, Cleanup, And Diagnostics 给出五组约定前两组可直接落到具体文件命名通过 support/naming.ts 生成带E2E前缀的一次性资源名确定性上传素材放在fixtures/test-materials/经 support/test-materials.ts 解析——二者在目录中均已确认存在。所有权分层seed 脚本拥有共享的长生命周期 fixture场景拥有其创建的一次性资源且必须注册清理。清理机制已知资源类型使用类型化的DifyWorld清理字段其他生命周期 owner 通过registerCleanup(...)注册注册的回调在类型化清理队列之后按LIFO顺序执行对应实现位于 support/cleanup.ts编排器的 teardown 即调用其中的runCleanupTasks见 run-cucumber.ts#L91-L98。顺序与归因先删子资源与被引用资源再删 owner清理失败要附到报告上不得吞掉。诊断产物失败场景在cucumber-report/artifacts/下生成截图与 HTML 捕获HTML 报告与 Cucumber Messages 报告统一放在cucumber-report/后端与前端的启动日志在.logs/额外 CI 通道各自保留自己的报告与日志目录。这些路径与cucumber.config.ts中html:./cucumber-report/report.html、message:./cucumber-report/report.ndjson的输出配置以及 run-cucumber.ts#L79-L80 中cucumberReportDir/logDir的定义完全对应。7. 小结这套 E2E 体系回答了什么问题把 e2e/AGENTS.md 的契约与仓库实现对照起来看Dify 的仓库级 E2E 体系围绕四个问题给出了一致的答案谁来拥有服务生命周期唯一编排器 scripts/run-cucumber.ts组合命令拥有从 reset 到 teardown 的全流程CI 不复刻生命周期哪些场景在默认流水线上跑cucumber.config.ts 的三级标签优先级 not skip默认排除axe/prepared/external-*四类可选与外部依赖标签浏览器测试与 API 的边界在哪When动作必须发生在浏览器API 只做 fixture 准备、轮询与清理且必须走带校验的生成式 oRPC 客户端校验失败按契约失败处理状态如何可复现、失败如何可归因E2E前缀的一次性命名、LIFO 清理注册、cucumber-report/与.logs/的固定诊断产物加上至少一条testCaseStarted的空跑门禁。对维护者的实际意义是新增一个 E2E 场景时先决定它的标签是否需要preparedfixture、是否触碰真实外部运行时、是否属于unauthenticated旅程再决定When动作走浏览器还是 API最后确认其创建的资源都注册了清理——这三步走完后场景自然落入既有的运行时生命周期与报告契约之中。【免费下载链接】difyBuild Agentic workflows, RAG pipelines, with rich AI model and tool support on one collaborative workspace. Deploy on cloud, VPC, or self-hosted, so teams move from prototype to production without rebuilding the stack.项目地址: https://gitcode.com/GitHub_Trending/di/dify创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进