ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

cloudflare-os 集成测试实战:用 wrangler Test Harness 在 workerd 中驱动真实 Worker 的端到端测试架构

2026/10/4 15:07:13 拓冰建站 浏览量
cloudflare-os 集成测试实战:用 wrangler Test Harness 在 workerd 中驱动真实 Worker 的端到端测试架构 人工智能AI 应用AI AgentAgent 沙箱AI 安全治理【免费下载链接】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点击查看免费下载导读cloudflare-osAgent workspace built on Cloudflare Workers的packages/integration-tests套件回答了一个困难问题如何在测试里同时运行真实的workshop-backend与真实的 gatekeeper Worker让被测代码跑在 workerd 的另一进程里却只 stub 掉出站 HTTP本文以 docs/integration-testing.md 为主线结合 harness.ts、rpc-client.ts、network-interceptor.ts 与 fixture gatekeeper 等源码实现完整讲解这套套件的工作方式、六个关键设计决策假定时器为何不可用、为何用 fixture 而非真实 gatekeeper、存储隔离为何靠约定等以及消费者仓库如何在不 fork 任何代码的前提下扩展出属于自己的 per-vendor 套件。读完你可以直接复刻这套架构为任何新的 gatekeeper 或新的消费方写出同样形态的集成测试。两种测试套件本仓库的与消费方仓库的原文档开篇就给出了这套测试体系最重要的坐标系存在两种形态的套件它们的职责边界必须分清。本仓库的packages/integration-tests消费方仓库的 per-vendor 套件运行方式pnpm testCI 常规测试任务的一部分自己的 CI 步骤Gatekeeper一个 fixture Worker其验证结果由测试控制一个真实厂商 gatekeeper未经任何修改覆盖范围overseer监督者的 observer 逻辑真实过期的凭证端到端负责内容harness、interceptor、RPC client该厂商的 handlers 与 token 铸造所谓“消费方仓库”consumer repo是指把本仓库作为public/子模块 vendor 进来、并在自己的pnpm-workspace.yaml中以工作区依赖方式消费 toolkitpublic/packages/integration-tests的仓库。需要特别强调当前仓库里并不存在这种 per-vendor 套件也没有任何东西依赖它存在。第二列之所以仍被详细描述是因为 toolkit 的整个参数化设计就是为它准备的——harness 接受 gatekeeper 列表interceptor 接受可插拔的 handler 模块正是为了让套件可以在本仓库之外添加而无需 fork 二者。因此文档中凡是描述 per-vendor 套件的内容都应视为“这种形态的工作示例”——一个未被修改的真实 gatekeeper 运行在它厂商的 mocked 端点之上——而不是你会在本仓库里找到的东西。这些测试到底是什么wrangler的createTestHarness()会在workerd 中把workshop-backend和一个或多个 gatekeeper 作为真实 Worker 启动它们的 checked-inwrangler.jsonc会在内存中被 patch。测试通过 WebSocket 上的 Capn Web 协议与/api通信——与浏览器使用的传输层完全一致——并提供一个ObserverConfigCallback供 overseer 回调。关键事实除了出站 HTTP 之外没有任何东西被 stub。由此导出的最重要推论值得内化被测代码运行在另一个进程里。文档后半部分的几乎所有设计决策都由这一事实推导而来。从源码看 harness 的启动细节harness.ts 的startHarness()是核心入口它做了三件事读取并 patch 每个 gatekeeper 的 wrangler 配置。readWorkerConfig()使用jsonc-parser解析 checked-in 的wrangler.jsonc并用一个刻意宽松的z.looseObjectschemaWORKER_CONFIG只校验 harness 关心的字段name、main、services、vars等。注释解释了这种“宽松”是有意为之其余字段原样透传由 wrangler 在 Worker 启动时对整个文件重新校验schema 只保护 harness 自身触碰的字段这样配置一旦损坏会在这里带着字段名报错而不是被强制类型转换后在某处更隐蔽地失败。把相对main改为绝对路径、固定build.cwd。因为 inline 配置没有自己的文件路径wrangler 会把相对main相对 harness 的root解析对于main由构建生成的 Worker如 capnweb-validate 产物必须把cwd钉到其自身目录否则输出会落到错误位置——run-dev-server.ts出于同样原因也这样做。清空本地 dev secrets。config.secrets { required: [] }确保.dev.vars/.env中的开发者本地设置如CF_AI_GATEWAY_*不会泄漏进测试——否则套件在个人机器与 CI 上行为会不一致甚至可能发出真实 AI 流量。workshopConfig()还做两件重要调整其一只为套件请求的 gatekeeper 添加services绑定GATEKEEPER_binding→ 对应 Workerentrypoint 固定为GatekeeperVendor这样buildGatekeeperVendorMap()只会发现这些 vendorobserver 配置提示不会出现意外行其二不设置CF_ACCESS_AUD让/api走未认证路径、开放密码注册并把ADMINS设为[admin]。绝大多数集成测试不需要 Gadget 执行因此默认删除worker_loaders只有显式开启enableGadgetExecution时才保留 loader。启动后返回的Harness对象暴露url运行中 server 的基地址和fetchWorker(name, ...)——后者直接向指定 Worker 自身的 HTTP entrypoint 派发请求host 永远不会被解析请求直达该 Worker因此无需routes配置但路径仍需匹配该 Worker 的预期。塑造这套设计的六个关键发现以下六条是原文档以及对应源码注释反复强调的、决定套件形态的根本性发现。1. 假定时器在这里不可用vi.useFakeTimers()patch 的是测试进程的时钟而被测代码读取的是workerd 的时钟——跨进程假时钟对它不可见。比如isTokenExpired()的 30 秒 skew 位于gatekeeper-shared内部在 Worker 内求值测试进程里的假定时器根本无法影响它。需要澄清边界在vitest-pool-workers下测试运行在与被测代码同一个 isolate内此时假定时器确实可用。所以这条限制只针对跨进程的集成测试不适用于 in-isolate 单元测试。2. 用 fixture gatekeeper 测 overseer 自己的逻辑而非真实 gatekeeperoverseer 的测试用例需要这样一个 gatekeeper能够按命令拒绝一个 observer。每个现役公开 gatekeeper 都做不到这一点或者说代价会主导整个测试OAuth 类 gatekeeper在账号存在之前就需要 mock 一整套厂商认证面Context Library只有在观察已被记录之后才会拒绝——那需要一次 gadget 读会话因此需要 Worker Loader、一次斜杠命令调用或一次 AI 聊天目录快照而且它是单例永远无法产生某些用例需要的“两个同时失败的绑定”。给这些 Worker 加测试钩子如“标记已观察”的方案被考虑过并否决了那会 stub 掉 tracker 维护的状态本身使测试变成循环论证。因此 fixtures/gatekeeper-test/ 是一个说着真实协议的真正 Worker其验证结果由测试通过 HTTP 控制路由设定。但它只服务于 overseer 逻辑的测试绝不是 per-vendor 覆盖的长期替代品。测试真实 gatekeeper 才是预期演进方向——这正是 harness 接受 gatekeeper列表、interceptor 接受可插拔handler 模块的原因未来一个gatekeeper-google套件就是“添加google-handlers.ts把 harness 指向那个包”——与消费方仓库 per-vendor 套件完全同形生产代码零修改。fixture 的设计细节test-gatekeeper.ts 内部结构清晰值得展开TestControlDurableObject持有全部控制状态。setVerifyOutcome(label, outcome, resourceUrl?)把“验证结果”写入 KV——按账号标签为键outcome:${label}或outcome:${label}:${resourceUrl}资源级结果优先于账号级。getVerifyOutcome()默认放行{ allow: true }保证协作者第一次打开必然能成功。此外还记录 observer 事件日志recordObserverEvent、环境验证计数recordAmbientVerification与测试动作状态机stageAction/applyAction/discardAction。GatekeeperVendorWorkerEntrypoint实现真实 vendor 协议。describe()返回autoProvisionsAccount: truecreateAccount()每次调用铸造一个全新账号test-${uuid}gadgets-test.example这正是“测试聚焦 overseer 而非 OAuth 舞会”的手段。connectAccount()按要求抛错——auto-provision 意味着 Workshop 永远不会发起 connect 流程。TestAccount实现GatekeeperUser。getVerifier()返回一个TestVerifieridentify()返回账号标签getGatekeeperClassFor(url)校验 URL 必须是https://gadgets-test.example/things/*否则抛错。TestGatekeeperDurableObject每个绑定资源一个核心是addObserver()——先通过 verifier 问“谁在请求”再查控制状态allow: false时直接throw new Error(reason)。抛错就是 gatekeeper 报告“此用户不可观察”的方式也是 overseer 失败处理围绕的核心行为。readValue()通过approvalQueue.authorizeObservation()走真实审批流后返回固定值 42。HTTP 控制面Worker 的fetch()上的一组普通 HTTP 路由/control/verify-outcome、/control/observer-events、/control/ambient-verification-count、/control/action-state、/control/submit-external-message、/control/external-gadget-id、/control/fetch-probe测试用harness.fetchWorker(gatekeeper-test, ...)调用。请求体被逐字段校验badRequest()返回指明哪个字段错了的 400——注释说得很清楚这不是为了安全调用者只有本包内的 helper而是为了失败模式——一个拼错的字段会给名为undefined的账号注册结果gatekeeper 继续放行本该失败的账号测试在若干步之后死于一个与真实原因毫不相干的断言。fixture 还刻意不建模“已定型的拒绝”你不可读此数据与“运行性失败”凭证过期的区别两者到达 overseer 时完全一样都是抛出的错误overseer 无法区分——这是设计使然因为它把所有失败都视为可修复的。所以这里只有一个控制旋钮allow而区分由 reason 字符串承载测试通过选择不同的 reason 文本来演练两种叙事。3. 存储隔离靠约定因为替代方案更糟server.reset()存在但实测数据一锤定音每次调用约 3 秒比整个套件跑一遍还久。它还会重启 server——server.url变为 undefined所有已打开的 WebSocket RPC 会话都会以 WebSocket connection failed 死掉。它不是可在测试之间使用的存储清空工具而是一个 teardown。于是存储在整个 harness 生命周期内持续存在任何测试都不得假设干净的起点。测试通过“每次取全新身份”保持独立rpc-client.ts 的nextUsernames()递增计数器生成alice7、bob7这类用户名注释提醒Workshop 要求用户名以字母开头且为字母数字因此前缀也必须如此资源 URL 每测试唯一账号标签由 connect/provision helper 分配而非调用方自选。一个容易踩错的推论“没有任何请求逃逸到互联网”的断言必须放在afterAll而不是afterEach。在it.concurrent下某个afterEach触发时兄弟姐妹仍在运行它会检查并清空它们还在使用的状态——甚至可能丢掉一个本该由某个兄弟测试背锅的逃逸请求。observer-reverification.test.ts 的afterAll正是这么做的interceptor.getUnmockedCalls()之后才server.close()与uninstall()再断言逃逸列表为空。4. wrangler 与 workerd 版本必须步调一致本仓库通过根级overrides条目把workerd钉死到单一版本从而把所有传递依赖对它的请求折叠到同一版本。更新的wrangler会带来更新的miniflare后者要求比 override 所给更新的workerd——harness 于是启动失败The Workers runtime failed to start ... requires compatibility date 2026-07-08, but the newest date supported by this server binary is 2026-06-30.因此本仓库把wrangler钉在~4.104.0——该版本捆绑的workerd与 override 匹配当前工作区实际已演进到 catalog 的^4.128.0见 pnpm-workspace.yaml 及其overrides块中对cloudflare/vitest-pool-workersminiflare与wrangler的联动固定。升级 wrangler 意味着必须同步升级 override二者不能各自为政。5. 消费方仓库可能同时存在两份 capnweb消费方仓库会安装自己的工作区和public/子模块的工作区作为两个独立的 pnpm store。于是capnweb会解析出两个不同副本toolkit 的rpc-client拿到子模块那份而从消费方自己包导入capnweb的代码拿到另一份。stub 只能由拥有会话的那个实例序列化混用必然失败TypeError: Cannot serialize value: [object RpcStub]陷阱在于开发机上单次pnpm install会把两份去重合并不会暴露此问题。它首次出现在 CI——CI 分别执行pnpm install与pnpm --dir public install。要在本地复现请照做。因此 toolkit独占 capnweb 边界回调 stub 一律用 rpc-client.ts 的stubFor()铸造绝不要直接导入RpcStub的值。把RpcStub当类型导入没问题类型在编译期擦除。本仓库在结构上强制执行了这一点——vite.config.ts里的 lint 规则把本包内capnweb的值导入限制到rpc-client.tsallowTypeImports放行类型导入。没有 linter 的消费方仓库应把这条规则当作约定测试文件统一经由stubFor()。6. Worker 入口模块只能导出类与默认 handlerworkerd 把入口模块的每个具名导出都当作一个 entrypoint。从 fixture 导出一个普通字符串常量会得到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”的来源。网络拦截器唯一被 stub 的东西network-interceptor.ts 是整个隔离保证的机制层。原理简洁createTestHarness会把 Worker 的出站fetch()路由回 Node 进程所以只需 patchglobalThis.fetch无需任何拦截库。行为规则默认放行 loopbacklocalhost/127.0.0.1/[::1]让测试客户端直连 harness安全敏感场景可关闭allowLoopback让模型作者发起的请求也走 handler 链从而无法触达宿主服务。对已放行的外部请求会强制设置accept-encoding: identity——因为 Node 的 fetch 解压了压缩体却原样转发其Content-Encodingworkerd 会对明文再次解压“Gzip decompression failed”就是这么杀死本地 eval 目标里每条 Anthropic 流的。未被任何 handler 接住的请求会记录并抛错Unmocked outbound request: ...——未 mock 的调用失败测试而不是悄悄触网。handler 是纯函数签名(url, method, headers, request) Response | null返回 null 表示“不接让下一个 handler 试”返回 Response 即接管。约束微妙handler 只有在决定自己拥有该 URL 之后才可读取request——先消费 body 再返回 null 会破坏后续 handler 对同一流的读取。handler 可以是 async 的——有些需要等待测试在 Worker 发起请求后决定返回什么。NetworkInterceptor自身有完整单元测试network-interceptor.test.ts覆盖 loopback 透传、handler 顺序、allow放行、parked handler、记录/复位/定向提取takeUnmockedCalls()等行为。逃逸断言的正确形态observer-reverification.test.ts的describe(harness)里有一个精妙的“负向证明”——/control/fetch-probe让 fixture Worker 对https://example.com/definitely-not-mocked发起真实子请求。若 Worker 子请求绕过了被 patch 的globalThis.fetch直奔互联网其它所有“零逃逸”断言也同样成立它们根本测不到。probe 证明拦截确实接住了它请求变成合成 500harness 代理出站请求代理侧失败以 500 形式返回而非在 Worker 内 reject且takeUnmockedCalls(target)精确取回了这条记录——只取自己的条目不 reset 整个列表这样afterAll仍能抓到并发的兄弟测试放跑的逃逸。RPC client以浏览器同款传输驱动 Workshoprpc-client.ts 让测试像浏览器一样说话connect(baseUrl)把/api转成 ws/wss URL用 capnweb 的newWebSocketRpcSessionPublicApi()开一个 RPC 会话。signUp/logIn用确定性哈希代替前端 64 MiB 的 argon2id——passwordHashFor()直接 SHA-256因为 server 对这些字节原样存储比较、从不重新推导。waitFor()30 秒内以 25ms 间隔轮询用于“效果只能通过 API 的最终状态观察”的场景如账号出现在用户列表。listConnectedAccounts()通过驱动subscribeConnectedAccounts()到其ready()调用把增量 add/remove 事件收集成快照用using声明式资源管理按逆序释放先退订再释放原始 stub并覆盖订阅调用自身抛错的路径。accountLabel()镜像 overseer#describeObserverFailures中的优先级uniqueName || displayName || account N保证测试断言的消息与用户读到的一致。ObserverConfigRecorder实现ObserverConfigCallback记录每次configure()调用calls数组即断言面并从脚本化队列应答。alwaysChoose(accountId, times)的times必须显式——队列空时configure()会抛错多出来的意外提示应当失败而不是被静默应答MAX_OBSERVER_PROMPTS 2overseer 的MAX_CONFIG_REPROMPTS为 1即初始提示 至多一次重提示作为常量集中断言避免每个套件里重复出现魔法数字。测试编排全局预构建与 watch 重建套件能跑起来还依赖两处编排细节test:prebuildpackage.json中test:run先执行pnpm run test:prebuildvp run -F gadgets/integration-tests build:test-gatekeeper把workshop-backend与 fixture gatekeeper 的main都先构建到.wrangler/validate/。wrangler.jsonc的main指向.wrangler/validate/src/test-gatekeeper.ts注释说明build:test-gatekeeper应用与生产 gatekeeper 相同的 RPC 校验。global-setup.ts验证两个预构建产物存在缺失即抛错设置WORKSHOP_INTEGRATION_PREBUILT1——harness 看到它就会删除config.build因为共享的.wrangler/validate构建已完成每个 fork 里重建会争抢该目录。watch 模式下测试重跑前会等待当前 run 结束再重建避免覆盖仍在启动的 Worker 正在读取的文件被删除的 Worker 输入文件也通过onFileChange路径触发重跑。vitest.config.ts 配置了globalSetup、forceRerunTriggers覆盖 Worker 输入源文件以及 120 秒的testTimeout/hookTimeout——workerd 启动、真实 RPC 往返都需要这个量级的余量。如何运行在仓库根目录pnpm test # CI 常规测试任务中的常规路径若只想跑集成测试包pnpm --filter gadgets/integration-tests run test:run # 预构建 vitest run pnpm --filter gadgets/integration-tests run test:watch # 预构建 监听模式test:watch会先执行一次预构建之后由global-setup.ts在每次重跑前重建被改动的 Worker 输入。注意vitest.config.ts的include只覆盖__tests__/**/*.test.ts套件文件全部平铺在该目录下如 observer-reverification.test.ts、workshop-sharing.test.ts、sensitive-observations.test.ts 等每个文件在自己的进程里共享一个 harness文件内用例用it.concurrent并行。端到端示例observer 重新验证回归测试observer-reverification.test.ts 是整套设计的浓缩演示。它回归验证一个真实 bug协作者的 observer 账号选择在首次成功打开后被持久化此后每次打开ensureObserver()都无提示地重新验证当验证失败凭证过期很常见时打开曾经会死在 “You are not permitted to observe all of the data this Gadget has accessed” 且无路可走。修复后应通过ObserverConfigCallback携带失败信息重新提示若重提示仍未解决则指明是哪条连接、哪个账号失败。测试流的骨架beforeAll安装无 handler的NetworkInterceptor本文件不应有任何出站请求每一个都是失败再startTestGatekeeperHarness()。每个用例用nextUsernames(alice, bob)铸新身份、signUp、provisionAccountfixture 无认证流一次 RPC 即铸造、newGadget()newGatekeeper()绑定资源、addCollaborator(bob, build)分享给 Bob。通过harness.fetchWorker调/control/verify-outcome把 Bob 的验证结果设为拒绝EXPIRED_REASON credentials expired — please reconnect模拟凭证过期DENIED_REASON模拟定型拒绝。断言ObserverConfigRecorder记录的configure()调用次数、need.failure.accountId、need.failure.reason以及最终错误消息是否包含绑定名“Test Thing named”、账号标签与原因——并且每个失败绑定一行split(\n).filter(...)断言行数。其中两个用例特别值得注意“一次重提示中报告所有失败绑定而非只报第一个”两个绑定同时失败时旧代码只保留第一个错误、丢掉其余第二个失败连接不可见。用例断言第二次calls[0]长度 2、两个failure.accountId都指向 Bob终端消息同时点名Test Thing multi-a与Test Thing multi-b。“另一个绑定终局失败时保留已修复的既有注册”重提示的 responder在两次验证 pass 之间精确执行修复 a、同时让 b 失效断言 a 的注册没有产生任何remove事件回滚只撤销本次调用新注册的 observer修复既有的持久化注册不得被重新归类为新添加——有 bug 的回滚恰好会多发一次 remove而两次成功验证留下恰好两条add。为新的 gatekeeper 添加套件无论在本仓库内还是消费方仓库内新增一个 gatekeeper 的集成套件都是同一形态且不需要 fork 任何 harness 代码新建 handler 模块如google-handlers.ts实现Handler签名mock 该厂商的各个端点token 端点、API 端点返回真实形状的响应。把 harness 指向该 gatekeeper 包startHarness({ gatekeepers: [{ binding: GOOGLE, dir: ../gatekeeper-google }] })必要时用GatekeeperSpec.patch调整其配置如设置测试依赖的vars。不要碰生产代码真实 gatekeeper 原样运行厂商外部面全部由 interceptor 的 handler 模块 mock——这正是文档第一张表“第二列”形态的落地。消费方仓库还需额外遵守两条本仓库已内建为结构的约定回调 stub 一律经stubFor()不要值导入RpcStub以及测试文件的“零逃逸”断言放在afterAll而非afterEach。常见陷阱速查陷阱现象正确做法测试之间共享持久存储用例互相污染用nextUsernames()、每测试唯一资源 URL、helper 分配账号标签绝不假设干净起点server.reset()当存储清空用每次约 3 秒且所有会话断开只当 teardown隔离靠全新身份逃逸断言放afterEach并发下误判/丢证据放afterAll用getUnmockedCalls()一次断言值导入RpcStubCI 报Cannot serialize value: [object RpcStub]一律stubFor()类型导入没问题升级 wrangler 不升 overrideharness 启动失败compatibility date 报错二者步调一致升级入口模块导出非类/非默认 handlerIncorrect type for map entry ...其它值保持模块私有用假定时器控制跨进程行为完全无效用 fixture 控制面如/control/verify-outcome制造时间敏感状态未 mock 的出站请求Unmocked outbound request抛错这正是隔离保证失败而非触网小结这套集成测试体系的本质一句话代码在另一个进程里测试通过真实传输协议驱动它唯一被 stub 的是出站 HTTP。由此导出的六条设计约束——假定时器不可用、fixture gatekeeper 承担 overseer 逻辑、存储隔离靠约定、版本步调一致、capnweb 边界独占、入口导出受限——共同塑造了 harness、interceptor 与 RPC client 的参数化形态。而这种参数化正是它的扩展点本仓库的套件验证 overseer 逻辑消费方仓库的 per-vendor 套件验证真实 gatekeeper 的端到端行为两者共享同一套基础设施都不需要 fork 任何东西。赞分享人工智能AI 应用AI AgentAgent 沙箱AI 安全治理【免费下载链接】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点击查看免费下载相关推荐微信QQ防撤回补丁教程4步装好补丁让对方撤回无效微信QQ防撤回补丁教程4步装好补丁让对方撤回无效 晚上十一点四十客户在群里发来发票金额没错按这个走几秒后这条消息变成一行灰字。第二天早上你追问时桌面应用即时通讯Rook 集成测试框架Rook Test Framework实战指南在 Kubernetes 上运行端到端测试Rook 集成测试框架Rook Test Framework实战指南在 Kubernetes 上运行端到端测试 本篇指南聚焦 Rook 存储编排项目的集成云原生存储容器编排运维基于 wrangler createTestHarness 的 Cloudflare Workers 端到端集成测试integration-tests 包设计与实战解析基于 wrangler createTestHarness 的 Cloudflare Workers 端到端集成测试integration tests 包设计人工智能AI 应用AI AgentAgent 沙箱AI 安全治理上一篇实测 XInputTest全面解锁游戏手柄延迟测试一眼看穿轮询率告别操作卡顿下一篇想要在Windows上流畅看B站这款第三方UWP客户端值得一试创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考