资讯动态

cloudflare-os 集成测试实战:如何用 Wrangler Test Harness 在 workerd 中驱动真实 Worker(3 个组件 · 6 个坑)

发布时间:2026/10/5 10:15:14 来源:尧图企业网站定制
cloudflare-os 集成测试实战如何用 Wrangler Test Harness 在 workerd 中驱动真实 Worker3 个组件 · 6 个坑【免费下载链接】cloudflare-osAgent workspace built on Cloudflare Workers for creating documents, building apps, and running agents with your company’s context and systems.项目地址: https://gitcode.com/GitHub_Trending/cl/cloudflare-oscloudflare-os 是构建在 Cloudflare Workers 上的 agent workspace。本文讲解它的集成测试架构用 wrangler Test Harness 在 workerd 里启动真实 Worker只 stub 出站 HTTP再经浏览器同款 WebSocket 传输做断言。关键结论先立住被测代码在另一个进程里wrangler 提供的createTestHarness()会在workerd 中把workshop-backend与一个或多个 gatekeeper 作为真实 Worker 启动它们 checked-in 的wrangler.jsonc在内存里被 patch。测试通过 WebSocket 上的 Capn Web 协议与/api通信——和浏览器用的传输完全一致——并向 overseer 提供一个ObserverConfigCallback供其回调。除此之外没有任何东西被 mock。这句话是全文的地基测试进程碰不到 Worker 进程里的变量、时钟和存储只能通过协议与它说话。后面几乎所有反直觉的设计都是这一条推出来的。三个组件总览harness / network-interceptor / rpc-clientharness.ts负责把环境拉起来。输入是 gatekeeper 列表绑定名、包目录、可选 patch 函数输出是一个带url运行中 server 的基地址与fetchWorker()直接向指定 Worker 的 HTTP 入口派发请求host 不被解析因此不需要routes配置的Harness对象。network-interceptor.ts是隔离的机制层。由于createTestHarness会把 Worker 的出站 fetch 路由回 Node 进程patch 掉globalThis.fetch就足够不需要任何拦截库。输入是 handler 链与放行规则输出是逃逸请求的记录。rpc-client.ts是测试驱动产品的操作面用前端同款 API 开 RPC 会话提供注册/登录、轮询等待、observer 提示记录器。ObserverConfigRecorder记录每次configure()调用作为断言面MAX_OBSERVER_PROMPTS 2把 overseer 的提示上限集中成常量避免每个套件里重复魔法数字。组件职责输入输出harness启动真实 Workergatekeeper 列表、配置 patch基地址、fetchWorkernetwork-interceptor拦截出站 HTTPhandler 链、放行规则逃逸记录rpc-client浏览器同款传输驱动基地址RPC 会话、记录器一句话分工harness 管开机interceptor 管断网rpc-client 管操作产品。跟一次集成测试的生命周期从预构建到 teardown预构建先把两个 Worker 的入口摆到同一位置package.json里test:run会先执行test:prebuild把workshop-backend与 fixture gatekeeper 的入口都构建到各自的.wrangler/validate/。global-setup.ts 校验这两个产物存在缺失直接抛错并设置WORKSHOP_INTEGRATION_PREBUILT1——harness 看到它会删掉config.build因为共享构建已完成每个 vitest fork 再重建只会争抢同一目录。harness 启动配置 patch 的三处讲究startHarness()对每个 gatekeeper 的配置做三件事路径钉死inline 配置没有自己的文件路径wrangler 会把相对main相对 harness 的root解析所以main必须改成绝对路径、build.cwd钉到 Worker 自身目录——main由构建生成的 Workercapnweb-validate 产物不这么做输出就会落到错误位置。只加套件要求的 services 绑定GATEKEEPER_binding指向对应 Workerentrypoint 固定GatekeeperVendor这样 vendor 发现不会有意外的一行。本地 secrets 不进测试harness 的root特意取一个不存放 vars 文件的目录。若 inline 配置落在仓库根开发者本地的.dev.vars比如CF_AI_GATEWAY_*会覆盖配置里的同名 vars套件在你机器上和 CI 上行为不一致甚至发出真实 AI 流量。workshop 侧同时不设CF_ACCESS_AUD/api走未认证路径、开放密码注册、把ADMINS设为[admin]、默认删掉worker_loaders绝大多数测试不需要 Gadget 执行。配置校验是刻意宽松的harness.ts 用一个z.looseObjectschema 只校验 harness 自己触碰的字段其余原样透传——wrangler 会在 Worker 启动时对整个文件重新校验。好处是配置损坏时在这里就带字段名报错而不是被强制类型转换后死在某个更隐蔽的角落。RPC 连接与登录像浏览器但不完全像connect(baseUrl)把/api转成 ws/wss 地址开会话。signUp()用 SHA-256 顶替前端的 64 MiB argon2id——server 对这些字节原样存储比较、从不重新推导所以确定性替身足够。waitFor()以 25ms 间隔在 30 秒内轮询专门用于效果只能通过 API 的最终状态观察到的场景比如账号出现在用户列表里。用控制面操纵 fixturefixture gatekeeper 在自身fetch()上挂了一组普通 HTTP 路由/control/verify-outcome、/control/observer-events、/control/fetch-probe等测试用harness.fetchWorker(gatekeeper-test, ...)调它。test-gatekeeper.ts 里addObserver()先问 verifier 谁在请求再查控制状态allow: false时直接throw new Error(reason)——gatekeeper 报告此用户不可观察的方式就是抛错overseer 的失败处理正是围绕这个行为构建的。控制路由对请求体逐字段校验返回指明错在哪个字段的 400。注释说得很直白这不是为了安全而是为了失败模式——不校验的话一个拼错的字段会注册给名为undefined的账号gatekeeper 继续放行本该失败的账号测试在几步之后死于一个与真实原因无关的断言。断言与逃逸检查一次负向证明observer-reverification.test.ts 安装不带任何 handler的 interceptor——本文件不应有任何出站请求发生即失败。它的afterAll里const unmocked interceptor.getUnmockedCalls(); await harness?.server.close(); interceptor.uninstall(); expect(unmocked).toEqual([]);但怎么证明拦截本身有效同文件里/control/fetch-probe让 fixture Worker 对一个未 mock 的地址发起子请求请求被拦截接住以合成 500 返回takeUnmockedCalls()精确取走这一条记录——只取自己的条目、不重置整个列表afterAll仍能抓到并发兄弟测试放跑的逃逸。若 Worker 子请求能绕过被 patch 的globalThis.fetch直奔互联网那所有零逃逸断言都是自证的。teardownreset 不是清盘工具server.close()关闭 server。而server.reset()每次调用约 3 秒——比整个套件跑一遍还久——且会重启 serverserver.url变 undefined所有已打开的 WebSocket RPC 会话以 WebSocket connection failed 死掉。它是 teardown不是可反复使用的存储清空器。为什么这么设计四个反直觉的决策假定时器为什么救不了你直觉做法vi.useFakeTimers()快进时钟让凭证过期。行不通——假定时器 patch 的是测试进程的时钟被测代码读的是 workerd 的时钟跨进程的假时钟对它不可见。isTokenExpired()的 30 秒 skew 在 Worker 内部求值你在测试进程里怎么拨都没用。边界要分清在vitest-pool-workers下测试与被测代码同处一个 isolate假定时器确实可用这条限制只属于跨进程的集成测试。所以时间敏感状态只能靠 fixture 的控制面制造——一次 HTTP 调用把验证结果设成拒绝比拨时钟可靠得多。为什么 overseer 逻辑要用 fixture 而不是真实 gatekeeper直觉做法拿 gatekeeper-google 直接测。但 overseer 的用例需要一个能按命令拒绝 observer的 gatekeeper真实 gatekeeper 的代价太高OAuth 类要先 mock 一整套厂商认证面才能铸出账号Context Library 只在观察被记录之后才拒绝而记录观察需要 Worker Loader、斜杠命令或 AI 聊天快照——它还是单例永远造不出两个绑定同时失败。给真实 Worker 加测试钩子比如标记已观察的方案被考虑过并否决那等于 stub 掉 tracker 自己维护的状态测试变成循环论证。所以 fixture 是一个说着真实协议的真正 Worker验证结果由测试经 HTTP 设定。两条边界它只服务 overseer 逻辑不是 per-vendor 覆盖的替代品它刻意不区分已定型的拒绝与运行性失败凭证过期——两者到达 overseer 时都是抛出的错误这是设计使然overseer 把所有失败都当可修复的只有一个allow旋钮区分靠 reason 字符串承载。为什么存储隔离靠约定而不靠 reset直觉做法测试之间清空存储。实测数据摆在这里server.reset()一次约 3 秒且杀死全部会话。于是存储在整个 harness 生命周期内持续存在任何测试都不得假设干净起点。独立性靠每次取全新身份rpc-client.ts 的nextUsernames()用递增计数器生成alice7、bob7Workshop 要求用户名以字母开头的字母数字串资源 URL 每测试唯一账号标签由 helper 分配而非调用方自选。一个容易踩错的推论没有任何请求逃逸到互联网的断言必须放afterAll而不是afterEach。it.concurrent下某个afterEach触发时兄弟测试还在运行它会检查并清空对方仍在用的状态甚至丢掉一个本该由兄弟背锅的逃逸。为什么两对依赖必须步调一致workerd 没有被直接钉住wrangler 与 miniflare 各自依赖精确版本的 workerd。版本漂移的后果是 harness 启动失败compatibility date 报错——锁文件里装出了第二套运行时栈。pnpm-workspace.yaml 的做法是catalog 里把miniflare精确钉到与 wrangler 配套的预发布版overrides再把cloudflare/vitest-pool-workers所依赖的 wrangler/miniflare 指向同一 catalog。升级 wrangler 而不动 miniflare等于装了两套栈。capnweb 双副本是另一类边界问题消费方仓库会安装自己的工作区和public/子模块的工作区两个 pnpm store 各自解析一份capnweb。stub 只能由拥有会话的那个实例序列化混用就是TypeError: Cannot serialize value: [object RpcStub]坑在于开发机单次pnpm install会把两份去重合并永远看不到这个错CI 分开执行两次 install它才首次出现。所以 toolkit 独占 capnweb 边界回调 stub 一律用stubFor()铸造RpcStub只作类型导入编译期擦除无碍。本仓库用 lint 规则在结构上强制了这一点——包内对capnweb的值导入只允许出现在rpc-client.ts。顺带一个更小的坑workerd 把入口模块的每个具名导出都当 entrypoint导出一个普通字符串常量会得到Incorrect type for map entry THING_URL_PATTERN: the provided value is not of type function or ExportedHandler.这正是 test-gatekeeper.ts 顶部注释Nothing but classes and the default handler may be exported的由来其余值必须保持模块私有。复刻指南为新 gatekeeper 写一套集成套件的 3 步无论在本仓库内还是消费方仓库内新增套件形态相同且不需要 fork 任何 harness 代码新建 handler 模块如google-handlers.ts实现Handler签名mock 厂商的 token 端点与 API 端点返回真实形状的响应。注意约束handler 只有在决定自己拥有该 URL 之后才可读取request——先消费 body 再返回 null会破坏后续 handler 对同一流的读取。把 harness 指向该包startHarness({ gatekeepers: [{ binding: GOOGLE, dir: ../gatekeeper-google }] })必要时用patch设置测试依赖的 vars。不碰生产代码真实 gatekeeper 原样运行厂商外部面全部由 interceptor 的 handler 模块 mock。消费方仓库把本仓库作为public/子模块 vendor、以工作区依赖消费public/packages/integration-tests还需守两条铁律回调 stub 一律经stubFor()禁止值导入RpcStub类型导入可以。零逃逸断言放afterAll不是afterEach。集成测试避坑速查表现象 / 原因 / 正确做法现象原因正确做法用例互相污染存储跨测试持久存在nextUsernames()全新身份 每测试唯一资源 URLreset 后全部会话断开、白等 3 秒server.reset()是 teardown 而非清盘隔离靠全新身份reset 只当 teardown并发下逃逸断言误判、丢证据afterEach触发时兄弟测试仍在运行放afterAllgetUnmockedCalls()一次性断言CI 报Cannot serialize value: [object RpcStub]capnweb 双副本stub 跨实例一律stubFor()类型导入无碍harness 启动报 compatibility date 错误wrangler/miniflare 漂移装出第二套 workerd 栈随 catalog/overrides 步调一致升级Unmocked outbound request抛错出站请求没被任何 handler 接住这是隔离保证本身让它失败而非触网收束这套集成测试架构的本质只有一句话代码在另一个进程里测试用真实传输驱动它唯一被 stub 的是出站 HTTP。假定时器、存储 reset、真实 gatekeeper 三个直觉选项都被这个前提逐一否掉换来了 fixture 控制面、身份级隔离与组件间的版本/边界耦合。参数化正是它的扩展点本仓库的套件验证 overseer 逻辑消费方仓库的 per-vendor 套件验证真实 gatekeeper 的端到端行为两者共享同一套基础设施谁也不用 fork 任何东西。【免费下载链接】cloudflare-osAgent workspace built on Cloudflare Workers for creating documents, building apps, and running agents with your company’s context and systems.项目地址: https://gitcode.com/GitHub_Trending/cl/cloudflare-os创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑