Composio Mastra Provider 测试指南:从 wrapTool 到 Schema 兼容性的完整测试体系
【免费下载链接】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 的@composio/mastra包将 1000+ Composio 工具转换为 Mastra AI 框架的原生createTool格式,并内置执行能力。本文基于仓库中 ts/packages/providers/mastra/test/README.md 展开,逐层讲解该 provider 的单元测试覆盖范围、测试结构与 Mock 策略,并结合 src/index.ts 与各测试文件的源码实现,剖析 wrapTool/wrapTools/executeTool 的底层原理、strict 模式、输出 Schema 放宽(relaxation)以及$ref悬空引用的容错机制。读完本文,你将掌握如何运行与扩展这套测试,并理解 Mastra 与 Composio 集成时的类型转换、Schema 映射与错误处理全貌。
测试目录概览:为什么需要一个专属测试 README
@composio/mastra的测试位于 ts/packages/providers/mastra/test,该目录包含 5 个测试文件,覆盖 provider 从"身份声明"到"真实 Schema 编译"的完整契约:
- mastra.test.ts:主测试套件,覆盖 provider 属性、工具包装、工具集合、执行、MCP 转换、strict 模式与类型安全;
- mastra-ref.test.ts:针对真实
@mastra/schema-compat的$ref回归测试; - mastra-dangling-defs.test.ts:针对悬空
$ref(声明了引用却未定义$defs)的容错回归测试; - output-validation.integration.test.ts:验证输出 Schema 放宽后真实第三方 API 响应可通过校验的集成测试;
- relax-output-schema.test.ts:对
relaxOutputSchema纯函数的逐条规则测试。
这套测试的价值在于:它不只验证"函数不抛错",还验证"与 Mastra 的集成契约是否正确"——包括 Schema 是否经createTool正确传递、工具结果能否通过 Mastra 的validateToolOutput校验、类型推断是否保持安全。
Provider 属性测试:身份与能力的断言
测试套件首先验证 provider 的基础属性(见 mastra.test.ts):
- Name verification:断言
provider.name === 'mastra',对应 src/index.ts 中readonly name = 'mastra'的实现; - Agentic nature:断言
provider._isAgentic === true,确认该 provider 支持工具执行,是"可行动的"(agentic)provider,而非纯被动数据源。
这两条断言为整个集成定下基调:MastraProvider继承自BaseAgenticProvider<MastraToolCollection, MastraTool, MastraUrlMap>,是 Composio 统一 Provider 抽象中的"Mastra 方言"实现。
wrapTool:Composio 工具到 Mastra createTool 的转换
wrapTool是整套集成的核心。测试覆盖以下场景(mastra.test.ts):
- Basic wrapping:验证
createTool以正确的参数被调用,返回的对象具有id、description、inputSchema、outputSchema、execute五个关键字段; - Schema handling:验证输入/输出参数通过 Schema 转换器正确映射;
- Edge cases:工具缺失
description(回退为空字符串'')、缺失inputParameters(回退为空对象{})、缺失outputParameters(同理)时均能优雅处理; - Execution context:测试
execute闭包在空参数{}、缺失参数undefined、完整参数三种上下文下的行为——缺失参数会被规范化为{}而不是原样透传undefined(对应 issue #2406)。
底层调用链:从 Tool 到 MastraTool
对照 src/index.ts,wrapTool的实际流程是:
- 读取
tool.inputParameters作为输入 Schema; - strict 模式:若构造 provider 时传入
strict: true,调用toStrictJsonSchema将输入 Schema 规范化为 OpenAI structured outputs 契约(所有属性进required、对象闭合、可选属性放宽为可接受null);若 Schema 无法表达(如接受任意键的对象、allOf、prefixItems、未解析的$ref),则保留原 Schema 并输出一条logger.warn; - 解引用
$ref:调用dereferenceJsonSchema,以onUnresolved: 'sentinel'模式处理内部$ref指针,将悬空引用替换为宽松的对象 Schema({ type: 'object', additionalProperties: true }); - 兼容层转换:
applyCompatLayer({ schema, compatLayers: [], mode: 'jsonSchema' })将 JSON Schema 交给@mastra/schema-compat处理,供 Mastra 内部编译为 Zod; - 输出 Schema 放宽:对
tool.outputParameters先解引用、再经relaxOutputSchema放宽(详见下文); - 创建 Mastra 工具:
createTool({ id: tool.slug, description, inputSchema, outputSchema, execute }); - 执行包装:
execute闭包内部先normalizeToolArguments规范化参数(兼容模型把工具输入输出成 JSON 字符串的情况,issue #2406),再在 strict 模式下用omitNullToolArguments剔除工具自身 Schema 不接受null的参数,最后调用全局executeTool函数。
测试通过 MockcreateTool后取出.mock.calls[0][0].execute(getCreatedToolExecute辅助函数)来直接驱动execute闭包,从而验证参数规范化与版本透传行为——包括工具带version: '20250101_01'与不带版本两种情形,执行函数均以(tool.slug, inputData)形式调用全局执行器。
wrapTools:批量包装与集合键映射
wrapTools将工具数组归约为以工具 slug 为键的键值集合(mastra.test.ts):
- Multiple tools:多个工具逐个经
wrapTool包装,createTool被调用的次数与工具数一致; - Empty arrays:空数组返回空对象
{},且不会触发createTool; - Key mapping:集合键严格使用
tool.slug(如first-tool、second-tool、third-tool); - Duplicate handling:重复 slug 时后者覆盖前者,集合仅保留一个键。
实现见 src/index.ts:tools.reduce((acc, tool) => { acc[tool.slug] = this.wrapTool(tool, executeTool); return acc; }, {})。该返回值直接可传给 MastraAgent的tools字段。
executeTool:全局执行、Modifiers 与错误传递
executeTool是执行层入口,测试验证(mastra.test.ts):
- Global execution:调用
provider.executeTool(slug, params)会转发到_setExecuteToolFn注入的全局执行函数,第三个参数为undefined; - Modifiers support:
beforeExecute/afterExecute修饰器会原样作为第三个参数传给全局执行函数; - Error handling:当执行函数返回
{ data: null, error: { message: 'Tool execution failed' }, successful: false }时,executeTool将其原样透传,不抛异常、不吞错误。
Mastra 集成与类型安全测试
集成层测试(mastra.test.ts)重点验证三件事:
- Compatibility:包装结果具备
id、description、inputSchema、outputSchema、execute属性,其中execute必须是函数,且id与工具 slug 一致——这是 MastracreateTool的硬性契约; - Type safety:
MastraTool与MastraToolCollection的类型定义(见 src/index.ts)在测试中通过类型断言验证;MastraToolCollection是字符串键对象而非数组; - Minimal tools:仅含 slug/name/description/tags 的最小工具也能包装成功,缺失的 Schema 全部回退为空对象。
错误处理:执行失败与畸形 Schema
错误路径测试(mastra.test.ts)确保鲁棒性:
- Execution failures:
execute闭包内执行函数rejects时,异常向上抛出,测试用rejects.toThrow('Execution failed')断言; - Malformed schemas:
inputParameters为null、outputParameters为undefined的工具调用wrapTool不会抛异常(Schema 回退为空对象)。
strict 模式测试:OpenAI structured outputs 契约
strict 模式是MastraProvider的可选能力(new MastraProvider({ strict: true })),测试覆盖(mastra.test.ts):
- 默认关闭:不传参构造时
strict === false; - required-nullable 语义:开启后,可选属性
optional_field: { type: 'string' }被改写为{ type: ['string', 'null'] }并加入required,对象追加additionalProperties: false——即"所有属性必填,但可选属性放宽为接受 null"; - 不可表达的工具保留原 Schema:如
headers: { type: 'object', additionalProperties: { type: 'string' } }(接受任意键的 map)无法表达为闭合对象,则整体保留原 Schema; - null 参数剔除:strict 模式下,工具自身 Schema 不接受
null的参数(如cfg.note: null)在执行前被剔除,而 Schema 本身声明可空(type: ['string', 'null'])的参数(如clearable: null)保留; - 非对象/缺失参数:
inputParameters为字符串 Schema 或undefined时原样/回退处理,不因 strict 模式崩溃; - wrapTools 联动:strict 模式下批量包装仍以 slug 为键,且每个工具的 Schema 都应用 required-nullable 改写。
输出 Schema 放宽:让真实第三方 API 响应通过校验
这是整个 provider 最具价值的设计之一。Mastra 会通过validateToolOutput用outputSchema校验每个工具结果,不匹配就丢弃数据并替换为错误。而 Composio API 下发的输出 Schema 是严格的:可选字段被声明为非空原始类型、对象带additionalProperties: false。真实第三方 API(Linear、Notion、Jira、Slack 等)常对未设置的字段返回null,偶尔还返回多余键,导致原本合法的响应被拒、模型看到的工具输出被截断。
解决方案是 relax-output-schema.ts 中的relaxOutputSchema纯函数,对输出 Schema 做四项"只放宽、不收紧"的改写:
- 所有类型节点可空化:
type: 'string'→['string', 'null'];已含null的类型数组不重复追加; additionalProperties: false(或未设置)的对象允许额外键:改为true;若additionalProperties本身是 Schema,则递归放宽;enum/const放宽为接受null:const: 'fixed'变为enum: ['fixed', null],enum: ['open', 'closed']变为enum: ['open', 'closed', null];- 删除
required:真实 API 对未设置的字段是直接省略而非返回null,强求字段存在会拒掉合法输出。
该函数递归遍历items、anyOf、oneOf、allOf、properties、$defs等所有子 Schema 位置,并刻意不处理not关键字——not是否定语义,放宽其内层 Schema 反而会收窄父级可接受的值域,违背"只放宽"的不变量(relax-output-schema.test.ts 专门回归验证)。同时函数不修改输入对象(不可变),null/undefined原样透传。
output-validation.integration.test.ts 用真实@mastra/schema-compat跑完整链路relaxOutputSchema → applyCompatLayer → convertSchemaToZod → parse,证明:严格 Schema 拒绝含null可选字段的真实响应(复现 bug),放宽后同一响应success === true且null与额外键完整保留、无数据截断;enum放宽后仍拒绝非法值('nope');not关键字下此前合法的值(null、数字)依旧合法。
$ref回归测试:解引用与悬空引用容错
Schema 中的$ref是另一个真实痛点,对应两个回归测试文件:
- mastra-ref.test.ts 使用真实
@mastra/schema-compat(不 Mock),验证dereferenceJsonSchema后:$defs中User.id的类型信息被保留(type: 'string',而非被降级为宽容的原始类型anyOf);输出侧definitions(Draft-7 写法)同样被保留(放宽后为['string', 'null']);包装后的 Schema 中不再残留任何$ref字符串; - mastra-dangling-defs.test.ts 针对 issue #3307:Composio API 下发的部分工具(如
GMAIL_FETCH_EMAILS)声明了$ref: '#/$defs/...'却从未定义$defs,严格解引用会抛异常并让tools.get直接崩溃。Provider 通过onUnresolved: 'sentinel'将悬空分支替换为宽松对象 Schema,并保证:- 包装不抛异常、输出 Schema 中无残留
$ref; - 每个
(toolSlug, ref)对只输出一条logger.warn(warnedDanglingRefs集合去重),提示降级为宽松校验并指向 issue 跟踪; - 不同 slug 携带相同 ref 时分别告警;
- 告警内容中的用户可控片段(slug、toolkit、ref)经
JSON.stringify转义,中和换行/ANSI 转义/控制字节,防止日志伪造(CWE-117 回归测试); - 同时向 telemetry 发送
composio.mastra.wrapTool.danglingRef聚合事件(每对一次,可被COMPOSIO_DISABLE_TELEMETRY=true关闭),可解析的$ref不触发任何告警与遥测。
- 包装不抛异常、输出 Schema 中无残留
测试结构:Vitest 约定与 Mock 策略
按 test/README.md 的说明,测试遵循 Vitest 约定:
- Comprehensive mocking:对
@mastra/core(具体是createTool)与@composio/core(jsonSchemaToModel,即 Schema 转换)使用vi.mock()隔离外部依赖; - Setup and teardown:
beforeEach中重新构造MastraProvider、重置 Mock 工具与执行函数、vi.clearAllMocks(),保证每个用例干净隔离; - Detailed assertions:成功与失败场景均有详尽断言(
expect(createTool).toHaveBeenCalledWith({...})全量比对调用参数); - Type-safe implementations:测试内定义
MockedMastraTool、CreateToolMockConfig等接口,保持类型安全。
值得注意的是 Vitest 的 Mock 作用域按文件隔离:mastra.test.ts顶层vi.mock('@mastra/schema-compat')不会泄漏到mastra-ref.test.ts与mastra-dangling-defs.test.ts,因此这两个文件能对真实@mastra/schema-compat(基于 AJV 的编译链路)做端到端验证。此外,由于@mastra/core的createTool(≥1.43)会把 JSON Schema 包装进JsonSchemaWrapper,测试用getSchema()辅助函数穿透包装层,检查真正编译产物的 JSON Schema。
运行测试
按 test/README.md 与 package.json 的脚本配置:
# 在 mastra provider 目录内运行(等价于 vitest run) npm test # 从工作区根目录运行,仅过滤 @composio/mastra 包 pnpm test --filter=@composio/mastrapackage.json中test: "vitest run"、typecheck: "tsc --noEmit --skipLibCheck"可配合使用。注意该包要求node >= 22.22.3,peer 依赖为@composio/core >= 0.10.0、@mastra/core ^1.46.0、zod ^3.25 || ^4,运行前需确保依赖满足。
扩展测试的建议
结合上述源码,若需为 provider 新增测试,可参考以下切入点:
- 新的 Schema 关键字放宽规则:在
relaxOutputSchema中增加规则时,先在 relax-output-schema.test.ts 补纯函数用例,再在 output-validation.integration.test.ts 补真实编译链路用例,保证"只放宽"不变量; - 新的执行上下文形态:在
execute闭包新增参数规范化逻辑时,参照 issue #2406 的测试模式,同时断言对象输入与 JSON 字符串输入两条路径; - strict 模式边界:凡新增"strict 模式无法表达"的 Schema 形态,应断言其保留原 Schema 且只产生一次告警,避免日志刷屏;
$ref场景:仿照mastra-dangling-defs.test.ts构造悬空/可解析/恶意 ref 三类 fixture,同时验证告警去重、CWE-117 转义与 telemetry 事件计数。
小结
test/README.md 用一份精炼的清单勾勒了@composio/mastra的测试全貌:provider 属性、工具包装、集合映射、执行链路、Mastra 集成契约、错误处理与类型安全。而测试文件本身则沉淀了四个真实生产问题的回归保障——模型输出 JSON 字符串参数(#2406)、严格输出 Schema 截断第三方响应(#3047)、悬空$ref导致tools.get崩溃(#3307)与日志注入(CWE-117)。理解这套测试,等于同时理解了 Mastra Provider 的 Schema 转换管线与容错哲学:对输入做结构化约束(strict 模式),对输出做宽容校验(relaxation),对上游数据缺陷做降级不崩溃。
【免费下载链接】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),仅供参考