Composio CLI 的端到端测试体系:在 Scratch 容器中验证编译后二进制的全流程实践
Composio CLI 的端到端测试体系在 Scratch 容器中验证编译后二进制的全流程实践【免费下载链接】composioComposio powers 1000 toolkits, tool search, context management, authentication, and a sandboxed workbench to help you build AI agents that turn intent into action.项目地址: https://gitcode.com/GitHub_Trending/co/composio本文讲解 Composio 仓库中ts/e2e-tests/cli/下 CLI 端到端测试E2E的设计与实现。该体系将composio编译为自包含二进制放入全新的 Debian scratch 容器中执行version、whoami、upgrade等命令并对退出码、stdout、stderr 及重定向输出做精确断言。读完本文你将理解这套 Docker 隔离测试的构建链路从bun build --compile到多阶段镜像、runCmd测试运行器的契约以及如何为新的 CLI 命令复制出一个可运行的 E2E 套件。核心机制编译产物 Scratch 容器隔离整套 CLI E2E 的骨架文档位于 README其核心设计可以概括为两点每个测试套件都在一个临时的 Debian Docker 容器中运行composio二进制。镜像由 Dockerfile.cli 构建二进制在镜像构建阶段通过bun build --compile编译得到一个无运行时依赖的自包含可执行文件——这正是与 Node.js/Deno 运行时测试ts/e2e-tests/runtimes/最本质的区别这里验证的不是包在某个 runtime 下的行为而是编译产物本身的行为。测试通过runCmd在容器内执行 shell 命令并断言退出码、stdout 和 stderr。当 stdout 被管道或重定向消费时如composio version out.txtCLI 会抑制一切装饰性输出只写入机器可读数据E2E 测试显式验证的就是这份“机器可读契约”。Dockerfile.cli 的两阶段构建Dockerfile.cli 是理解整个体系的关键文件它采用 builder runtime 两阶段结构builder 阶段基于node:${NODE_VERSION}-slimFROM node:${NODE_VERSION}-slim AS builder # ... 通过 mise 安装 bun pnpm版本由 mise.toml/mise.lock 锁定 RUN pnpm install --frozen-lockfile # 编译 composio CLI 二进制 WORKDIR /app/ts/packages/cli RUN bun build ./src/bin.ts \ --env DEBUG_OVERRIDE_* \ --compile \ --production \ --outfile /out/composio # 构建 composio run 的伴生模块 RUN bun scripts/build-companion-modules.ts /out --host-only \ rm -rf /out/acp-adapters其中几个细节直接服务于测试需求--env DEBUG_OVERRIDE_*把DEBUG_OVERRIDE_*前缀的环境变量烘焙进编译产物使测试可以在不改代码的前提下改变二进制行为。例如 upgrade 套件用DEBUG_OVERRIDE_UPGRADE_TARGET把升级目标指向容器内的本地文件避免依赖真实的发布源该调试开关定义在 debug-config.ts。--host-only与删除 acp-adapterscomposio run的伴生模块必须与二进制同目录从dirname(process.execPath)解析否则 CLI 会认为安装损坏并进入 GitHub 自修复路径。Dockerfile 注释里给出了明确的体积核算携带全部四个平台的 codex-acp 会增加约 870MB、约 2 分钟构建时间因此只保留当前平台acp-adapters 因没有 E2E 用例调用而被整体删除以节省约 224MB。预先创建/tmp/.composio绕过 Bun issue #7967 的缓存目录问题。runtime 阶段基于debian:bookworm-slimFROM debian:bookworm-slim ENV PATH/usr/local/bin:/bin ENV HOME/tmp COPY --frombuilder /out/ /usr/local/bin/ COPY --frombuilder /app/ts/e2e-tests/cli/ /app/ts/e2e-tests/cli/最终镜像极其精简README 中称之为 “scratch” 风格PATH只有/usr/local/bin:/binHOME指向/tmp。这个刻意的“贫瘠”环境正是测试价值所在——任何依赖 shell 配置、宿主机环境变量的行为都会在这里暴露。测试套件与断言内容README 中的套件表列出了三个基础套件当前仓库中实际还包含install、run、agent-signin、setup-plugins和toolkitslist/info/search等更多套件目录结构如下套件验证的命令 / 行为所需环境变量upgradecomposio upgrade原子替换正在运行的 Linux 可执行文件无versioncomposio version的输出与退出码含--check机器可读输出无whoamicomposio whoami打印 API keyCOMPOSIO_USER_API_KEYinstall安装脚本路径内置release-server.ts模拟发布源无toolkitscomposio toolkits list/info/search无所有套件统一使用versions: { cli: [current] }其中current会解析为当前 monorepo 构建中的 CLI 版本——从源码看config.ts 通过execFileSync(mise, [current, tool])从mise.toml锁定值取得当前版本保证测试始终针对“本仓库正在发布的二进制”而不是某个固定旧版本。version 套件验证机器可读输出契约version/e2e.test.ts 覆盖四个场景基础composio version退出码为 0stderr 为空stdout 经sanitizeOutput清洗后必须精确等于ts/packages/cli/package.json中的版本号--check发现新版本测试先写入一个latestVersion: 99.0.0的.composio/update-check.json然后断言 stdout 可被解析为如下 JSON对应 update-check.ts 的状态机{ current: 当前版本, latestStable: 99.0.0, updateAvailable: true, checkStatus: update-available, lastChecked: 2099-01-01T00:00:00.000Z }--check未知状态只有lastAttempted、没有成功检查记录时输出checkStatus: unknown、latestStable: null——测试专门断言 CLI不会在未知时谎报“已是最新”stdout 重定向composio version out.txt时stdout/stderr 均应为空out.txt内容经清洗恰好是版本号本身。这正是 README 所述“管道模式下抑制装饰、只写机器可读数据”契约的直接验证。upgrade 套件原子替换运行中的可执行文件upgrade/e2e.test.ts 是三个基础套件中最有技术含量的一个它验证“一个正在运行的 Linux 二进制能否把自己替换掉”。测试脚本在容器内完成以下步骤记录目标可执行文件的 inodestat -c %i通过DEBUG_OVERRIDE_UPGRADE_TARGET将升级源指向容器内复制出的二进制副本执行composio upgrade再次统计 inode并依次检查升级命令退出码为 0、可执行文件仍然存在且可执行、重新运行version成功且输出符合 semver 格式核心断言升级前后的 inode 必须不同说明文件被整体替换而非原地写入且 stderr 中不允许出现ETXTBSY/ “text file busy” 类错误。这套断言与 CLI 侧的实现相对应upgrade-binary.ts 从 atomic-replace.ts 引入atomicReplaceFile与atomicReplaceDirectory通过“写入临时文件 原子 rename”的方式替换二进制和伴生模块从而规避直接writeFile覆盖正在执行的 ELF 文件所触发的 ETXTBSY 问题。测试还覆盖了一个拷贝到/tmp/composio-copy/composio的路径验证非标准安装位置的替换同样成立。whoami 套件环境变量注入whoami/e2e.test.ts 演示了env选项的用法e2e()配置声明COMPOSIO_USER_API_KEY: Bun.env.COMPOSIO_USER_API_KEY后该变量会被注入容器环境并在启动时校验。测试断言composio whoami退出码为 0、stderr 为空并准备了对完整 JSON 输出的断言global_user_api_key、default_org_id、default_project_id、test_user_id字段——注意其中两条精确 JSON 断言当前标记为it.skip即该套件目前实际生效的是退出码、空 stderr 与重定向空输出这几项契约。运行测试README 与根 package.json 中的 turbo 脚本对应关系如下# 仓库根目录运行全部 CLI e2e 测试 pnpm test:e2e:cli # 等价于: turbo test:e2e:cli --filtere2e-tests/cli-* # 单独运行某个套件 cd ts/e2e-tests/cli/version pnpm test:e2e:cli cd ts/e2e-tests/cli/whoami pnpm test:e2e:cli cd ts/e2e-tests/cli/upgrade pnpm test:e2e:cli每个套件的package.json如 version/package.json都定义了两个脚本{ test:e2e: bun test e2e.test.ts, test:e2e:cli: bun test e2e.test.ts }即测试文件本身用bun test驱动而e2e()调用内部会负责构建/复用 Docker 镜像并在容器内执行命令。需要注意的运行前提见 AGENTS.md需要正在运行的 Docker daemon包名必须保持e2e-tests/cli-*的私有作用域命名turbo 的--filter依赖这个命名约定来发现套件建议为打包/运行时回归添加窄而聚焦的 E2E 覆盖而不是宽泛的重复测试。测试运行器e2e、runCmd与结果类型所有套件共享 ts/e2e-tests/_utils/ 中的基础设施Node/Deno 测试同样复用另见总览 README。从 types.ts 可以看到核心契约E2ETestResult{ exitCode, stdout, stderr }是runCmd的返回类型E2ETestResultWithFilesF在结果上附加files字段把容器内的指定文件如out.txt拷出并作为断言对象——version/whoami 套件的红重定向测试都依赖它runCmd有两种签名只传命令字符串或传{ command, files }对象以声明需要拷出的文件CLI 运行时下测试上下文只提供runCmdNode 运行时测试则提供runFixture。标准套件写法来自 总 README 的 CLI 模板import { e2e, sanitizeOutput, type E2ETestResult } from e2e-tests/utils; import { TIMEOUTS } from e2e-tests/utils/const; import { describe, it, expect, beforeAll } from bun:test; e2e(import.meta.url, { versions: { cli: [current], }, defineTests: ({ runCmd }) { let result: E2ETestResult; beforeAll(async () { result await runCmd(composio version); }, TIMEOUTS.FIXTURE); describe(output, () { it(exits successfully, () { expect(result.exitCode).toBe(0); }); it(stdout matches snapshot, () { expect(sanitizeOutput(result.stdout)).toMatchSnapshot(); }); }); }, });其中sanitizeOutputsanitize.ts负责剥离 ANSI 转义序列、统一换行符并去除首尾空白保证跨平台的稳定比较同文件的parseJsonStdout则在解析失败时给出包含 stderr 前 300 字符的清晰错误信息——version 套件的--checkJSON 断言就依赖它。调试方面每个套件运行时会生成一份临时的DEBUG.log见 总 README 的 Debugging 一节其中按运行时版本分段记录每个 Docker 阶段的容器名、命令、耗时、退出码及 stdout/stderr 全文便于定位“镜像构建成功但命令失败”这类问题。新增一个 CLI E2E 套件的完整步骤结合 总 README 的 “Adding New Tests → CLI Runtime Tests” 与既有套件的实际结构新增套件的流程为在ts/e2e-tests/cli/下创建目录如cli/my-test添加package.json包名遵循e2e-tests/cli-my-test约定并定义test:e2e与test:e2e:cli两个脚本内容均为bun test e2e.test.ts编写e2e.test.tse2e(import.meta.url, { versions: { cli: [current] }, defineTests: ... })在beforeAll中用runCmd执行命令随后对exitCode/stdout/stderr/ 拷出文件做断言如命令需要环境变量在env中声明如 whoami 套件的COMPOSIO_USER_API_KEY如需要改变二进制的内部行为如指向本地升级源利用烘焙进产物的DEBUG_OVERRIDE_*系列环境变量而非修改源码。由于镜像构建脚本docker-build.ts预构建、docker-clean.ts清理位于 ts/e2e-tests/_utils/scripts/按Dockerfile.cli统一产出 CLI 镜像新增套件不需要任何额外的 Dockerfile 工作——只需遵守命名与目录约定pnpm test:e2e:cli的 turbo filter 即可自动纳入。小结ts/e2e-tests/cli/的价值在于把“编译产物在干净 Linux 环境下的行为”变成了可重复回归的契约Docker 两阶段镜像保证每次测试都面对同一套bun build --compile产物 精简 Debian 环境runCmd的{ exitCode, stdout, stderr, files }结果模型让断言直接落在 CLI 面向用户和脚本的真实输出上version/upgrade/whoami等套件则分别守护机器可读输出、原子自升级inode 变化 无 ETXTBSY与凭据展示这几条最容易在打包回归中被破坏的路径。如需查看 CLI 侧被验证的实现可从 upgrade-binary.ts、version.cmd.ts 与 update-check.ts 继续深入。【免费下载链接】composioComposio powers 1000 toolkits, tool search, context management, authentication, and a sandboxed workbench to help you build AI agents that turn intent into action.项目地址: https://gitcode.com/GitHub_Trending/co/composio创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考