Skip to main content

插件测试

OpenClaw 插件的测试工具、模式以及 lint 强制规则参考。
在找测试示例吗? 操作指南中包含了完整的测试示例: 渠道插件测试提供商插件测试

测试工具

导入: openclaw/plugin-sdk/testing testing 子路径为插件作者导出了一组精简的辅助工具:
import {
  installCommonResolveTargetErrorCases,
  shouldAckReaction,
  removeAckReactionAfterReply,
} from "openclaw/plugin-sdk/testing";

可用导出

导出用途
installCommonResolveTargetErrorCases目标解析错误处理的共享测试用例
shouldAckReaction检查渠道是否应添加 ack reaction
removeAckReactionAfterReply在回复投递后移除 ack reaction

类型

testing 子路径还会重新导出在测试文件中有用的类型:
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 }) => {
      // 你的渠道目标解析逻辑
      return myChannelResolveTarget({ to, mode, allowFrom });
    },
    implicitAllowFrom: ["user1", "user2"],
  });

  // 添加渠道特定测试用例
  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");
    // 不暴露 token 值
    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);
  });
});

Mock 插件运行时

对于使用 createPluginRuntimeStore 的代码,请在测试中 mock 运行时:
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");

// 在测试设置中
const mockRuntime = {
  agent: {
    resolveAgentDir: vi.fn().mockReturnValue("/tmp/agent"),
    // ... 其他 mocks
  },
  config: {
    loadConfig: vi.fn(),
    writeConfigFile: 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/
这些测试会断言:
  • 哪些插件注册了哪些提供商
  • 哪些插件注册了哪些语音提供商
  • 注册形状是否正确
  • 运行时契约是否合规

运行范围限定测试

对于某个特定插件:
pnpm test -- <bundled-plugin-root>/my-channel/
仅运行契约测试:
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

Lint 强制规则(仓库内插件)

pnpm check 会对仓库内插件强制执行三条规则:
  1. 禁止使用单体根导入 —— 会拒绝 openclaw/plugin-sdk 根 barrel
  2. 禁止直接导入 src/ —— 插件不能直接导入 ../../src/
  3. 禁止自导入 —— 插件不能导入它们自己的 plugin-sdk/<name> 子路径
外部插件不受这些 lint 规则约束,但仍建议遵循相同 模式。

测试配置

OpenClaw 使用带有 V8 覆盖率阈值的 Vitest。对于插件测试:
# 运行所有测试
pnpm test

# 运行特定插件测试
pnpm test -- <bundled-plugin-root>/my-channel/src/channel.test.ts

# 使用特定测试名过滤器运行
pnpm test -- <bundled-plugin-root>/my-channel/ -t "resolves account"

# 运行覆盖率
pnpm test:coverage
如果本地运行导致内存压力:
OPENCLAW_VITEST_MAX_WORKERS=1 pnpm test

相关内容