Test OpenClaw Plugins: Utilities and Patterns Guide
Esta página aún no está disponible en tu idioma.
Writing tests for plugins can feel like a chore, especially when you are dealing with complex integrations or external APIs. We have all been there—staring at a failing test suite because a mock did not behave or a target resolution failed. To make your life easier, OpenClaw provides a set of utilities and patterns designed to keep your plugin code reliable without the headache.
If you are looking for specific examples, the how-to guides include worked test examples for Channel plugin tests and Provider plugin tests.
Test utilities
Section titled “Test utilities”You can find the main testing helpers in the openclaw/plugin-sdk/testing subpath. This exports a narrow set of tools to help you handle common plugin tasks.
import { installCommonResolveTargetErrorCases, shouldAckReaction, removeAckReactionAfterReply,} from "openclaw/plugin-sdk/testing";Available exports
Section titled “Available exports”| Export | Purpose |
|---|---|
installCommonResolveTargetErrorCases | Shared test cases for target resolution error handling |
shouldAckReaction | Check whether a channel should add an ack reaction |
removeAckReactionAfterReply | Remove ack reaction after reply delivery |
The testing subpath also re-exports types you will need in your test files:
import type { ChannelAccountSnapshot, ChannelGatewayContext, OpenClawConfig, PluginRuntime, RuntimeEnv, MockFn,} from "openclaw/plugin-sdk/testing";Testing target resolution
Section titled “Testing target resolution”You should use installCommonResolveTargetErrorCases to add standard error cases for channel target resolution. This ensures your channel handles edge cases consistently.
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", () => { // ... });});Testing patterns
Section titled “Testing patterns”Unit testing a channel plugin
Section titled “Unit testing a channel plugin”When testing a channel plugin, you want to verify that accounts resolve correctly from the configuration and that secrets remain protected.
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"); });});Unit testing a provider plugin
Section titled “Unit testing a provider plugin”For provider plugins, focus on verifying dynamic model resolution and ensuring the catalog behaves correctly when API keys are present.
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); });});Mocking the plugin runtime
Section titled “Mocking the plugin runtime”If your code uses createPluginRuntimeStore, you need to mock the runtime in your tests to simulate the environment.
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();Testing with per-instance stubs
Section titled “Testing with per-instance stubs”It is better to use per-instance stubs rather than mutating prototypes. This keeps your tests isolated and prevents side effects.
// Preferred: per-instance stubconst client = new MyChannelClient();client.sendMessage = vi.fn().mockResolvedValue({ id: "msg-1" });
// Avoid: prototype mutation// MyChannelClient.prototype.sendMessage = vi.fn();Contract tests (in-repo plugins)
Section titled “Contract tests (in-repo plugins)”Bundled plugins include contract tests. These verify that registration ownership and runtime compliance are correct.
pnpm test -- src/plugins/contracts/These tests check:
- Which plugins register which providers
- Which plugins register which speech providers
- Registration shape correctness
- Runtime contract compliance
Running scoped tests
Section titled “Running scoped tests”You can run tests for a specific plugin or focus only on contract tests.
pnpm test -- <bundled-plugin-root>/my-channel/For contract tests only:
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 enforcement (in-repo plugins)
Section titled “Lint enforcement (in-repo plugins)”If you are working on in-repo plugins, pnpm check enforces three specific rules to keep the codebase clean:
- No monolithic root imports — You cannot use the
openclaw/plugin-sdkroot barrel. - No direct
src/imports — Plugins are not allowed to import../../src/directly. - No self-imports — Plugins cannot import their own
plugin-sdk/<name>subpath.
While external plugins do not have to follow these rules, sticking to these patterns is a good idea.
Test configuration
Section titled “Test configuration”OpenClaw uses Vitest. You can run tests for the whole project or target specific files and filters.
# 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:coverageIf you run into memory issues during local runs, use these environment variables:
OPENCLAW_TEST_PROFILE=low OPENCLAW_TEST_SERIAL_GATEWAY=1 pnpm testRelated
Section titled “Related”- SDK Overview — import conventions
- SDK Channel Plugins — channel plugin interface
- SDK Provider Plugins — provider plugin hooks
- Building Plugins — getting started guide
Next Steps
Section titled “Next Steps”- Check out the SDK Overview to understand how imports work.
- Learn about Building Plugins if you are just starting.
Need more help? Try the AI Setup Assistant.
OpenClaw Expert
Sigues atascado?
Si esta pagina no resolvio tu caso, pregunta a OpenClaw Expert para pasos concretos.