Kilo 测试基础设施实战指南:临时目录 Fixture、Effect 测试模式与并发同步

Kilo 测试基础设施实战指南:临时目录 Fixture、Effect 测试模式与并发同步 Kilo 测试基础设施实战指南临时目录 Fixture、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导读本文以 Kilokilocode仓库中 packages/opencode/test/AGENTS.mdTest Fixtures Guide为骨架系统讲解opencode包测试体系的三大核心tmpdir临时目录 Fixture、基于 Effect 的testEffect测试模式、以及与并发工作同步的正确姿势。读完本文你将掌握如何为 Effect 服务编写可复现、不 flaky 的集成测试理解it.effect/it.live/it.instance三种 runner 的取舍并学会用就绪信号替代sleep式竞态等待。文中所有结论均可对照仓库源码验证。一、临时目录 Fixturetmpdir1.1 为什么需要临时目录大多数集成测试需要一个真实的工作目录初始化 git 仓库、写入配置文件、执行 shell 命令。直接在系统临时目录手写fs.mkdtemp加 try/finally 清理会导致每个测试重复样板代码且容易泄漏。opencode测试体系用tmpdir统一解决这个问题。tmpdir定义在 packages/opencode/test/fixture/fixture.ts核心行为如下在系统临时目录下创建前缀为opencode-test-的目录源码见 fixture.ts路径会经过sanitizePath处理剥离空字节\0这是针对 CI 环境的防御性修复源码见 fixture.ts返回的对象实现了Symbol.asyncDispose配合await using在测试结束时自动清理目录。1.2 基本用法import { tmpdir } from ./fixture/fixture test(example, async () { await using tmp await tmpdir() // tmp.path 是临时目录的绝对路径 // 测试结束时目录被自动清理 })注意这里用的是await usingES 显式资源管理而非普通的await。当变量离开作用域时[Symbol.asyncDispose]会被自动调用无需手写清理代码。测试文件 packages/opencode/test/fixture/fixture.test.ts 验证了dispose之后目录确实被删除。1.3 选项详解tmpdir接受一个可选的 options 对象选项类型说明gitboolean初始化一个 git 仓库并创建 root commitconfigPartialConfig.Info写入opencode.json配置文件init(dir: string) PromiseT自定义初始化函数返回值可通过tmp.extra访问dispose(dir: string) PromiseT自定义清理函数Git 仓库模式await using tmp await tmpdir({ git: true })开启git: true后源码会依次执行git init→ 关闭core.fsmonitor→ 关闭commit.gpgsign→ 设置测试专用 user 身份testopencode.test/Test→ 创建空 root commit见 fixture.ts。其中关闭 fsmonitor 和 gpgsign 是关键的确定性保证前者避免文件监视器在测试进程外驻留后者确保 commit 不受开发者全局签名配置影响。fixture 测试 fixture.test.ts 专门断言了core.fsmonitor被置为false。写入配置文件await using tmp await tmpdir({ config: { model: test/model, username: testuser }, })源码中会生成opencode.json内容为{ $schema: ..., ...options.config }其中$schema指向https://app.kilo.ai/config.json见 fixture.ts。自定义初始化返回额外数据await using tmp await tmpdirstring({ init: async (dir) { await Bun.write(path.join(dir, file.txt), content) return extra data }, }) // 通过 tmp.extra 访问返回的数据 console.log(tmp.extra) // extra data泛型参数T对应init的返回值类型init接收的dir是 realpath 解析后的真实路径。自定义清理await using tmp await tmpdir({ init: async (dir) { const specialDir path.join(dir, special) await fs.mkdir(specialDir) return specialDir }, dispose: async (dir) { // 自定义清理逻辑 await fs.rm(path.join(dir, special), { recursive: true }) }, })当同时提供dispose时清理顺序是先执行自定义dispose再执行实例资源释放、git daemon 停止如适用、最终目录删除见 fixture.ts。1.4 返回对象字段说明path: string临时目录的绝对路径经realpath解析extra: Tinit函数的返回值[Symbol.asyncDispose]支持await using自动清理1.5 注意事项目录统一创建在系统临时目录os.tmpdir()前缀为opencode-test-必须使用await using才能享受作用域退出时的自动清理路径经过空字节剥离这是为 CI 环境做的防御性修复。二、Effect 测试模式testEffectopencode的服务层基于 Effect 构建因此测试也围绕 Effect 生态展开。核心入口是testEffect(...)定义在 packages/opencode/test/lib/effect.ts。2.1 核心模式import { describe, expect } from bun:test import { Effect, Layer } from effect import { testEffect } from ../lib/effect const it testEffect(Layer.mergeAll(MyService.defaultLayer)) describe(my service, () { it.instance(does the thing, () Effect.gen(function* () { const svc yield* MyService.Service const out yield* svc.run() expect(out).toEqual(ok) }), ) })测试体完全处于Effect.gen(function* () { ... })内通过yield*获取服务expect断言在 Effect 内部直接使用——Effect 测试的运行时由testEffect统一搭建不需要手写ManagedRuntime。2.2it.effectvsit.livevsit.instancetestEffect(layer)返回一个 runner 对象源码见 effect.ts提供三种测试方法it.effect(...)使用TestClock与TestConsole。适合纯 Effect 行为测试——虚拟时钟可以确定性推进时间TestClock让定时器、超时等逻辑无需真实等待。it.live(...)使用真实时钟但仍保留TestConsole捕获输出。适用于依赖真实时间、文件系统 mtime、子进程、git、锁等真实操作系统行为的测试。本包绝大多数集成测试使用it.live(...)。it.instance(...)live的增强版自动为测试提供一个作用域化的临时实例scoped temporary directory instance context适合需要实例级状态session、background job、config 等的测试。从源码看effect.tsit.instance实际是test(body.pipe(withTmpdirInstance(options)), liveLayer)——即在 live 环境下注入TestInstance和InstanceStore服务测试体内可以直接yield* TestInstance拿到临时目录。实际的实例切换逻辑封装在withTmpdirInstance中见 fixture.ts它会创建临时目录、绑定实例并在 scope 结束时释放。2.3 风格规范在文件顶部定义const it testEffect(...)测试体保持在Effect.gen(function* () { ... })内用yield* MyService.Service或yield* MyTool直接获取服务避免自定义ManagedRuntime、attach(...)或临时run(...)包装——testEffect已提供运行时需要实例本地状态时优先it.instance(...)而不是在 Promise 风格测试里手动Instance.provide(...)。关于这套模式的目标形态与迁移路径可参考同目录的 packages/opencode/test/EFFECT_TEST_MIGRATION.md它明确了每个 Effect 服务测试文件应在顶部声明一个本地 runner并按行为选择it.effect/it.instance/it.live同时列出了一批需要消除的反模式如test(..., async () Effect.runPromise(...))、自定义ManagedRuntime.make(...)、用Bun.sleep/setTimeout做同步等。三、Effect 化 Fixture作用域目录与实例上下文3.1 四种辅助工具tmpdir面向 Promise 风格测试在 Effect 测试里应优先使用 packages/opencode/test/fixture/fixture.ts 提供的 Effect 感知辅助函数而不是在每个测试里手工搭建 runtimetmpdirScoped(options?)创建作用域化临时目录当 Effect scope 关闭时自动清理见 fixture.ts。它支持git、config可为函数、init返回 Effect选项内部通过Effect.addFinalizer注册清理逻辑与tmpdir保持行为同步。provideInstance(dir)(effect)底层辅助函数。它不创建目录只把dir作为InstanceRef提供给 Effect 运行见 fixture.ts。provideTmpdirInstance((dir) effect, options?)便捷函数。创建临时目录 → 绑定为当前活跃实例 → 清理时释放实例见 fixture.ts。provideTmpdirServer((input) effect, options?)在provideTmpdirInstance基础上额外提供测试 LLM 服务器TestLLMServer见 fixture.ts适合需要走完整 LLM 调用链的测试。3.2 用it.instanceTestInstance获取目录当测试只需要一个临时实例时默认使用it.instance(...)需要目录路径时从fixture/fixture.ts引入TestInstance其定义见 fixture.tsimport { TestInstance } from ../fixture/fixture it.instance(uses the temp directory, () Effect.gen(function* () { const test yield* TestInstance expect(test.directory).toContain(opencode-test-) }), )3.3 何时使用显式辅助函数以下场景应使用provideTmpdirInstance(...)或tmpdirScoped()provideInstance(...)组合一个测试需要多个临时目录绑定实例前需要自定义 setup需要在单个测试内切换实例上下文显式测试实例 dispose / reload 生命周期。例如 background job 的测试就大量使用it.instance见 packages/opencode/test/background/job.test.ts——它通过testEffect(LayerNode.compile(BackgroundJob.node))拿到 runner然后用it.instance验证任务跟踪、超时快照、并发 start 去重、extension 等待等行为。四、部分服务桩Layer.mock4.1 为什么优先Layer.mock当测试只需要覆盖服务的某几个方法时优先使用Layer.mock而非手写Layer.succeed(Service, Service.of({ ... }))。Layer.mock允许你只提供关心的方法——其余方法在测试意外调用时会抛出UnimplementedErrordefect。这正是你想要的信号测试触发了未预期的依赖路径说明被测行为与你的假设不一致。4.2 示例import { Effect, Layer } from effect import { Account } from /account/account const failingAccountLayer Layer.mock(Account.Service, { orgsByAccount: () Effect.fail(new Account.AccountServiceError({ message: simulated upstream failure })), })该示例中的Account服务位于 packages/opencode/src/account/account.tsAccountServiceError是其错误类型。这段代码模拟上游失败让被测逻辑走错误分支。相比用Effect.void/Effect.succeed(...)把每个方法都填一遍Layer.mock的代码量更少且让测试聚焦在被测行为上。4.3 仓库中的实际应用Layer.mock在测试目录中被广泛使用例如 packages/opencode/test/fake/account.ts 用它构造empty假层active返回Option.none()packages/opencode/test/agent/agent.test.ts 用Layer.mock(MCP.Service)({})给 MCP 服务一个空实现packages/opencode/test/config/config.test.ts 用它构造 well-known 认证假层。这些可复用假层集中在 packages/opencode/test/fake 目录与 EFFECT_TEST_MIGRATION.md 中优先提取小型可复用 fake 边界层的建议一致。五、与并发工作同步告别sleep竞态5.1 反模式固定睡眠等待在测试中用Effect.sleep(N)或setTimeout作为等待 fork 的 fiber 就绪的手段是在和调度器竞速在慢速 CI 主机上被 fork 的 fiber 可能还没到达同步点N毫秒就已经过去测试因此间歇性失败。文档中引用的 PR #27623 正是这类模式的真实 flake 案例。5.2 正确做法等待已发布的就绪信号不要等墙钟时间要等一个已发布的就绪信号。可用工具如下工具来源用途pollWithTimeout(effect, message, duration?)test/lib/effect.ts反复运行谓词 Effect直到返回非undefined值带超时默认 5 秒轮询间隔 20msawaitWithTimeout(effect, message, duration?)test/lib/effect.ts用Effect.timeoutOrElse包装任意 Effect超时抛出自定义错误消息默认 2 秒llm.wait(n)test/lib/llm-server.ts等待 mock LLM 收到n次 HTTP 调用SessionStatus.Service的.get(sessionID)src/session/status在 app-runtime.ts 中引入查询每个 session 的可观察状态{ type: busy \| idle \| ... }BackgroundJob.wait({ id, timeout })src/background/job.ts等待后台任务完成Bus 订阅测试内 forkStream.runForEach(bus.subscribe(Event), ...)在回调内打开Latch发出首事件信号等待首个事件就绪Deferred.await(deferred)Effect.timeoutOrElseEffect 标准库一次性信号以llm.wait为例其源码实现llm-server.ts是若hits.length count直接返回否则创建一个Deferred注册到等待队列每次收到 HTTP 请求时notify检查并唤醒满足条件的等待者见 llm-server.ts——这正是发布就绪信号而非轮询墙钟的机制。5.3 完整示例等 session 变 busy 再取消// 反模式 —— 竞态 yield * prompt.shell({ command: sleep 30 }).pipe(Effect.forkChild) yield * Effect.sleep(50) yield * prompt.cancel(chat.id) // 正确 —— 等待已发布的就绪信号 yield * prompt.shell({ command: sleep 30 }).pipe(Effect.forkChild) yield * pollWithTimeout( Effect.gen(function* () { const s yield* (yield* SessionStatus.Service).get(chat.id) return s.type busy ? (true as const) : undefined }), session never became busy, ) yield * prompt.cancel(chat.id)这个例子的逻辑是先 fork 一个会执行 30 秒的 shell 任务然后轮询 session 状态直到它变为busy说明任务确实已开始运行再执行取消。这样无论 CI 快慢取消都发生在任务真正启动之后测试稳定可复现。5.4 什么时候固定睡眠是合理的文档明确列出了允许使用固定睡眠的场景测试 debounce / throttle 行为——此时 sleep本身就是被测对象让真实墙钟越过某个真实的时间戳分辨率边界如 mtime 粒度在刻意验证执行顺序的竞态回归测试中模拟网络延迟。除此之外凡是等待就绪的语义都应替换为就绪信号。六、总结opencode的测试基础设施围绕三条主线展开本指南全部给出了源码级依据确定性环境tmpdir/tmpdirScoped提供自动清理的临时目录支持 git 仓库、opencode.json配置、自定义 init/dispose测试实现见 packages/opencode/test/fixture/fixture.ts行为验证见 packages/opencode/test/fixture/fixture.test.ts。统一测试运行时testEffect屏蔽了 Effect 运行时的搭建细节用it.effect虚拟时钟、it.live真实时钟、it.instance 临时实例三个 runner 覆盖不同行为类型实现见 packages/opencode/test/lib/effect.ts配套迁移指南见 packages/opencode/test/EFFECT_TEST_MIGRATION.md。稳定的并发同步用pollWithTimeout、awaitWithTimeout、llm.wait、SessionStatus、BackgroundJob.wait、Deferred等就绪信号取代sleep竞态让测试在慢速 CI 上也能稳定通过。对于任何在opencode包中新增或迁移测试的开发者这套约定既是规范也是工具箱遵循它测试将更短、更快、更稳定。【免费下载链接】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),仅供参考