跳转到内容

插件测试

OpenClaw 插件的测试工具、模式和 Lint 执行参考。

导入: openclaw/plugin-sdk/testing

测试子路径导出了一组有限的辅助工具,供插件作者使用:

import { installCommonResolveTargetErrorCases, shouldAckReaction, removeAckReactionAfterReply } from "openclaw/plugin-sdk/testing";
导出用途
installCommonResolveTargetErrorCases用于目标解析错误处理的共享测试用例
shouldAckReaction检查渠道是否应添加确认反应
removeAckReactionAfterReply在回复传递后移除确认反应

测试子路径还重新导出了测试文件中有用的类型:

import type { ChannelAccountSnapshot, ChannelGatewayContext, OpenClawConfig, PluginRuntime, RuntimeEnv, MockFn } from "openclaw/plugin-sdk/testing";

使用 installCommonResolveTargetErrorCases 为渠道目标解析添加标准错误用例:

import { describe } from "vitest";
import { installCommonResolveTargetErrorCases } from "openclaw/plugin-sdk/testing";
describe("my-channel target resolution", () => {
installCommonResolveTargetErrorCases({
resolveTarget: ({ to, mode, allowFrom }) => {
// Your channel's target resolution logic
return myChannelResolveTarget({ to, mode, allowFrom });
},
implicitAllowFrom: ["user1", "user2"],
});
// Add channel-specific test cases
it("should resolve @username targets", () => {
// ...
});
});
import { describe, it, expect, vi } from "vitest";
describe("my-channel plugin", () => {
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");
// No token value exposed
expect(inspection).not.toHaveProperty("token");
});
});
import { describe, it, expect } from "vitest";
describe("my-provider plugin", () => {
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);
});
});

对于使用 createPluginRuntimeStore 的代码,请在测试中模拟运行时:

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");
// In test setup
const mockRuntime = {
agent: {
resolveAgentDir: vi.fn().mockReturnValue("/tmp/agent"),
// ... other mocks
},
config: {
loadConfig: vi.fn(),
writeConfigFile: vi.fn(),
},
// ... other namespaces
} as unknown as PluginRuntime;
store.setRuntime(mockRuntime);
// After tests
store.clearRuntime();

优先使用每个实例的存根,而不是原型修改:

// Preferred: per-instance stub
const client = new MyChannelClient();
client.sendMessage = vi.fn().mockResolvedValue({ id: "msg-1" });
// Avoid: prototype mutation
// MyChannelClient.prototype.sendMessage = vi.fn();

捆绑插件具有验证注册所有权的合约测试:

Terminal window
pnpm test -- src/plugins/contracts/

这些测试断言:

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

对于特定插件:

Terminal window
pnpm test -- <bundled-plugin-root>/my-channel/

仅对于合约测试:

Terminal window
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.ts

对于仓库内插件,pnpm check 强制执行三条规则:

  1. 禁止单体根导入 — 拒绝 openclaw/plugin-sdk 根桶
  2. 禁止直接导入 src/ — 插件无法直接导入 ../../src/
  3. 禁止自导入 — 插件无法导入自己的 plugin-sdk/<name> 子路径

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

OpenClaw 使用 Vitest 和 V8 覆盖率阈值。对于插件测试:

Terminal window
# Run all tests
pnpm test
# Run specific plugin tests
pnpm test -- <bundled-plugin-root>/my-channel/src/channel.test.ts
# Run with a specific test name filter
pnpm test -- <bundled-plugin-root>/my-channel/ -t "resolves account"
# Run with coverage
pnpm test:coverage

如果本地运行导致内存压力:

Terminal window
OPENCLAW_TEST_PROFILE=low OPENCLAW_TEST_SERIAL_GATEWAY=1 pnpm test