Appearance
使用 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/testing和openclaw/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-runtime 或 plugin-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 |
类型导出
聚焦子路径也重新导出了测试文件中常用的类型:
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.tsLint 规则(内置插件)
pnpm check 对内置插件强制执行三条规则:
- 禁止整桶根导入:
openclaw/plugin-sdk根 barrel 导入会被拒绝 - 禁止直接
src/导入:插件不能直接导入../../src/路径 - 禁止自我导入:插件不能导入自身的
plugin-sdk/<name>子路径
外部插件不受这些 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相关文档
常见问题
如何为我的渠道插件编写单元测试?
参考上面的“渠道插件单元测试”代码块。关键步骤:使用 vitest 的 describe 和 it;测试配置解析、账号检查、消息投递逻辑。对于目标解析错误处理,可以调用 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")。修改导入路径到具体的子路径即可。外部插件不受此规则约束,但建议同样避免。