Skip to content

使用 OpenClaw 插件 SDK 测试工具,可以为渠道和提供商插件编写单元测试和合约测试。测试工具从 openclaw/plugin-sdk/plugin-test-api 等聚焦子路径导入,避免使用已废弃的 openclaw/plugin-sdk/testing 宽桶。内置插件(in-repo)需通过 pnpm check 的 lint 检查:禁止桶导入、禁止直接 src/ 导入、禁止自我导入。运行合约测试用 pnpm test -- src/plugins/contracts/。本地测试内存不足时可设 OPENCLAW_VITEST_MAX_WORKERS=1 降低并发。

OpenClaw 插件 SDK 测试工具和合约测试怎么用

本文为 OpenClaw 插件作者提供 SDK 测试工具、测试模式、合约测试要求和 lint 规则。如果你正在为渠道插件或提供商插件编写测试,或者想理解内置插件的合约测试流程,可以参考下面各节。

需要测试示例? 操作指南中有完整的测试代码: 渠道插件测试提供商插件测试

测试工具(子路径导入)

下列子路径是 OpenClaw 仓库内供内置插件测试使用的本地源码入口,不是对外发布的 npm 包导出。第三方插件如果需要类似功能,请自行复制模式或联系社区。

子路径(Import)说明
openclaw/plugin-sdk/plugin-test-api插件 API mock:createTestPluginApi
openclaw/plugin-sdk/agent-runtime-test-contractsAgent 运行时合约 fixture:AUTH_PROFILE_RUNTIME_CONTRACT
openclaw/plugin-sdk/channel-contract-testing渠道入站/出站上下文合约断言
openclaw/plugin-sdk/channel-test-helpers渠道账号生命周期、动作、状态、配对等辅助函数
openclaw/plugin-sdk/channel-target-testing目标解析错误测试用例:installCommonResolveTargetErrorCases
openclaw/plugin-sdk/plugin-test-contracts插件注册合约检查:describePluginRegistrationContract
openclaw/plugin-sdk/plugin-test-runtime插件运行时 mock 与注册工具
openclaw/plugin-sdk/provider-test-contracts提供商系列运行时合约检查
openclaw/plugin-sdk/provider-http-test-mocks提供商 HTTP 和认证 mock
openclaw/plugin-sdk/test-env环境变量、本地 HTTP 服务器、fetch preconnect 等
openclaw/plugin-sdk/test-fixturesCLI 运行时捕获、模块热加载、沙箱上下文、技能写入等
openclaw/plugin-sdk/test-node-mocksNode 内置模块 mock

废弃说明: openclaw/plugin-sdk/testingopenclaw/plugin-sdk/test-utils 桶导入仅用于旧版兼容,仓库 lint 已拒绝新代码使用它们。新测试优先使用上面列出的聚焦子路径。

全部可用导出和用途见下表(完整列表,按子路径组织)。

导出用途子路径
createTestPluginApi构建最小插件 API mockplugin-sdk/plugin-test-api
AUTH_PROFILE_RUNTIME_CONTRACT原生 agent 运行时认证 profile 合约 fixtureplugin-sdk/agent-runtime-test-contracts
DELIVERY_NO_REPLY_RUNTIME_CONTRACT原生 agent 运行时禁止回复合约 fixture同上
OUTCOME_FALLBACK_RUNTIME_CONTRACT原生 agent 运行时 fallback 分类合约 fixture同上
createParameterFreeTool构建动态工具 schema fixture同上
expectChannelInboundContextContract断言渠道入站上下文结构plugin-sdk/channel-contract-testing
installChannelOutboundPayloadContractSuite安装出站 payload 合约用例同上
createStartAccountContext构建渠道账号生命周期上下文plugin-sdk/channel-test-helpers
installChannelActionsContractSuite安装通用渠道消息动作合约用例同上
installChannelSetupContractSuite安装通用渠道设置合约用例同上
installChannelStatusContractSuite安装通用渠道状态合约用例同上
expectDirectoryIds从目录列表函数断言渠道目录 ID同上
assertBundledChannelEntries断言内置渠道入口暴露正确公有合约同上
formatEnvelopeTimestamp格式化确定性信封时间戳同上
expectPairingReplyText断言渠道配对回复文本并提取代码同上
describePluginRegistrationContract安装插件注册合约检查plugin-sdk/plugin-test-contracts
registerSingleProviderPlugin注册一个提供商插件(loader smoke 测试)plugin-sdk/plugin-test-runtime
registerProviderPlugin捕获一个插件中的所有提供商种类同上
registerProviderPlugins跨多个插件捕获提供商注册同上
requireRegisteredProvider断言提供商集合中包含某个 id同上
createRuntimeEnv构建 mock CLI/插件运行时环境同上
createPluginSetupWizardStatus为渠道插件构建设置状态辅助同上
describeOpenAIProviderRuntimeContract安装提供商系列运行时合约检查plugin-sdk/provider-test-contracts
expectPassthroughReplayPolicy断言提供商重播策略透传自有工具和元数据同上
runRealtimeSttLiveTest使用共享音频 fixture 运行实时 STT 提供商测试同上
normalizeTranscriptForMatch格式化实时转录输出用于模糊断言同上
expectExplicitVideoGenerationCapabilities断言视频提供商声明显式生成模式能力同上
expectExplicitMusicGenerationCapabilities断言音乐提供商声明显式生成/编辑能力同上
mockSuccessfulDashscopeVideoTask模拟 DashScope 视频任务成功响应同上
getProviderHttpMocks获取可选的提供商 HTTP/auth Vitest mockplugin-sdk/provider-http-test-mocks
installProviderHttpMockCleanup每个测试后重置提供商 HTTP/auth mock同上
installCommonResolveTargetErrorCases目标解析错误处理的共享测试用例plugin-sdk/channel-target-testing
shouldAckReaction检查渠道是否应添加 ack 回应plugin-sdk/channel-feedback
removeAckReactionAfterReply回复投递后移除 ack 回应同上
createTestRegistry构建渠道插件注册表 fixtureplugin-sdk/plugin-test-runtimeplugin-sdk/channel-test-helpers
createEmptyPluginRegistry构建空注册表 fixture同上
setActivePluginRegistry为插件运行时测试安装注册表 fixture同上
createRequestCaptureJsonFetch捕获媒体辅助测试中的 JSON fetch 请求plugin-sdk/test-env
withServer通过一次性的本地 HTTP 服务器运行测试同上
createMockIncomingRequest构建最小入站 HTTP 请求对象同上
withFetchPreconnect安装 preconnect hooks 后运行 fetch 测试同上
withEnv / withEnvAsync临时修补环境变量同上
createTempHomeEnv / withTempHome / withTempDir创建隔离的文件系统测试 fixture同上
createMockServerResponse创建最小 HTTP 服务器响应 mock同上
createCliRuntimeCapture在测试中捕获 CLI 运行时输出plugin-sdk/test-fixtures
importFreshModule使用新鲜查询 token 导入 ESM 模块(绕过缓存)同上
bundledPluginRoot / bundledPluginFile解析内置插件源码或构建产物 fixture 路径同上
mockNodeBuiltinModule安装窄域 Node 内置模块 Vitest mockplugin-sdk/test-node-mocks
createSandboxTestContext构建沙箱测试上下文plugin-sdk/test-fixtures
writeSkill写入技能 fixture同上
makeAgentAssistantMessage构建 agent 转录消息 fixture同上
peekSystemEvents / resetSystemEventsForTest检查并重置系统事件 fixture同上
sanitizeTerminalText清洗终端输出用于断言同上
countLines / hasBalancedFences断言分块输出形状同上
runProviderCatalog执行提供商目录钩子(带测试依赖)同上
resolveProviderWizardOptions在合约测试中解析提供商设置向导选项同上
resolveProviderModelPickerEntries解析提供商模型选择项同上
buildProviderPluginMethodChoice为断言构建提供商向导选择 ID同上
setProviderWizardProvidersResolverForTest为隔离测试注入提供商向导解析器同上
createProviderUsageFetch构建提供商使用量 fetch fixture同上
useFrozenTime / useRealTime冻结或恢复定时器(用于时间敏感测试)plugin-sdk/test-env
createTestWizardPrompter构建 mock 设置向导提示器同上
createRuntimeTaskFlow创建隔离的运行时任务流状态同上
typedCases保留字面类型用于表驱动测试plugin-sdk/test-fixtures

类型导出

聚焦子路径也重新导出了测试文件中常用的类型:

typescript
import type {
  ChannelAccountSnapshot,
  ChannelGatewayContext,
} from "openclaw/plugin-sdk/channel-contract";
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
import type { MockFn, PluginRuntime, RuntimeEnv } from "openclaw/plugin-sdk/plugin-test-runtime";

测试目标解析

使用 installCommonResolveTargetErrorCases 添加标准错误用例:

typescript
import { describe } from "vitest";
import { installCommonResolveTargetErrorCases } from "openclaw/plugin-sdk/channel-target-testing";

describe("my-channel 目标解析", () => {
  installCommonResolveTargetErrorCases({
    resolveTarget: ({ to, mode, allowFrom }) => {
      // 你的渠道目标解析逻辑
      return myChannelResolveTarget({ to, mode, allowFrom });
    },
    implicitAllowFrom: ["user1", "user2"],
  });

  // 添加渠道特有的测试用例
  it("should resolve @username targets", () => {
    // ...
  });
});

测试模式

注册合约测试

只用手写 api mock 调用 register(api) 不会测试 OpenClaw 加载器的验收门。建议为每个注册表面添加至少一个基于 loader 的 smoke 测试,尤其对 hook 和独占能力(如 memory)更要测试。真正加载器会在缺少必要 metadata 或插件调用不属于它的能力 API(如 api.registerHook(...) 需要 hook 名称,api.registerMemoryCapability(...) 要求 manifest 或导出入口声明 kind: "memory")时拒绝注册。

测试运行时配置访问

测试内置渠道插件时,优先使用 openclaw/plugin-sdk/channel-test-helpers 中共享的运行时 mock。它的 runtime.config.loadConfig()runtime.config.writeConfigFile(...) mock 默认会抛出异常,帮助捕获对新兼容 API 的使用。只有测试明确覆盖旧兼容行为时才需要覆盖这些 mock。

渠道插件单元测试

typescript
import { describe, it, expect, vi } from "vitest";

describe("my-channel 插件", () => {
  it("should resolve account from config", () => {
    const cfg = {
      channels: {
        "my-channel": {
          token: "test-token",
          allowFrom: ["user1"],
        },
      },
    };

    const account = myPlugin.setup.resolveAccount(cfg, undefined);
    expect(account.token).toBe("test-token");
  });

  it("should inspect account without materializing secrets", () => {
    const cfg = {
      channels: {
        "my-channel": { token: "test-token" },
      },
    };

    const inspection = myPlugin.setup.inspectAccount(cfg, undefined);
    expect(inspection.configured).toBe(true);
    expect(inspection.tokenStatus).toBe("available");
    // 不暴露 token 值
    expect(inspection).not.toHaveProperty("token");
  });
});

提供商插件单元测试

typescript
import { describe, it, expect } from "vitest";

describe("my-provider 插件", () => {
  it("should resolve dynamic models", () => {
    const model = myProvider.resolveDynamicModel({
      modelId: "custom-model-v2",
      // ... context
    });

    expect(model.id).toBe("custom-model-v2");
    expect(model.provider).toBe("my-provider");
    expect(model.api).toBe("openai-completions");
  });

  it("should return catalog when API key is available", async () => {
    const result = await myProvider.catalog.run({
      resolveProviderApiKey: () => ({ apiKey: "test-key" }),
      // ... context
    });

    expect(result?.provider?.models).toHaveLength(2);
  });
});

Mock 插件运行时

对于使用 createPluginRuntimeStore 的代码,参考以下 mock 方式:

typescript
import { createPluginRuntimeStore } from "openclaw/plugin-sdk/runtime-store";
import type { PluginRuntime } from "openclaw/plugin-sdk/runtime-store";

const store = createPluginRuntimeStore<PluginRuntime>({
  pluginId: "test-plugin",
  errorMessage: "test runtime not set",
});

// 测试 setup 中
const mockRuntime = {
  agent: {
    resolveAgentDir: vi.fn().mockReturnValue("/tmp/agent"),
    // ... 其他 mock
  },
  config: {
    current: vi.fn(() => ({}) as const),
    mutateConfigFile: vi.fn(),
    replaceConfigFile: vi.fn(),
  },
  // ... 其他命名空间
} as unknown as PluginRuntime;

store.setRuntime(mockRuntime);

// 测试结束后
store.clearRuntime();

使用每实例 stub 测试

优先使用每实例 stub,而不是修改原型链:

typescript
// 推荐:每实例 stub
const client = new MyChannelClient();
client.sendMessage = vi.fn().mockResolvedValue({ id: "msg-1" });

// 避免:原型链修改
// MyChannelClient.prototype.sendMessage = vi.fn();

合约测试(内置插件)

内置插件有合约测试用于验证注册所有权:

bash
pnpm test -- src/plugins/contracts/

这些测试验证:

  • 哪些插件注册了哪些提供商
  • 哪些插件注册了哪些 speech 提供商
  • 注册形状正确性
  • 运行时合约合规性

运行特定范围的测试

针对具体插件:

bash
pnpm test -- <内置插件根目>/my-channel/

只运行合约测试:

bash
pnpm test -- src/plugins/contracts/shape.contract.test.ts
pnpm test -- src/plugins/contracts/auth-choice.contract.test.ts
pnpm test -- src/plugins/contracts/runtime-seams.contract.test.ts

Lint 规则(内置插件)

pnpm check 对内置插件强制执行三条规则:

  1. 禁止整桶根导入openclaw/plugin-sdk 根 barrel 导入会被拒绝
  2. 禁止直接 src/ 导入:插件不能直接导入 ../../src/ 路径
  3. 禁止自我导入:插件不能导入自身的 plugin-sdk/&lt;name&gt; 子路径

外部插件不受这些 lint 规则约束,但建议遵循相同模式。

测试配置

OpenClaw 使用 Vitest 并配置 V8 覆盖率阈值。常用命令:

bash
# 运行所有测试
pnpm test

# 运行具体插件测试
pnpm test -- <内置插件根目>/my-channel/src/channel.test.ts

# 按测试名称过滤运行
pnpm test -- <内置插件根目>/my-channel/ -t "resolves account"

# 运行并生成覆盖率报告
pnpm test:coverage

如果本地运行内存压力大:

bash
OPENCLAW_VITEST_MAX_WORKERS=1 pnpm test

相关文档

常见问题

如何为我的渠道插件编写单元测试?

参考上面的“渠道插件单元测试”代码块。关键步骤:使用 vitestdescribeit;测试配置解析、账号检查、消息投递逻辑。对于目标解析错误处理,可以调用 installCommonResolveTargetErrorCases 快速覆盖标准错误场景。确保从正确的子路径导入工具,如 openclaw/plugin-sdk/channel-target-testing

合约测试报错了,怎么排查?

合约测试位于 src/plugins/contracts/ 下,运行 pnpm test -- src/plugins/contracts/ 查看所有合约是否通过。常见失败原因:插件注册的提供商或语音提供商与实际实现不匹配;注册形状不正确(如缺少必要的 metadata);运行时合约不满足(如 auth profile 或 delivery-no-reply 行为不符)。可以单独运行某个合约文件缩小范围。

lint 检查失败,怎么修复?

pnpm check 会检查三条规则:确保没有从 openclaw/plugin-sdk 根导入;确保没有直接 import 仓库 src/ 目录下的文件;确保没有插件自我导入(如某插件内 import "...plugin-sdk/my-channel")。修改导入路径到具体的子路径即可。外部插件不受此规则约束,但建议同样避免。