OpenClaw Plugin Testing Utilities and Patterns
Reference for testing OpenClaw plugins, covering test utilities, patterns, and lint enforcement. Essential for plugin developers writing or maintaining tests.
Read this when
- You are writing tests for a plugin
- You need test utilities from the plugin SDK
- You want to understand contract tests for bundled plugins
Reference material covering test utilities, patterns, and lint enforcement for OpenClaw plugins.
Tip
Need test examples? The how-to guides provide worked examples: Channel plugin tests and Provider plugin tests.
Test utilities
These subpaths act as repo-local source entrypoints for OpenClaw's bundled plugin tests. They are not exposed as package.json exports for third-party plugins, and they may rely on Vitest or other repo-only test dependencies.
import {
shouldAckReaction,
removeAckReactionAfterReply,
} from "openclaw/plugin-sdk/channel-feedback";
import { installCommonResolveTargetErrorCases } from "openclaw/plugin-sdk/channel-target-testing";
import { AUTH_PROFILE_RUNTIME_CONTRACT } from "openclaw/plugin-sdk/agent-runtime-test-contracts";
import { createTestPluginApi } from "openclaw/plugin-sdk/plugin-test-api";
import { expectChannelInboundContextContract } from "openclaw/plugin-sdk/channel-contract-testing";
import { createStartAccountContext } from "openclaw/plugin-sdk/channel-test-helpers";
import { describePluginRegistrationContract } from "openclaw/plugin-sdk/plugin-test-contracts";
import { registerSingleProviderPlugin } from "openclaw/plugin-sdk/plugin-test-runtime";
import { describeOpenAIProviderRuntimeContract } from "openclaw/plugin-sdk/provider-test-contracts";
import { getProviderHttpMocks } from "openclaw/plugin-sdk/provider-http-test-mocks";
import { createOpenClawTestState } from "openclaw/plugin-sdk/test-state";
import { withEnv, withFetchPreconnect, withServer } from "openclaw/plugin-sdk/test-env";
import { isLiveTestEnabled } from "openclaw/plugin-sdk/test-live";
import { createRequestCaptureJsonFetch } from "openclaw/plugin-sdk/test-media-understanding";
import {
bundledPluginRoot,
createCliRuntimeCapture,
runDirectImportSmoke,
typedCases,
} from "openclaw/plugin-sdk/test-fixtures";
import { mockNodeBuiltinModule } from "openclaw/plugin-sdk/test-node-mocks";
For bundled plugin tests, stick to these focused subpaths. The earlier openclaw/plugin-sdk/testing barrel was repo-local, omitted from shipped packages, and has since been removed. The previous openclaw/plugin-sdk/test-utils alias was also dropped at that time. pnpm run lint:plugins:no-extension-test-core-imports (scripts/check-no-extension-test-core-imports.ts) keeps extension tests on the focused test subpaths listed above.
Available exports
| Export | Purpose |
|---|---|
createTestPluginApi | Construct a minimal mock of the plugin API for direct registration unit tests. Pull it in via plugin-sdk/plugin-test-api |
AUTH_PROFILE_RUNTIME_CONTRACT | Reusable auth-profile contract fixture meant for native agent runtime adapters. Source it from plugin-sdk/agent-runtime-test-contracts |
DELIVERY_NO_REPLY_RUNTIME_CONTRACT | Reusable delivery suppression contract fixture meant for native agent runtime adapters. Source it from plugin-sdk/agent-runtime-test-contracts |
OUTCOME_FALLBACK_RUNTIME_CONTRACT | Reusable fallback-classification contract fixture meant for native agent runtime adapters. Source it from plugin-sdk/agent-runtime-test-contracts |
createParameterFreeTool | Generate dynamic-tool schema fixtures for native runtime contract tests. Source it from plugin-sdk/agent-runtime-test-contracts |
expectChannelInboundContextContract | Verify the shape of channel inbound context. Source it from plugin-sdk/channel-contract-testing |
installChannelOutboundPayloadContractSuite | Load contract cases for channel outbound payloads. Source it from plugin-sdk/channel-contract-testing |
createStartAccountContext | Create lifecycle contexts for channel accounts. Source it from plugin-sdk/channel-test-helpers |
installChannelActionsContractSuite | Load generic contract cases for channel message actions. Source it from plugin-sdk/channel-test-helpers |
installChannelSetupContractSuite | Load generic contract cases for channel setup. Source it from plugin-sdk/channel-test-helpers |
installChannelStatusContractSuite | Load generic contract cases for channel status. Source it from plugin-sdk/channel-test-helpers |
expectDirectoryIds | Verify channel directory ids from a directory-list function. Source it from plugin-sdk/channel-test-helpers |
formatEnvelopeTimestamp | Produce deterministic envelope timestamps. Source it from plugin-sdk/channel-test-helpers |
expectPairingReplyText | Verify channel pairing reply text and pull out its code. Source it from plugin-sdk/channel-test-helpers |
describePluginRegistrationContract | Load checks for plugin registration contracts. Source it from plugin-sdk/plugin-test-contracts |
registerSingleProviderPlugin | Register a single provider plugin in loader smoke tests. Source it from plugin-sdk/plugin-test-runtime |
registerProviderPlugin | Collect every provider kind from one plugin. Source it from plugin-sdk/plugin-test-runtime |
registerProviderPlugins | Collect provider registrations from multiple plugins. Source it from plugin-sdk/plugin-test-runtime |
requireRegisteredProvider | Confirm a provider collection holds a given id. Source it from plugin-sdk/plugin-test-runtime |
createRuntimeEnv | Construct a mocked CLI and plugin runtime environment. Source it from plugin-sdk/plugin-test-runtime |
createPluginRuntimeMock | Construct a mocked plugin runtime surface. Source it from plugin-sdk/plugin-test-runtime |
createPluginSetupWizardStatus | Build setup status helpers for channel plugins. Source it from plugin-sdk/plugin-test-runtime |
createTestWizardPrompter | Construct a mocked setup wizard prompter. Source it from plugin-sdk/plugin-test-runtime |
createRuntimeTaskFlow | Set up isolated runtime task-flow state. Source it from plugin-sdk/plugin-test-runtime |
runProviderCatalog | Run a provider catalog hook with test dependencies. Source it from plugin-sdk/plugin-test-runtime |
resolveProviderWizardOptions | Determine provider setup wizard choices in contract tests. Source it from plugin-sdk/plugin-test-runtime |
resolveProviderModelPickerEntries | Determine provider model-picker entries in contract tests. Source it from plugin-sdk/plugin-test-runtime |
buildProviderPluginMethodChoice | Generate provider wizard choice ids for assertions. Source it from plugin-sdk/plugin-test-runtime |
setProviderWizardProvidersResolverForTest | Supply provider wizard providers for isolated tests. Source it from plugin-sdk/plugin-test-runtime |
describeOpenAIProviderRuntimeContract | Load runtime contract checks for provider families. Source it from plugin-sdk/provider-test-contracts |
expectPassthroughReplayPolicy | Verify that provider replay policies forward provider-owned tools and metadata. Pull from plugin-sdk/provider-test-contracts |
runRealtimeSttLiveTest | Execute a live realtime STT provider test using shared audio fixtures. Pull from plugin-sdk/provider-test-contracts |
normalizeTranscriptForMatch | Standardize live transcript output ahead of fuzzy assertions. Pull from plugin-sdk/provider-test-contracts |
expectExplicitVideoGenerationCapabilities | Confirm video providers advertise explicit generation mode capabilities. Pull from plugin-sdk/provider-test-contracts |
expectExplicitMusicGenerationCapabilities | Confirm music providers advertise explicit generation and edit capabilities. Pull from plugin-sdk/provider-test-contracts |
mockSuccessfulDashscopeVideoTask | Set up a working DashScope-compatible video task response. Pull from plugin-sdk/provider-test-contracts |
getProviderHttpMocks | Reach opt-in provider HTTP and auth Vitest mocks. Pull from plugin-sdk/provider-http-test-mocks |
installProviderHttpMockCleanup | Clear provider HTTP and auth mocks after every test. Pull from plugin-sdk/provider-http-test-mocks |
createOpenClawTestState / withOpenClawTestState / OpenClawTestState | Set up and tear down isolated OpenClaw state, config, workspace, environment, and auth-profile fixtures. Pull from plugin-sdk/test-state |
installCommonResolveTargetErrorCases | Reusable test cases for target resolution error handling. Pull from plugin-sdk/channel-target-testing |
shouldAckReaction | Determine if a channel should add an ack reaction. Pull from plugin-sdk/channel-feedback |
removeAckReactionAfterReply | Take away the ack reaction once the reply is delivered. Pull from plugin-sdk/channel-feedback |
createTestRegistry | Construct a channel plugin registry fixture. Pull from plugin-sdk/plugin-test-runtime or plugin-sdk/channel-test-helpers |
createEmptyPluginRegistry | Construct an empty plugin registry fixture. Pull from plugin-sdk/plugin-test-runtime or plugin-sdk/channel-test-helpers |
setActivePluginRegistry | Set up a registry fixture for plugin runtime tests. Pull from plugin-sdk/plugin-test-runtime or plugin-sdk/channel-test-helpers |
createRequestCaptureJsonFetch | Record JSON fetch requests during media helper tests. Pull from plugin-sdk/test-media-understanding |
isLiveTestEnabled | Restrict opt-in live provider tests. Pull from plugin-sdk/test-live |
collectProviderApiKeys | Locate credentials for live provider tests. Pull from plugin-sdk/test-live-auth |
parseProviderModelMap | Read music and video live-test model overrides. Pull from plugin-sdk/test-media-generation |
withServer | Run tests against a temporary local HTTP server. Pull from plugin-sdk/test-env |
createMockIncomingRequest | Create a minimal incoming HTTP request object. Pull from plugin-sdk/test-env |
withFetchPreconnect | Execute fetch tests with preconnect hooks in place. Pull from plugin-sdk/test-env |
withEnv / withEnvAsync | Temporarily modify environment variables. Pull from plugin-sdk/test-env |
createTempHomeEnv / withTempHome / withTempDir | Generate isolated filesystem test fixtures. Pull from plugin-sdk/test-env |
createMockServerResponse | Generate a minimal HTTP server response mock. Pull from plugin-sdk/test-env |
createProviderUsageFetch | Construct provider usage fetch fixtures. Pull from plugin-sdk/test-env |
useFrozenTime / useRealTime | Pause and resume timers when tests depend on time. Pull them in from plugin-sdk/test-env |
createCliRuntimeCapture | Grab CLI output produced at runtime for test assertions. Source it from plugin-sdk/test-fixtures |
runDirectImportSmoke | Execute a plugin's public-surface import inside a separate Node process. Get it from plugin-sdk/test-fixtures |
importFreshModule | Bring in an ESM module using a new query token so the module cache is skipped. Obtain it from plugin-sdk/test-fixtures |
bundledPluginRoot / bundledPluginFile | Locate paths for bundled plugin source or dist fixtures. Fetch them from plugin-sdk/test-fixtures |
mockNodeBuiltinModule | Set up minimal Node builtin mocks for Vitest. Take them from plugin-sdk/test-node-mocks |
createSandboxTestContext | Create sandboxed test contexts. Derive them from plugin-sdk/test-fixtures |
writeSkill | Generate skill fixture data. Pull from plugin-sdk/test-fixtures |
makeAgentAssistantMessage | Assemble agent transcript message fixtures. Get them from plugin-sdk/test-fixtures |
peekSystemEvents / resetSystemEventsForTest | Review and clear system event fixtures. Source from plugin-sdk/test-fixtures |
sanitizeTerminalText | Clean terminal output so assertions can compare it. Import from plugin-sdk/test-fixtures |
countLines / hasBalancedFences | Verify the structure of chunking results. Import from plugin-sdk/test-fixtures |
typedCases | Keep literal types intact for table-driven tests. Import from plugin-sdk/test-fixtures |
Contract suites for bundled plugins also rely on these SDK testing subpaths
for test-only registry, manifest, public-artifact, and runtime fixture
helpers. Suites that are core-only and depend on bundled OpenClaw inventory
remain under src/plugins/contracts.
Types
Testing subpaths that are more focused also re-export types that come in handy within test files:
import type {
ChannelAccountSnapshot,
ChannelGatewayContext,
} from "openclaw/plugin-sdk/channel-contract";
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
import type { MockFn, PluginRuntime, RuntimeEnv } from "openclaw/plugin-sdk/plugin-test-runtime";
Testing target resolution
Add standard error cases for channel target resolution with installCommonResolveTargetErrorCases:
import { describe } from "vitest";
import { installCommonResolveTargetErrorCases } from "openclaw/plugin-sdk/channel-target-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
Testing registration contracts
When unit tests hand a manually written api mock to register(api),
they skip OpenClaw's loader acceptance checks. For every registration surface
your plugin relies on, add at least one smoke test backed by the loader,
particularly for hooks and exclusive capabilities like memory.
If required metadata is absent, or a plugin invokes a capability API it does
not own, the real loader rejects registration. As an example,
api.registerHook(...) needs a hook name, while
api.registerMemoryCapability(...) expects the plugin manifest or exported
entry to declare kind: "memory".
Testing runtime config access
Go with the shared plugin runtime mock available from
openclaw/plugin-sdk/plugin-test-runtime. Its runtime config helpers reflect the
current snapshot and mutation APIs.
Unit testing a channel plugin
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
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
For code that relies on createPluginRuntimeStore, mock the runtime during tests:
import { createPluginRuntimeStore } from "openclaw/plugin-sdk/runtime-store";
import type { PluginRuntime } from "openclaw/plugin-sdk/runtime-store";
const store = createPluginRuntimeStore<PluginRuntime>({
pluginId: "test-plugin",
errorMessage: "test runtime not set",
});
// In test setup
const mockRuntime = {
agent: {
resolveAgentDir: vi.fn().mockReturnValue("/tmp/agent"),
// ... other mocks
},
config: {
current: vi.fn(() => ({}) as const),
mutateConfigFile: vi.fn(),
replaceConfigFile: vi.fn(),
},
// ... other namespaces
} as unknown as PluginRuntime;
store.setRuntime(mockRuntime);
// After tests
store.clearRuntime();
Testing with per-instance stubs
Use per-instance stubs rather than altering the prototype:
// Preferred: per-instance stub
const client = new MyChannelClient();
client.sendMessage = vi.fn().mockResolvedValue({ id: "msg-1" });
// Avoid: prototype mutation
// MyChannelClient.prototype.sendMessage = vi.fn();
Contract tests (in-repo plugins)
Contract tests for bundled plugins confirm registration ownership:
pnpm test src/plugins/contracts/
These tests verify:
- Which plugins register which providers
- Which plugins register which speech providers
- Registration shape correctness
- Runtime contract compliance
Running scoped tests
For a specific plugin:
pnpm test <bundled-plugin-root>/my-channel/
For contract tests only:
pnpm test src/plugins/contracts/shape.contract.test.ts
pnpm test src/plugins/contracts/auth-choice.contract.test.ts
pnpm test src/plugins/contracts/runtime-seams.contract.test.ts
Lint enforcement (in-repo plugins)
scripts/run-additional-boundary-checks.mts executes a collection of lint:plugins:*
import-boundary validations during CI, though each one can also be invoked independently on a local machine:
| Command | Enforces |
|---|---|
pnpm run lint:plugins:no-monolithic-plugin-sdk-entry-imports | Plugins that ship bundled must not pull in the monolithic openclaw/plugin-sdk root barrel. |
pnpm run lint:plugins:no-extension-src-imports | Files for production extensions are barred from importing the repo src/** tree directly (../../src/...). |
pnpm run lint:plugins:no-extension-test-core-imports | Test files for extensions cannot rely on removed SDK test aliases or other helpers exclusive to the core. |
These lint rules do not apply to external plugins, yet adopting the same conventions is advised.
Test configuration
Vitest 4, paired with informational V8 coverage reporting, is the testing setup OpenClaw relies on. For tests written against plugins:
# 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
Should memory pressure appear during local execution:
OPENCLAW_VITEST_MAX_WORKERS=1 pnpm test
Related
- SDK Overview -- import conventions
- SDK Channel Plugins -- channel plugin interface
- SDK Provider Plugins -- provider plugin hooks
- Building Plugins -- getting started guide