插件测试
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";测试目标解析
Section titled “测试目标解析”使用 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", () => { // ... });});渠道插件的单元测试
Section titled “渠道插件的单元测试”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"); });});提供商插件的单元测试
Section titled “提供商插件的单元测试”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); });});模拟插件运行时
Section titled “模拟插件运行时”对于使用 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 setupconst 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 testsstore.clearRuntime();使用每个实例的存根进行测试
Section titled “使用每个实例的存根进行测试”优先使用每个实例的存根,而不是原型修改:
// Preferred: per-instance stubconst client = new MyChannelClient();client.sendMessage = vi.fn().mockResolvedValue({ id: "msg-1" });
// Avoid: prototype mutation// MyChannelClient.prototype.sendMessage = vi.fn();合约测试(仓库内插件)
Section titled “合约测试(仓库内插件)”捆绑插件具有验证注册所有权的合约测试:
pnpm test -- src/plugins/contracts/这些测试断言:
- 哪些插件注册了哪些提供商
- 哪些插件注册了哪些语音提供商
- 注册形状的正确性
- 运行时合约合规性
运行范围限定测试
Section titled “运行范围限定测试”对于特定插件:
pnpm test -- <bundled-plugin-root>/my-channel/仅对于合约测试:
pnpm test -- src/plugins/contracts/shape.contract.test.tspnpm test -- src/plugins/contracts/auth.contract.test.tspnpm test -- src/plugins/contracts/runtime.contract.test.tsLint 执行(仓库内插件)
Section titled “Lint 执行(仓库内插件)”对于仓库内插件,pnpm check 强制执行三条规则:
- 禁止单体根导入 — 拒绝
openclaw/plugin-sdk根桶 - 禁止直接导入
src/— 插件无法直接导入../../src/ - 禁止自导入 — 插件无法导入自己的
plugin-sdk/<name>子路径
外部插件不受这些 Lint 规则约束,但建议遵循相同的模式。
OpenClaw 使用 Vitest 和 V8 覆盖率阈值。对于插件测试:
# Run all testspnpm test
# Run specific plugin testspnpm test -- <bundled-plugin-root>/my-channel/src/channel.test.ts
# Run with a specific test name filterpnpm test -- <bundled-plugin-root>/my-channel/ -t "resolves account"
# Run with coveragepnpm test:coverage如果本地运行导致内存压力:
OPENCLAW_TEST_PROFILE=low OPENCLAW_TEST_SERIAL_GATEWAY=1 pnpm test