Composio E2E 测试工具链解析:在隔离 Docker 环境中运行 `@composio/core` 与 CLI 端到端测试
2026/9/11 15:39:34 网站建设 项目流程

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.denoDeno 测试环境镜像
Dockerfile.cli基于 scratch 的 CLI 测试环境镜像

从 src/index.ts 可以确认公共 API 面:它导出全部类型(E2EConfigE2ETestResultE2ETestResultWithSetupE2ETestResultWithFilesRunFixtureOptionsDefineTestsContext、各运行时版本元类型等)、e2e()主入口、sanitizeOutput/parseJsonStdout输出处理函数,以及安装型 E2E 的配置与镜像生命周期函数(resolveInstallE2EConfigcheckDockerensureInstallImagerunInstallContainer)。

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):

属性类型说明
versionsRuntimeVersions要测试的运行时版本,见下文
envRecord<string, string \| undefined>传递给 Docker 容器的环境变量,启动时会做校验
usesFixturesboolean为 true 时将 cwd 切换到{testDir}/fixtures,默认false
defineTests(ctx: DefineTestsContext) => void使用 bun:test 原语定义测试的回调

defineTests会在每个运行时版本的describe块内各调用一次,因此同一个测试文件即可完成多版本矩阵覆盖。

RuntimeVersionsDefineTestsContext

RuntimeVersions的三个字段均可省略,省略时各有默认来源:

属性类型默认来源
nodereadonly NodeVersionFromUser[]mise.toml中的 Node 版本
denoreadonly DenoVersionFromUser[]mise.toml中的 Deno 版本
clireadonly CliVersionFromUser[]ts/packages/cli/package.json的版本

DefineTestsContextdefineTests回调收到的上下文,包含三个成员:

属性类型/签名说明
runtime'node' \| 'deno' \| 'cli'当前被测运行时
runCmd(command: string) => Promise<E2ETestResult>在 Docker 容器中执行任意命令
runFixture(options: RunFixtureOptions) => Promise<E2ETestResult \| E2ETestResultWithSetup>运行 fixture,可选 setup 阶段

值得注意的类型约束:runCmdrunFixture都使用了泛型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清理。

结果类型:E2ETestResultE2ETestResultWithSetup

interface E2ETestResult { exitCode: number; // 命令退出码(0 为成功) stdout: string; // 捕获的 stdout stderr: string; // 捕获的 stderr }

E2ETestResultWithSetup在其基础上扩展了setup字段:

interface E2ETestResultWithSetup extends E2ETestResult { setup: { exitCode: number; stdout: string; stderr: string; }; }

顶层字段(exitCodestdoutstderr)反映fixture 阶段的结果,setup对象则记录 setup 命令的结果,便于分别断言两个阶段。

输出清洗与 JSON 解析:sanitizeOutput/parseJsonStdout

sanitizeOutput用于稳定化输出以便断言与快照比较(实现见 src/sanitize.ts):

import { sanitizeOutput } from '@e2e-tests/utils'; const clean = sanitizeOutput(result.stdout);

它依次完成三件事:

  1. 用正则\x1b\[[0-9;]*m移除 ANSI 转义序列(颜色/格式码);
  2. \r\n归一化为\n(Windows → Unix 换行);
  3. 对首尾空白做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):

  1. COMPOSIO_E2E_NODE_VERSION/COMPOSIO_E2E_DENO_VERSION/COMPOSIO_E2E_CLI_VERSION环境变量(最高优先级):本地模式下直接覆盖为单一版本;
  2. config.versions.*:使用配置数组;
  3. 默认值:Node/Deno 读取mise.toml(通过getMiseVersion()执行mise current <tool>),CLI 读取ts/packages/cli/package.jsonversion字段。

值得注意的实现细节:resolveNodeVersionMetaList()在检查环境变量之后才读取mise.toml,因此即使本机未安装 mise 工具链,环境变量覆盖也能正常工作。

预定义的 well-known 版本

src/const.ts 从仓库根 toolchain-versions.json 读取版本清单:

  • Node.js22.22.324.17.025.9.0,以及current(解析为 mise.toml 版本);
  • Deno2.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_KEYCOMPOSIO_API_KEYCOMPOSIO_BASE_URLOPENAI_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_MODECOMPOSIO_INSTALL_E2E_SHELLCOMPOSIO_INSTALL_E2E_VERSIONCOMPOSIO_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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询