Composio E2E 测试工具链解析:在隔离 Docker 环境中运行@composio/core与 CLI 端到端测试
【免费下载链接】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/_utils/README.md 展开,系统讲解这套端到端测试基础设施的设计与用法。它面向@composio/coreSDK 与composioCLI 的跨运行时(Node.js / Deno / CLI)测试场景:每个测试用例都会在独立的 Docker 容器中执行,从而保证环境隔离、版本可复现。读完本文,你将掌握e2e()入口、E2EConfig配置、runCmd/runFixture执行原语、运行时版本解析策略、环境变量校验、Docker 卷共享机制以及DEBUG.log结构化日志,能够直接编写出新的端到端测试套件。
目录结构与模块划分
这套工具位于 ts/e2e-tests/_utils,package.json声明了私有包@e2e-tests/utils,其 exports 映射了两个子路径:@e2e-tests/utils(主入口)与@e2e-tests/utils/const(超时与版本常量)。
| 文件/目录 | 职责 |
|---|---|
src/ | TypeScript 工具实现(e2e 运行器、配置解析、类型定义) |
scripts/ | Docker 镜像构建与清理脚本 |
Dockerfile.node | 多阶段构建的 Node.js 测试环境镜像 |
Dockerfile.deno | Deno 测试环境镜像 |
Dockerfile.cli | 基于 scratch 的 CLI 测试环境镜像 |
从 src/index.ts 可以确认公共 API 面:它导出全部类型(E2EConfig、E2ETestResult、E2ETestResultWithSetup、E2ETestResultWithFiles、RunFixtureOptions、DefineTestsContext、各运行时版本元类型等)、e2e()主入口、sanitizeOutput/parseJsonStdout输出处理函数,以及安装型 E2E 的配置与镜像生命周期函数(resolveInstallE2EConfig、checkDocker、ensureInstallImage、runInstallContainer)。
e2e():自动推断工作目录与套件名的主入口
e2e()是所有测试的入口函数,它基于bun:test搭建测试框架。其最大特点是免配置推断:调用者只需传入import.meta.url,工具链会自动推导出测试工作目录(cwd)与套件名(suiteName)。
import { e2e, 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: { node: ['22.22.3', '24.17.0', '25.9.0'], // 可选,默认取 mise.toml deno: ['2.6.7'], // 可选,默认取 mise.toml cli: ['current'], // 可选,默认取 CLI package.json 版本 }, env: { MY_VAR: 'value' }, // 可选环境变量 defineTests: ({ runtime, runCmd, runFixture }) => { let result: E2ETestResult; beforeAll(async () => { result = await runFixture({ filename: 'fixtures/test.mjs' }); }, TIMEOUTS.FIXTURE); describe('output', () => { it('exits successfully', () => { expect(result.exitCode).toBe(0); }); }); }, });推断逻辑的源码实现
在 src/e2e.ts 中可以看到完整的推导链路:
validateImportMetaUrl()要求第一个参数必须是file://开头的 URL,否则抛出明确错误;inferCwd()将调用者文件所在目录转换为相对仓库根的路径,并统一为正斜杠(/)以便 Docker 使用;- 套件名
suiteName取 cwd 路径的最后一段(如openai-zod4-compat),该名称会贯穿容器命名、卷命名与日志文件。
因此测试文件的组织方式天然决定套件名:放在ts/e2e-tests/runtimes/node/<suite>/e2e.test.ts的测试,其 cwd 与 suiteName 都自动与目录保持一致。
E2EConfig:测试套件的核心配置
e2e()接收的配置对象定义如下(类型见 src/types.ts):
| 属性 | 类型 | 说明 |
|---|---|---|
versions | RuntimeVersions | 要测试的运行时版本,见下文 |
env | Record<string, string \| undefined> | 传递给 Docker 容器的环境变量,启动时会做校验 |
usesFixtures | boolean | 为 true 时将 cwd 切换到{testDir}/fixtures,默认false |
defineTests | (ctx: DefineTestsContext) => void | 使用 bun:test 原语定义测试的回调 |
defineTests会在每个运行时版本的describe块内各调用一次,因此同一个测试文件即可完成多版本矩阵覆盖。
RuntimeVersions与DefineTestsContext
RuntimeVersions的三个字段均可省略,省略时各有默认来源:
| 属性 | 类型 | 默认来源 |
|---|---|---|
node | readonly NodeVersionFromUser[] | mise.toml中的 Node 版本 |
deno | readonly DenoVersionFromUser[] | mise.toml中的 Deno 版本 |
cli | readonly CliVersionFromUser[] | ts/packages/cli/package.json的版本 |
DefineTestsContext是defineTests回调收到的上下文,包含三个成员:
| 属性 | 类型/签名 | 说明 |
|---|---|---|
runtime | 'node' \| 'deno' \| 'cli' | 当前被测运行时 |
runCmd | (command: string) => Promise<E2ETestResult> | 在 Docker 容器中执行任意命令 |
runFixture | (options: RunFixtureOptions) => Promise<E2ETestResult \| E2ETestResultWithSetup> | 运行 fixture,可选 setup 阶段 |
值得注意的类型约束:runCmd与runFixture都使用了泛型const参数与NonEmptyString工具类型(定义在 src/types.ts),可以在编译期拒绝空字符串命令;runCmd传入{ command, files }对象时会返回携带files字段的结果类型,实现重载间的类型收窄。
runFixture:单阶段与双阶段(setup + fixture)执行模式
runFixture支持两种模式,行为差异见 src/types.ts 与 src/runner.ts 中的实现:
- 无
setup:直接执行node <filename>,返回E2ETestResult; - 有
setup:创建 Docker 命名卷 → 以读写方式挂载卷执行 setup 命令(如npm install)→ 再以只读方式挂载卷执行 fixture 命令,返回E2ETestResultWithSetup。
// 简单 fixture(无需安装依赖) const result = await runFixture({ filename: 'test.mjs' }); // 带 setup 阶段的 fixture(使用 Docker 卷) const result = await runFixture({ filename: 'index.mjs', setup: 'npm install --legacy-peer-deps', }); expect(result.setup.exitCode).toBe(0); // 校验 setup 阶段 expect(result.exitCode).toBe(0); // 校验 fixture 阶段卷机制的原理细节
在 src/volume.ts 中可以看到完整的卷生命周期管理:
generateVolumeName(suiteName, version)生成e2e-{suiteName}-{version}-{timestamp}格式的唯一卷名,并对套件名做字符净化;createVolume()调用docker volume create,失败即抛出带 cause 的错误;initializeVolumeOwnership()以 root 身份启动一次性容器执行chown -R:Node 镜像使用node:node,Deno 镜像使用deno:deno——因为 Docker 卷默认属主为 root,不初始化会导致非 root 运行时用户无法写入;removeVolume()在finally中清理,且失败只告警不抛错(best-effort 清理,不因清理问题拖垮测试结果)。
运行时差异:Deno 与 CLI
- Deno 环境下,fixture 命令统一包装为
deno run --allow-all <filename>(见 src/runner.ts 中createDenoDockerExecutors); - CLI 运行时不支持
runFixture,调用会直接抛出'runFixture is not supported for CLI runtime tests'——CLI 测试请使用runCmd执行composio ...命令。
runCmd与文件捕获(File Capture)
当需要在容器内断言生成的文件时,给runCmd传入files数组:
const result = await runCmd({ command: 'composio version > out.txt', files: ['out.txt'], }); expect(result.files['out.txt']).toBe('0.1.24');其底层实现(src/runner.ts 中的captureFilesFromContainer):执行完成后用docker cp把指定文件复制到宿主机临时目录再读取,文件以请求路径为键返回Record<string, string>。需要文件捕获时容器不会被自动删除(remove: false),捕获完成后在finally中执行docker rm清理。
结果类型:E2ETestResult与E2ETestResultWithSetup
interface E2ETestResult { exitCode: number; // 命令退出码(0 为成功) stdout: string; // 捕获的 stdout stderr: string; // 捕获的 stderr }E2ETestResultWithSetup在其基础上扩展了setup字段:
interface E2ETestResultWithSetup extends E2ETestResult { setup: { exitCode: number; stdout: string; stderr: string; }; }顶层字段(exitCode、stdout、stderr)反映fixture 阶段的结果,setup对象则记录 setup 命令的结果,便于分别断言两个阶段。
输出清洗与 JSON 解析:sanitizeOutput/parseJsonStdout
sanitizeOutput用于稳定化输出以便断言与快照比较(实现见 src/sanitize.ts):
import { sanitizeOutput } from '@e2e-tests/utils'; const clean = sanitizeOutput(result.stdout);它依次完成三件事:
- 用正则
\x1b\[[0-9;]*m移除 ANSI 转义序列(颜色/格式码); - 将
\r\n归一化为\n(Windows → Unix 换行); - 对首尾空白做
trim()。
同模块还导出了parseJsonStdout(result):当 CLI 出错(如 401)时 stdout 为空,直接JSON.parse("")会抛出难以理解的SyntaxError: Unexpected EOF;该工具会在解析失败时把exitCode与 stderr 预览一并写入错误信息,帮助快速定位根因。
TIMEOUTS:预定义超时常量
import { TIMEOUTS } from '@e2e-tests/utils/const'; it( 'calls LLM', async () => { // test code }, { timeout: TIMEOUTS.LLM_SHORT } );常量定义位于 src/const.ts:
| 常量 | 值 | 适用场景 | | ---- | -- | ---- | |DEFAULT|5_000| 常规测试操作 | |FIXTURE|120_000| 调用runFixture()的beforeAll钩子 | |LLM_SHORT|30_000| 快速 LLM 调用 | |LLM_LONG|60_000| 复杂 LLM 操作 |
运行时版本解析策略
优先级统一为:环境变量 > 配置 > 默认值
三类运行时的解析优先级一致(实现见 src/config.ts):
COMPOSIO_E2E_NODE_VERSION/COMPOSIO_E2E_DENO_VERSION/COMPOSIO_E2E_CLI_VERSION环境变量(最高优先级):本地模式下直接覆盖为单一版本;config.versions.*:使用配置数组;- 默认值:Node/Deno 读取
mise.toml(通过getMiseVersion()执行mise current <tool>),CLI 读取ts/packages/cli/package.json的version字段。
值得注意的实现细节:resolveNodeVersionMetaList()在检查环境变量之后才读取mise.toml,因此即使本机未安装 mise 工具链,环境变量覆盖也能正常工作。
预定义的 well-known 版本
src/const.ts 从仓库根 toolchain-versions.json 读取版本清单:
- Node.js:
22.22.3、24.17.0、25.9.0,以及current(解析为 mise.toml 版本); - Deno:
2.6.7,以及current(解析为 mise.toml 版本); - CLI:仅
current(解析为 CLI package.json 版本)。
版本元类型NodeVersionMeta/DenoVersionMeta/CliVersionMeta(见 src/types.ts)使用可辨识联合标记来源:'static'(well-known 固定版本)、'overridden'(环境变量覆盖)、'current'(跟随仓库默认),并携带SkipInCI跳过状态。
CI 模式下的矩阵裁剪
当设置了CI环境变量且同时设置了对应版本环境变量时(如 GitHub Actions 矩阵任务),computeSkipForVersion会把不匹配的版本标记为skip,测试运行器会为它们生成SKIPPED的日志段落与it.skip用例,从而让矩阵中的每个 job 只跑自己被分配到的版本(相关逻辑见 src/config.ts 与 src/runner.ts)。
环境变量校验:fail-fast 防静默失败
E2EConfig.env中的变量在测试启动时(任何 Docker 操作之前)就会校验。若某个变量值为undefined,测试立即失败并输出明确错误:
[my-test] Missing required environment variables: COMPOSIO_API_KEY, OPENAI_API_KEY Set these variables before running the tests, or remove them from E2EConfig.env if not required.实现位于 src/runner.ts 的validateRequiredEnvVars()。此外buildContainerEnv()会自动透传 well-known 变量(ANTHROPIC_API_KEY、COMPOSIO_API_KEY、COMPOSIO_BASE_URL、OPENAI_API_KEY,见 src/const.ts),再与显式配置的env合并——这样 LLM 类测试无需重复声明密钥即可在容器内使用。
usesFixtures:自带 package.json 的测试布局
当测试目录自带package.json并需要执行npm install时,开启usesFixtures: true:
import { TIMEOUTS } from '@e2e-tests/utils/const'; e2e(import.meta.url, { usesFixtures: true, defineTests: ({ runFixture }) => { beforeAll(async () => { // 两条命令都在 fixtures/ 目录下执行 result = await runFixture({ filename: 'index.mjs', // 解析为 fixtures/index.mjs setup: 'npm install', // 在 fixtures/ 中执行 }); }, TIMEOUTS.FIXTURE); }, });开启后产生三个影响(类型注释见 src/types.ts):
- Docker 命令的工作目录变为
{testDir}/fixtures/; - Docker 卷挂载点变为
fixtures/node_modules(而非testDir/node_modules); runFixture({ filename })中的路径相对fixtures/解析。
normalizeVersionConfig()在未配置versions时默认返回{ node: ['current'], deno: ['current'] },即默认覆盖 Node 与 Deno 的仓库默认版本,CLI 为 opt-in。
Docker 镜像构建与清理脚本
工具包在package.json中声明了两个 pnpm 脚本:
# 为所有 well-known 的 Node、Deno、CLI 版本预构建 Docker 镜像 pnpm docker:build # 移除所有 e2e Docker 镜像(Node.js、Deno、CLI) pnpm docker:clean以 Dockerfile.node 为例,Node 测试镜像的设计要点:
- 基于官方
node:${NODE_VERSION}-slim,通过ARG NODE_VERSION支持多版本构建,Node 本身是矩阵轴、保留在基础镜像上; - 通过
MISE_DISABLE_TOOLS="node,deno,python,uv"禁用 mise 对 Node 的管理,避免遮蔽基础镜像的 Node; - 用 mise 安装 bun 与 pnpm,版本以仓库 mise.toml /
mise.lock为唯一来源; - 先复制 lockfile 与工作区配置,再复制已构建的
ts/packages/,最后复制 e2e 测试——利用 Docker 层缓存加速迭代; - 使用
bun build --compile将 CLI 编译为/usr/local/bin/composio二进制; - 预创建
/home/node/.composio目录并修正属主,规避 Bun 编译产物在 Docker 中静默建目录失败的问题(对应 Bun issue #7967); - 最终以非 root 用户
node运行。
DEBUG.log结构化输出
每个测试套件都会生成一份DEBUG.log,按运行时版本分组输出结构化结果。日志管理器DebugLogManager的实现位于 src/runner.ts:
================================================================================ E2E Test: openai-zod4-compat Started: 2026-01-30T12:18:42.000Z Test file: ts/e2e-tests/runtimes/node/openai-zod4-compat/e2e.test.ts Runtime versions: Node.js 22.22.3, Node.js 24.17.0, Node.js 25.9.0 ================================================================================ ################################################################################ ### Node.js 22.22.3 ################################################################################ Image: composio-e2e-node:22.22.3 --- Phase 1/2: setup --- Container: e2e-openai-zod4-compat-22-22-3-1769775520382-setup Command: npm install --legacy-peer-deps Duration: 2.55s Exit Code: 0 (success) [stdout] added 3 packages, and audited 5 packages in 2s [stderr] (empty) --- Phase 2/2: fixture --- Container: e2e-openai-zod4-compat-22-22-3-1769775520382-fixture Command: node index.mjs Duration: 0.56s Exit Code: 0 (success) [stdout] zod@4 works openai@5 works All packages work together! [stderr] (empty) ================================================================================ Summary ================================================================================ Node.js 22.22.3: PASS (2 phases, 3.11s total) Node.js 24.17.0: PASS (2 phases, 3.09s total) Node.js 25.9.0: PASS (2 phases, 3.08s total) Finished: 2026-01-30T12:18:46.500Z Total duration: 4.50s ================================================================================日志特性总结:
- 每次测试运行开始时清空日志文件,避免残留旧数据;
- 阶段(phase)按运行时版本分组,便于快速扫描;
- 视觉层级分明:
===表示文件边界、###表示版本、---表示阶段; - 空的 stdout/stderr 显示为
(empty); - 结尾提供包含 pass/fail/skip 状态与耗时的汇总段落。
运行行为与约束
- 构建隔离的 Docker 容器并在其中执行测试命令;
- Docker 为必装前置;
- 每个 Node 版本顺序执行测试(非并行);
- 卷清理为 best-effort,清理失败不会导致测试失败;
- 镜像预热:每个版本的
describe块在beforeAll中通过ensureNodeImage/ensureDenoImage/ensureCliImage确保镜像存在,超时上限 600 秒(见 src/runner.ts)。
扩展:安装型 E2E 配置
除运行时测试外,@e2e-tests/utils还导出了resolveInstallE2EConfig()(src/config.ts),用于验证composioCLI 的安装脚本(bash / zsh / fish 三种 shell,local / prod 两种模式),通过COMPOSIO_INSTALL_E2E_MODE、COMPOSIO_INSTALL_E2E_SHELL、COMPOSIO_INSTALL_E2E_VERSION、COMPOSIO_INSTALL_E2E_RELEASE_DIR四个环境变量控制,其中版本必须是latest或完整的@composio/cli@x.y.zrelease tag 格式。仓库根目录的 test/ 下也提供了对应的安装脚本单测(如install-sh-atomic-replace.test.sh)。
总结
ts/e2e-tests/_utils提供了一套"配置驱动 + Docker 隔离 + 多版本矩阵"的端到端测试范式:e2e()自动推断上下文,E2EConfig集中声明版本与环境,runCmd/runFixture提供命令级与 fixture 级执行原语,环境变量 fail-fast 校验与DEBUG.log结构化日志共同保障了 CI 场景的可观测性。理解这套工具的实现(尤其是 src/runner.ts 与 src/volume.ts),即可在ts/e2e-tests/runtimes/下按相同模式快速新增覆盖 Node、Deno 与 CLI 的端到端测试套件。
【免费下载链接】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),仅供参考