Appearance
插件测试
OpenClaw 插件的测试工具、测试模式和 lint 规则参考文档。
测试工具
导入路径: openclaw/plugin-sdk/testing
testing 子路径导出一组专为插件作者设计的辅助函数:
typescript
import {
installCommonResolveTargetErrorCases,
shouldAckReaction,
removeAckReactionAfterReply,
} from "openclaw/plugin-sdk/testing";可用导出
| 导出 | 用途 |
|---|---|
installCommonResolveTargetErrorCases | 目标解析错误处理的通用测试用例 |
shouldAckReaction | 检查渠道是否应添加 ack 回应 |
removeAckReactionAfterReply | 消息发送后移除 ack 回应 |
类型
testing 子路径还重新导出了测试文件中常用的类型:
typescript
import type {
ChannelAccountSnapshot,
ChannelGatewayContext,
OpenClawConfig,
PluginRuntime,
RuntimeEnv,
MockFn,
} from "openclaw/plugin-sdk/testing";测试目标解析
使用 installCommonResolveTargetErrorCases 为渠道目标解析添加标准错误用例:
typescript
import { describe } from "vitest";
import { installCommonResolveTargetErrorCases } from "openclaw/plugin-sdk/testing";
describe("my-channel 目标解析", () => {
installCommonResolveTargetErrorCases({
resolveTarget: ({ to, mode, allowFrom }) => {
// 你的渠道目标解析逻辑
return myChannelResolveTarget({ to, mode, allowFrom });
},
implicitAllowFrom: ["user1", "user2"],
});
// 添加渠道特有的测试用例
it("should resolve @username targets", () => {
// ...
});
});测试模式
渠道插件单元测试
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>("test runtime not set");
// 测试 setup 中
const mockRuntime = {
agent: {
resolveAgentDir: vi.fn().mockReturnValue("/tmp/agent"),
// ... 其他 mock
},
config: {
loadConfig: vi.fn(),
writeConfigFile: 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/这些测试验证:
- 哪些插件注册了哪些提供商
- 哪些插件注册了哪些语音提供商
- 注册形状是否正确
- 运行时合约合规性
运行特定范围的测试
针对特定插件运行测试,给你的小龙虾做体检:
bash
pnpm test -- extensions/my-channel/只运行合约测试:
bash
pnpm test -- src/plugins/contracts/shape.contract.test.ts
pnpm test -- src/plugins/contracts/auth.contract.test.ts
pnpm test -- src/plugins/contracts/runtime.contract.test.tsLint 规则(内置插件)
pnpm check 对内置插件强制执行三条规则:
- 禁止整体根路径导入 —
openclaw/plugin-sdk根 barrel 会被拒绝 - 禁止直接导入
src/— 插件不能直接导入../../src/ - 禁止自我导入 — 插件不能导入自身的
plugin-sdk/<name>子路径
外部插件不受这些 lint 规则约束,但建议遵循相同的模式。
测试配置
OpenClaw 使用 Vitest 并配置 V8 覆盖率阈值。运行插件测试的命令:
bash
# 运行所有测试
pnpm test
# 运行特定插件的测试
pnpm test -- extensions/my-channel/src/channel.test.ts
# 使用名称过滤器运行
pnpm test -- extensions/my-channel/ -t "resolves account"
# 生成覆盖率报告
pnpm test:coverage如果本地运行出现内存压力:
bash
OPENCLAW_TEST_PROFILE=low OPENCLAW_TEST_SERIAL_GATEWAY=1 pnpm test