尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Kilo opencode 包 Effect 测试迁移指南:统一 testEffect 测试模式

Kilo opencode 包 Effect 测试迁移指南:统一 testEffect 测试模式 Kilo opencode 包 Effect 测试迁移指南统一 testEffect 测试模式【免费下载链接】kilocodeKilo is the all-in-one agentic engineering platform. Build, ship, and iterate faster with the most popular open source coding agent.项目地址: https://gitcode.com/GitHub_Trending/ki/kilocode导读Kilo 仓库中的packages/opencode包正逐步把测试从 Promise 世界test(..., async () ...)Effect.runPromise迁移到共享的testEffect模式每个被测服务在文件顶部声明一个本地 runner用it.effect/it.instance/it.live三种形态覆盖纯逻辑、实例相关与真实环境三类测试。本文以 EFFECT_TEST_MIGRATION.md 为主线结合 effect.ts、fixture.ts、test/fake/*等源码完整讲解目标模式、runner 选择、Layer 与 Fixture 规则、待移除的反模式以及一条可落地的 9 步转换配方帮助你安全、可验证地完成任意 Effect 服务测试的迁移。迁移背景为什么要把测试搬出 Promise 世界仓库中的 Effect 服务Session、Agent、Provider、Account、Auth 等都以Effect作为方法返回值并通过Layer组合依赖。早期测试习惯用test(..., async () Effect.runPromise(...))直接运行 Effect但这带来几个结构性问题测试跑在最外层无法复用服务的依赖注入每个测试都要自己写run(...)、load(...)、svc(...)之类的本地包装器只为把一个 layer 喂进去Promise 与 Effect 边界混乱await using tmp await tmpdir(...)、Promise.withResolvers、Bun.sleep、setTimeout等同步手段混在 Effect 断言里测试既不幂等也难以控制并发失败路径不可见用try/catch包住 Effect 失败丢失了Exit/Cause的结构化信息。配套的 specs/effect/migration.md 描述了同一轮重构在源码侧的目标形态服务方法返回Effect、用Effect.fn(Domain.method)命名、预期失败走类型化错误通道、defaultLayer只接生产依赖而测试使用开放 layer 替换依赖。本文档正是这套迁移在测试侧的落地规范。目标模式每个测试文件一个本地 runner迁移后每个测试文件在顶部只声明一次 runnerconst it testEffect(layer)之后所有用例都通过这个 runner 的三种方法书写。testEffect由 test/lib/effect.ts 导出其核心实现是const testEnv Layer.mergeAll(TestConsole.layer, TestClock.layer()) const liveEnv TestConsole.layer export const testEffect R, E(layer: Layer.LayerR, E) makeR, E(Layer.provideMerge(layer, testEnv), Layer.provideMerge(layer, liveEnv))也就是说testEffect(layer)返回的 runner 内置了两套环境it.effect使用testEnv注入TestClock虚拟时钟与TestConsole捕获输出让基于时钟/控制台的纯 Effect 行为可确定性测试it.live使用liveEnv保留真实时钟只保留TestConsole用于捕获输出其余依赖全部来自你传入的 layer。make(...)内部把body(value)包进Effect.scoped后provide(layer)再以Effect.exit捕获结果失败时会把Cause.prettyErrors(exit.cause)逐条Effect.logError打印出来最后Effect.runPromise仅在文件最外层执行一次这正是 Promise 只在非 Effect 边界 的具体体现。三种 runner 的语义与选择runner环境适用场景it.effect(name, body)TestClockTestConsole 被测 layer纯 Effect 行为状态机、错误通道、时钟相关逻辑、控制台输出it.instance(name, body, options?)真实环境 一个 scoped 的临时 opencode 实例需要单实例上下文目录隔离、实例切换、实例生命周期it.live(name, body)真实时钟 TestConsole 被测 layer真实时间、文件系统 mtime、子进程、git、锁、服务器、watcher、OS 行为文档明确给出判断依据大多数集成式测试使用it.live(...)或it.instance(...)只有纯服务逻辑才用it.effect(...)。以 instance-state.test.ts 为参照一个典型的 runner 声明是const it testEffect(Layer.mergeAll(LayerNode.compile(CrossSpawnSpawner.node), testInstanceStoreLayer))该文件全部用例都用it.live(...)书写因为InstanceState的行为依赖真实临时目录、git 仓库、并发 fiber 与Deferred唤醒无法用虚拟时钟模拟。其中 InstanceState caches values per directory 用例展示了标准写法it.live(InstanceState caches values per directory, () Effect.gen(function* () { const dir yield* tmpdirScoped() let n 0 const state yield* InstanceState.make(() Effect.sync(() ({ n: n }))) const a yield* access(state, dir) const b yield* access(state, dir) expect(a).toBe(b) expect(n).toBe(1) }), )三个 runner 的扩展变体make(...)还生成了.only与.skip变体it.effect.only、it.live.skip、it.instance.only等分别映射到 bun:test 的test.only/test.skip便于聚焦调试单个用例。it.instance额外接受实例选项并通过withTmpdirInstance(options)包装 bodyconst instance (name, value, options?, opts?) test(name, () run(body(value).pipe(withTmpdirInstance(args.instanceOptions)), liveLayer), args.testOptions)withTmpdirInstance会先tmpdirScoped(options)创建临时目录再同时提供TestInstance服务携带directory字段与实例上下文最后补上testInstanceStoreLayer和AppNodeBuilder编译的 spawner 节点见 fixture.ts。InstanceOptions支持三个可选字段git?: boolean—— 是否在临时目录执行git init并配置测试身份与一个空根提交config?: PartialConfigV1.Info | (() PartialConfigV1.Info)—— 写入opencode.json测试配置init?: (directory) Effect.Effectvoid—— 目录就绪后的额外初始化 Effect。Layer 规则开放 layer 组合不要事后覆盖迁移文档对 Layer 组合提出明确约束用开放的服务 layer 组合测试。需要替换某个依赖时把替换层作为 layer 的一部分在 runner 声明处组合好禁止使用封闭的defaultLayer后再覆盖其内部依赖——依赖一旦被提供就无法在之后安全替换优先使用test/fake/*下的小型可复用假边界层。文档给出的现成清单均已在仓库实现AuthTest.empty // test/fake/auth.ts AccountTest.empty // test/fake/account.ts NpmTest.noop // test/fake/npm.ts SkillTest.empty // test/fake/skill.ts ProviderTest.fake().layer // test/fake/provider.ts局部 stub 使用Layer.mock。以 fake/auth.ts 为例export const empty Layer.mock(Auth.Service)({ all: () Effect.succeed({}), })Layer.mock的好处是未实现的成员方法在测试意外调用时会大声失败die而不是静默返回错误结果。不要过早抽象。在多个文件的本地组合重复出现之前不要引入通用的测试 layer 构造器builder。test/fake/provider.ts中的ProviderTest.fake()展示了假层的完整形态返回{ model, info, layer }其中layer用Layer.succeed(Provider.Service, ...)提供完整实现对于未被override覆盖的方法如getLanguage直接Effect.die同样遵循 未配置即大声失败 原则。Fixture 规则使用 Effect 感知的 fixturefixture.ts 提供了一组与 Effect 生态对齐的 fixture迁移时按需取用TestInstance—— 在it.instance(...)内部使用指向当前 scoped 临时实例其directory字段可直接断言如expect(test.directory).toContain(opencode-test-)。它的实现是一个Context.Servicetest/Instance由withTmpdirInstance提供tmpdirScoped(...)—— 在Effect.gen内部yield*随 Effect scope 关闭自动清理addFinalizer中会先释放该目录的实例与 watcher、停止 git fsmonitor 守护进程再删除目录支持git、config、init三个选项。注意它与遗留的 Promise 版tmpdir(...)的清理逻辑保持同步源码注释明确要求 Make sure these stay in syncprovideInstance(dir)(effect)—— 当单个测试需要在多个实例上下文之间切换时使用等价于InstanceStore.Service.use((store) store.provide({ directory: dir }, self))provideTmpdirInstance((dir) effect, options)—— live 测试需要自定义实例初始化或多种实例作用域时使用它内部组合tmpdirScopedprovideInstance并预先提供testInstanceStoreLayerdisposeAllInstances()—— 仅用于有意触碰共享实例注册表的集成测试放在afterEach中。Effect 侧等价物是disposeAllInstancesEffect。除此之外迁移文档还有两条纪律避免可变全局状态。若迁移过程中不可避免要改动全局必须用 acquire/release 包裹作用域并视为临时方案长期目标当行为可以用服务建模时不要开关process.env、Global.Path或可变 flag。优先使用RuntimeFlags.layer(...)之类的层或聚焦的假服务。待移除的反模式清单迁移的目标是删除以下反模式全部来自 EFFECT_TEST_MIGRATION.mdtest(..., async () Effect.runPromise(...))直接把 Effect 塞进 Promise 测试体本地的run(...)、load(...)、svc(...)、runtime.runPromise(...)包装器——它们唯一作用是提供一个 layerPromise 测试体里的tmpdir()加遗留实例提供逻辑测试文件里自定义的ManagedRuntime.make(...)用 Promisetry/catch包裹 Effect 失败用Promise.withResolvers、Bun.sleep或setTimeout做同步——当事件、Deferred、fiber 或确定性状态检查可以胜任时在 layer 构建之后再修改 env/global/flag。Promise 辅助函数并非完全禁止它们可以在非 Effect 边界使用但必须从 Effect 体内通过Effect.promise(...)产出而不是让它们成为测试骨架本身。tmpdirScoped内部对fs.mkdir、fs.realpath等 Node API 的调用正是通过Effect.promise(...)桥接的见 fixture.ts这就是 Effect 体内桥接 Promise 的规范范例。转换配方9 步完成一个文件的迁移识别被测的真实服务判断应当使用其开放的layer还是封闭的defaultLayer需要替换依赖就用开放的在文件顶部构建一个layer真实依赖按需保留慢速或外部边界HTTP、npm、provider、auth替换为test/fake/*假层用 Effect 辅助函数替换本地 Promise 包装器把test(..., async () { ... })改写为it.effect、it.instance或it.live把await调用移入Effect.gen改写为yield*把await using tmp await tmpdir(...)替换为yield* tmpdirScoped(...)当临时目录活在 Effect 测试内部时用Effect.exit、Effect.flip或聚焦的断言辅助函数替换 Promise 失败断言从源码看runner 本身就用Effect.exit捕获失败并打印Cause.prettyErrors测试体内如需显式断言失败Effect.exit是同一思路的直接延续用 fiber、Deferred与Effect.all(..., { concurrency: unbounded })保留并发不要把原本并行的行为意外串行化——instance-state.test.ts 的 high-contention、deferred resume 用例就是保留并发的范本在packages/opencode下运行聚焦测试文件并执行类型检查bun test test/effect/instance-state.test.ts bun typecheck优秀示例与迁移队列策略文档给出的参考示例复用前请重新核对因为迁移仍在进行中test/effect/instance-state.test.ts —— scoped 目录、实例切换、dispose 与并发test/agent/plugin-agent-regression.test.ts —— 真实服务 layer 加假边界 layer 的组合test/account/service.test.ts —— 服务级 live 测试、类型化错误、假 HTTP 客户端。迁移队列刻意不维护长文件清单——它很快就会过时。寻找下一个迁移目标的方式是直接搜索当前的反模式git grep -n Effect.runPromise\|ManagedRuntime\|Promise.withResolvers\|Bun.sleep\|withTestInstance -- packages/opencode/test选中一个文件或一小簇相关文件后保持 PR 聚焦并在 PR 描述中写明聚焦验证的方式。仓库中已有超过 150 个测试文件在使用testEffect模式涵盖 session、server、agent、provider、tool、kilocode 等目录新迁移的测试应当与这些既有用例保持一致风格。需要留意的粗糙边缘Exit/Cause的失败断言容易冗长。等同一形状在多个文件重复出现后再抽取辅助函数不要过早添加部分测试仍需要用Effect.promise(...)包裹 Node/Bun API。当周边代码已使用 Effect 平台服务时优先用平台服务但不要为了追求完美抽象而阻塞有价值的迁移Layer 组合可能显得嘈杂——当测试需要真实服务子树加假边界时。先抽取小型test/fake/*layer而不是发明更大的 builder并发测试在替换 Promise resolver 后更难读懂。留意值得命名的重复模式可参考 effect.ts 中现成的awaitWithTimeout与pollWithTimeout辅助函数分别用于带超时断言和轮询等待确定性状态迁移时若出现同类高频需求可遵循同一思路抽取本地辅助。结语testEffect模式的本质是把测试的编排权从 Promise 收回到 Effectrunner 负责环境注入、失败结构化与资源作用域测试体只关心业务行为。沿着本文的 runner 语义、Layer/Fixture 规则与 9 步配方你可以把任何 Effect 服务测试迁移到这一统一形态并获得更确定、更可并发的测试基线。【免费下载链接】kilocodeKilo is the all-in-one agentic engineering platform. Build, ship, and iterate faster with the most popular open source coding agent.项目地址: https://gitcode.com/GitHub_Trending/ki/kilocode创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表