使用 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-contracts Agent 运行时合约 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-fixtures CLI 运行时捕获、模块热加载、沙箱上下文、技能写入等
openclaw/plugin-sdk/test-node-mocks Node 内置模块 mock

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

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

导出 用途 子路径
createTestPluginApi 构建最小插件 API mock plugin-sdk/plugin-test-api
AUTH_PROFILE_RUNTIME_CONTRACT 原生 agent 运行时认证 profile 合约 fixture plugin-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 mock plugin-sdk/provider-http-test-mocks
installProviderHttpMockCleanup 每个测试后重置提供商 HTTP/auth mock 同上
installCommonResolveTargetErrorCases 目标解析错误处理的共享测试用例 plugin-sdk/channel-target-testing
shouldAckReaction 检查渠道是否应添加 ack 回应 plugin-sdk/channel-feedback
removeAckReactionAfterReply 回复投递后移除 ack 回应 同上
createTestRegistry 构建渠道插件注册表 fixture plugin-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 mock plugin-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

类型导出

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

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 添加标准错误用例:

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。

渠道插件单元测试

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");
  });
});

提供商插件单元测试

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 方式:

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,而不是修改原型链:

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

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

合约测试(内置插件)

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

pnpm test -- src/plugins/contracts/

这些测试验证:

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

运行特定范围的测试

针对具体插件:

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

只运行合约测试:

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 覆盖率阈值。常用命令:

# 运行所有测试
pnpm test

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

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

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

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

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")。修改导入路径到具体的子路径即可。外部插件不受此规则约束,但建议同样避免。