
opencode 测试基建实战指南tmpdir 临时目录夹具、testEffect 测试器与并发同步模式【免费下载链接】opencodeThe open source coding agent.项目地址: https://gitcode.com/GitHub_Trending/openc/opencode本篇指南基于packages/opencode包内的测试基础设施文档完整讲解三类核心测试能力tmpdir临时目录夹具Promise 风格、testEffect提供的 Effect 测试器it.effect/it.live/it.instance、以及与并发工作同步的就绪信号模式。读完后你能够独立为 opencode 的会话、工具、实例等 Effect 服务编写稳定、无 flake 的集成测试。测试基础设施总览opencode 包packages/opencode的测试代码位于test/目录下其中与本文直接相关的三个核心文件是文件职责test/fixture/fixture.tstmpdir临时目录夹具、Effect 感知的实例夹具tmpdirScoped、provideTmpdirInstance、TestInstance等test/lib/effect.tstestEffect测试器工厂以及pollWithTimeout、awaitWithTimeout同步工具test/lib/llm-server.tsTestLLMServer模拟 LLM 服务含wait(n)就绪等待下面按文档脉络逐节展开。一、tmpdir带自动清理的临时目录夹具tmpdir函数定义在 fixture/fixture.ts为测试创建临时目录并在测试结束时自动清理是包内 Promise 风格测试的基础设施。基本用法import { tmpdir } from ./fixture/fixture test(example, async () { await using tmp await tmpdir() // tmp.path 是临时目录路径 // 测试结束时自动清理 })关键点在于await using语法返回对象实现了Symbol.asyncDispose变量离开作用域测试函数返回时会自动执行清理逻辑。选项Optionstmpdir接受一个泛型选项对象返回类型中的T即init回调的返回值选项类型说明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 })带配置文件await using tmp await tmpdir({ config: { model: test/model, username: testuser }, })自定义初始化返回额外数据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带自定义清理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 }) }, })返回对象tmpdir的返回值包含三个成员path: string—— 临时目录的绝对路径经过fs.realpath解析后的真实路径extra: T——init函数返回的值[Symbol.asyncDispose]—— 支撑await using的自动清理协议。源码级细节它到底做了什么阅读 fixture.ts 源码 可以看到几个实现要点目录命名目录创建在系统临时目录os.tmpdir()下名称为opencode-test-前缀加随机后缀Math.random().toString(36).slice(2)保证并发测试互不干扰。路径消毒所有路径都经过sanitizePath处理剥离空字节\0。这是一处针对 CI 环境问题的防御性修复。git 选项的完整初始化序列git init之后依次执行git config core.fsmonitor false、git config commit.gpgsign false并设置测试身份user.email testopencode.test、user.name Test最后git commit --allow-empty生成 root commit。禁用 fsmonitor 避免了 git 文件监听守护进程在测试间的干扰——这一点还有专门测试守护fixture.test.ts 中disables fsmonitor for git fixtures用例会读取git config core.fsmonitor并断言其值为false。config 选项的写入格式写入的opencode.json会自动带上$schema: https://opencode.ai/config.json字段再展开传入的config部分字段。清理顺序asyncDispose中先执行用户自定义的dispose回调放在try块允许其失败然后在finally中若是 git 仓库则先git fsmonitor--daemon stop再调用内部clean()递归删除目录。删除使用了fs.rm(dir, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 })带重试机制以应对文件系统瞬时占用。清理行为亦有测试验证fixture.test.ts 的removes directories on dispose用例显式调用tmp[Symbol.asyncDispose]()后断言目录已不存在。注意事项目录统一使用opencode-test-前缀便于在系统临时目录中识别和排查残留优先使用await using获得自动清理路径的空字节清洗是防御性措施主要服务于特定 CI 环境。二、使用testEffect测试 Effect 服务对于会触达 Effect 服务或 Effect 工作流的测试应使用 test/lib/effect.ts 中的testEffect(...)而不是手写运行时。核心模式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) }), ) })testEffect(layer)会把你的服务层与测试环境合并返回一个包含effect、live、instance三种测试器的对象每种都提供only/skip变体见 effect.ts 中的make工厂。it.effect与it.live的取舍从源码可以看到两套环境的差异effect.tstestEnv由TestConsole.layer与TestClock.layer()合并而成liveEnv只保留TestConsole.layer即真实时钟。因此选择规则是it.effect(...)测试应在TestClock与TestConsole下运行的场景虚拟时钟、日志捕获it.live(...)测试依赖真实时间、文件系统 mtime、子进程、git、文件锁或其他活的操作系统行为时使用。包内大多数集成风格测试都使用it.live(...)it.instance(...)需要作用域内临时目录和实例上下文的 live Effect 测试。另外源码中还提供了 testEffectShared它通过进程级共享memoMap构建测试层使Bus、Session等缓存服务与Server.Default解析到同一实例。仅当测试需要与进程内 HTTP 服务器保持 pub/sub 身份一致时使用普通测试应坚持用testEffect。Effect 感知的实例夹具对于需要实例上下文opencode 中一个项目目录对应的运行时实例的测试优先使用 fixture.ts 中的 Effect 感知助手而不是在每个测试里手工搭运行时tmpdirScoped(options?)创建作用域感知的临时目录Effect 作用域关闭时自动清理。可推断从源码结构看它与 Promise 版tmpdir行为刻意保持同步源码注释亦注明 Make sure these stay in sync且额外支持config传入函数、init返回Effect。provideInstance(dir)(effect)底层助手不创建目录仅为dir提供InstanceRef经由InstanceStore.Service.provide后运行 Effect。provideTmpdirInstance((dir) effect, options?)便捷助手创建临时目录、绑定为活动实例并在清理时 dispose 实例。provideTmpdirServer((input) effect, options?)在上一者基础上额外提供测试 LLM 服务TestLLMServerinput形如{ dir, llm }且config选项可写成(url: string) PartialConfigV1.Info以将模拟 LLM 的 URL 注入配置。默认选择it.instance(...)当测试只需要一个临时实例时用它即可无需手工拼装目录。当测试需要临时目录路径时从 fixture 中取出TestInstance服务import { TestInstance } from ../fixture/fixture it.instance(uses the temp directory, () Effect.gen(function* () { const test yield* TestInstance expect(test.directory).toContain(opencode-test-) }), )从源码看it.instance的实现是把测试体包进withTmpdirInstance(options)effect.ts后者会创建临时目录、通过Effect.provideService(TestInstance, { directory })注入TestInstance服务再用provideInstanceEffect(directory)绑定实例并附带git/config/init选项支持。何时改用低级助手当测试需要多个目录、绑定前需要自定义初始化、要在单个测试内切换实例上下文或要显式测试实例的 dispose/reload 生命周期时改用provideTmpdirInstance(...)或组合tmpdirScoped()provideInstance(...)。代码风格约定在文件顶部附近定义const it testEffect(...)测试体保持在Effect.gen(function* () { ... })内直接yield* MyService.Service或yield* MyTool取服务既然testEffect(...)已提供运行时避免自定义ManagedRuntime、attach(...)或临时拼装的run(...)包装需要实例局部状态时优先用it.instance(...)而不是在 Promise 风格测试里手写Instance.provide(...)。用Layer.mock做部分服务桩当测试只需覆盖服务的个别方法时优先使用Layer.mock而不是手搓Layer.succeed(Service, Service.of({ ... }))。Layer.mock只要求你提供关心的方法——其余方法一旦被误调用会抛出UnimplementedError缺陷而这正是测试想要的最清晰的信号。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 })), })这比用Effect.void/Effect.succeed(...)占位每个方法短得多也让测试聚焦在被测行为本身。三、与并发工作同步等待就绪信号而非墙上时钟反模式用Effect.sleep(N)或setTimeout充当等待 fork 出的 fiber 就绪的 hack本质上是在与调度器赛跑在较慢的 CI 主机上被 fork 的 fiber 未必在N毫秒内到达同步点测试就会间歇性失败。文档以 PR #27622 中由该模式导致的真实 flake 为例。正确做法等待已发布的就绪信号。可用的同步手段均已在仓库中确认实现手段位置说明pollWithTimeout(effect, message, duration?)test/lib/effect.ts反复执行谓词 Effect直到返回非undefined值超时默认 5 秒轮询间隔 20ms超时抛出带自定义消息的ErrorawaitWithTimeout(effect, message, duration?)test/lib/effect.ts用Effect.timeoutOrElse包装任意 Effect默认超时 2 秒超时消息自定义llm.wait(n)test/lib/llm-server.ts等待模拟 LLM 收到n次 HTTP 调用实现上用Deferred登记期望值当hits.length count时统一放行SessionStatus.Service的.get(sessionID)src/session/status.ts可观察的每会话状态形如{ type: busy \| idle \| ... }未注册会话默认返回idleBackgroundJob.wait({ id, timeout })background-job.ts等待后台任务完成实现为Deferred.await(job.done)指定timeout时管道Effect.timeoutOption返回{ info, timedOut }Bus 订阅各事件总线forkStream.runForEach(bus.subscribe(Event), ...)在回调内打开一个Latch以标记首事件就绪Deferred.await(deferred).pipe(Effect.timeoutOrElse(...))Effect 标准库一次性信号one-shot signal的超时等待前后对比示例文档给出的典型修复——shell 任务取消前确保会话已进入 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)注意谓词 Effect 返回true as const或undefinedpollWithTimeout只对非undefined结果放行超时消息session never became busy让失败原因一目了然。固定睡眠何时可接受并非所有sleep都是反模式以下场景例外测试防抖debounce或节流throttle行为此时睡眠本身就是被测对象让真实时钟越过真正的时间戳分辨率边界例如文件系统 mtime 粒度在刻意验证时序的竞态回归测试中模拟网络延迟。快速参考本文涉及的文件用途路径tmpdir/ Effect 实例夹具packages/opencode/test/fixture/fixture.ts夹具自身测试packages/opencode/test/fixture/fixture.test.tstestEffect/pollWithTimeout/awaitWithTimeoutpackages/opencode/test/lib/effect.ts模拟 LLM 服务packages/opencode/test/lib/llm-server.ts会话状态服务就绪信号来源packages/opencode/src/session/status.ts后台任务等待packages/core/src/background-job.ts本指南的原始文档packages/opencode/test/AGENTS.md【免费下载链接】opencodeThe open source coding agent.项目地址: https://gitcode.com/GitHub_Trending/openc/opencode创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考