openclaw/docs/plugins/sdk-testing.md
Peter Steinberger a6089edc5c
fix(test): retire MCP managers between shared test files (#159800)
* fix(test): retire MCP managers between shared test files

Drain the current manager before resetting file-scoped modules, preserve replacement and uncertain custody, and carry earlier disposal failures into later global cleanup scopes. Keep the existing best-effort disposal contract.

* fix(test): settle MCP clients before closing HTTP fixtures

Keep intentional failed retirement on a fixture-owned real manager, drain shared clients before server closure, and join fixture shutdown. Preserve all failure assertions and custody guards. Extract the three HTTP lifecycle cases without changing their registration order. Final runtime owner suite passes all 114 cases with clean teardown.

* fix(test): resolve deferred helper under raw Node

Import the canonical dependency-free deferred implementation by its real
TypeScript path so standalone shutdown fixtures do not rely on Vitest aliases.

The exact preimage reproduces module-not-found; all 21 raw shutdown cases, both
shared-runner cases, and affected static checks pass on macOS Node 26.10.

* test(ui): wait for history before asserting effort controls

Model browsing can become ready while chat history is still hydrating. Bind the existing partial-refresh fixture to the successful history response and current rendered session before checking effort controls, preserving the immediate enabled-state assertion.
2026-10-01 16:34:10 -07:00

28 KiB

summary title sidebarTitle read_when
Testing utilities and patterns for OpenClaw plugins Plugin testing Testing
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 for test utilities, patterns, and lint enforcement for OpenClaw plugins.

**Looking for test examples?** The how-to guides include worked test examples: [Channel plugin tests](/plugins/sdk-channel-plugins#step-6-test) and [Provider plugin tests](/plugins/sdk-provider-plugins#step-6-test).

Test utilities

These subpaths are repo-local source entrypoints for OpenClaw's own bundled plugin tests. They are not published package.json exports for third-party plugins, and they may import 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";

Use these focused subpaths for bundled plugin tests. The former openclaw/plugin-sdk/testing barrel was repo-local, excluded from shipped packages, and has been removed. The former openclaw/plugin-sdk/test-utils alias was removed with it. 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 above.

Bundled channel integration tests can use agent-runtime-test-contracts for real session and subscriber fixtures, reply-payload-testing for payload construction and delivery settlement, and plugin-test-runtime for hook runners and registries. These helpers reuse their core owners; register the session fixture lifecycle explicitly. Use published runtime subpaths when they already expose the needed operation.

After closing retained plugin runtime handles, call resetPluginRuntimeStateForTest() and await waitForPluginCacheRetirement(true) from plugin-test-runtime to include borrowed cache generations. Assert that its failures array is empty before deleting fixture files or restoring the environment. A rejected or failed retirement must leave the fixture intact.

Await listChannelIngressQueueAccountIdsForTests from channel-ingress-test-runtime or plugin-state-test-runtime. It uses the shared read-only worker and leaves missing state uncreated. Join asynchronous database cleanup before removing a fixture's state directory.

For direct worker fixtures, pair resolveRuntimeWorkerUrl from process-runtime with resolveRuntimeWorkerThreadExecArgv from test-env. This keeps source and built workers on the runtime owner's startup arguments.

Available exports

Export Purpose
createTestPluginApi Build a minimal plugin API mock for direct registration unit tests. Import from plugin-sdk/plugin-test-api
AUTH_PROFILE_RUNTIME_CONTRACT Shared auth-profile contract fixture for native agent runtime adapters. Import from plugin-sdk/agent-runtime-test-contracts
DELIVERY_NO_REPLY_RUNTIME_CONTRACT Shared delivery suppression contract fixture for native agent runtime adapters. Import from plugin-sdk/agent-runtime-test-contracts
OUTCOME_FALLBACK_RUNTIME_CONTRACT Shared fallback-classification contract fixture for native agent runtime adapters. Import from plugin-sdk/agent-runtime-test-contracts
createParameterFreeTool Build dynamic-tool schema fixtures for native runtime contract tests. Import from plugin-sdk/agent-runtime-test-contracts
expectChannelInboundContextContract Assert channel inbound context shape. Import from plugin-sdk/channel-contract-testing
installChannelOutboundPayloadContractSuite Install channel outbound payload contract cases. Import from plugin-sdk/channel-contract-testing
createStartAccountContext Build channel account lifecycle contexts. Import from plugin-sdk/channel-test-helpers
installChannelActionsContractSuite Install generic channel message-action contract cases. Import from plugin-sdk/channel-test-helpers
installChannelSetupContractSuite Install generic channel setup contract cases. Import from plugin-sdk/channel-test-helpers
installChannelStatusContractSuite Install generic channel status contract cases. Import from plugin-sdk/channel-test-helpers
expectDirectoryIds Assert channel directory ids from a directory-list function. Import from plugin-sdk/channel-test-helpers
formatEnvelopeTimestamp Format deterministic envelope timestamps. Import from plugin-sdk/channel-test-helpers
expectPairingReplyText Assert channel pairing reply text and extract its code. Import from plugin-sdk/channel-test-helpers
describePluginRegistrationContract Install plugin registration contract checks. Import from plugin-sdk/plugin-test-contracts
registerSingleProviderPlugin Register one provider plugin in loader smoke tests. Import from plugin-sdk/plugin-test-runtime
registerProviderPlugin Capture all provider kinds from one plugin. Import from plugin-sdk/plugin-test-runtime
registerProviderPlugins Capture provider registrations across multiple plugins. Import from plugin-sdk/plugin-test-runtime
requireRegisteredProvider Assert that a provider collection contains an id. Import from plugin-sdk/plugin-test-runtime
createRuntimeEnv Build a mocked CLI/plugin runtime environment. Import from plugin-sdk/plugin-test-runtime
createPluginRuntimeMock Build a mocked plugin runtime surface. Import from plugin-sdk/plugin-test-runtime
createPluginSetupWizardStatus Build setup status helpers for channel plugins. Import from plugin-sdk/plugin-test-runtime
createTestWizardPrompter Build a mocked setup wizard prompter. Import from plugin-sdk/plugin-test-runtime
runProviderCatalog Execute a provider catalog hook with test dependencies. Import from plugin-sdk/plugin-test-runtime
resolveProviderModelPickerEntries Resolve provider model-picker entries in contract tests. Import from plugin-sdk/plugin-test-runtime
buildProviderPluginMethodChoice Build provider wizard choice ids for assertions. Import from plugin-sdk/plugin-test-runtime
setProviderWizardProvidersResolverForTest Inject provider wizard providers for isolated tests. Import from plugin-sdk/plugin-test-runtime
describeOpenAIProviderRuntimeContract Install provider-family runtime contract checks. Import from plugin-sdk/provider-test-contracts
expectPassthroughReplayPolicy Assert provider replay policies pass through provider-owned tools and metadata. Import from plugin-sdk/provider-test-contracts
runRealtimeSttLiveTest Run a live realtime STT provider test with shared audio fixtures. Import from plugin-sdk/provider-test-contracts
normalizeTranscriptForMatch Normalize live transcript output before fuzzy assertions. Import from plugin-sdk/provider-test-contracts
expectExplicitVideoGenerationCapabilities Assert video providers declare explicit generation mode capabilities. Import from plugin-sdk/provider-test-contracts
expectExplicitMusicGenerationCapabilities Assert music providers declare explicit generation/edit capabilities. Import from plugin-sdk/provider-test-contracts
mockSuccessfulDashscopeVideoTask Install a successful DashScope-compatible video task response. Import from plugin-sdk/provider-test-contracts
getProviderHttpMocks Access opt-in provider HTTP/auth Vitest mocks. Import from plugin-sdk/provider-http-test-mocks
installProviderHttpMockCleanup Reset provider HTTP/auth mocks after each test. Import from plugin-sdk/provider-http-test-mocks
createOpenClawTestState / withOpenClawTestState / OpenClawTestState Create and clean up isolated OpenClaw state, config, workspace, environment, and auth-profile fixtures. Import from plugin-sdk/test-state
installCommonResolveTargetErrorCases Shared test cases for target resolution error handling. Import from plugin-sdk/channel-target-testing
shouldAckReaction Check whether a channel should add an ack reaction. Import from plugin-sdk/channel-feedback
removeAckReactionAfterReply Remove ack reaction after reply delivery. Import from plugin-sdk/channel-feedback
createTestRegistry Build a channel plugin registry fixture. Import from plugin-sdk/plugin-test-runtime or plugin-sdk/channel-test-helpers
createEmptyPluginRegistry Build an empty plugin registry fixture. Import from plugin-sdk/plugin-test-runtime or plugin-sdk/channel-test-helpers
createPluginMetadataSnapshotFixture Build a complete metadata snapshot with aligned manifest and installed-plugin views. Import from plugin-sdk/plugin-test-runtime
setActivePluginRegistry Install a registry fixture for plugin runtime tests. Import from plugin-sdk/plugin-test-runtime or plugin-sdk/channel-test-helpers
createRequestCaptureJsonFetch Capture JSON fetch requests in media helper tests. Import from plugin-sdk/test-media-understanding
isLiveTestEnabled Gate opt-in live provider tests. Import from plugin-sdk/test-live
collectProviderApiKeys Discover credentials for live provider tests. Import from plugin-sdk/test-live-auth
parseProviderModelMap Parse music/video live-test model overrides. Import from plugin-sdk/test-media-generation
withServer Run tests against a disposable local HTTP server. Import from plugin-sdk/test-env
createMockIncomingRequest Build a minimal incoming HTTP request object. Import from plugin-sdk/test-env
withFetchPreconnect Run fetch tests with preconnect hooks installed. Import from plugin-sdk/test-env
withEnv / withEnvAsync Temporarily patch environment variables. Import from plugin-sdk/test-env
createTempHomeEnv / withTempHome / withTempDir Create isolated filesystem test fixtures. Import from plugin-sdk/test-env
createStagedInputOwnershipFixture Create owner-staged attachment files and unowned lookalikes. Import from plugin-sdk/test-env
createMockServerResponse Create a minimal HTTP server response mock. Import from plugin-sdk/test-env
createProviderUsageFetch Build provider usage fetch fixtures. Import from plugin-sdk/test-env
useFrozenTime / useRealTime Freeze and restore timers for time-sensitive tests. Import from plugin-sdk/test-env
createCliRuntimeCapture Capture CLI runtime output in tests. Import from plugin-sdk/test-fixtures
findSourceImportBackedges Asynchronously inspect repository-source static import closures for forbidden dependencies. Import from plugin-sdk/test-fixtures
runDirectImportSmoke Run a plugin public-surface import in an isolated Node process. Import from plugin-sdk/test-fixtures
importFreshModule Import an ESM module with a fresh query token to bypass module cache. Import from plugin-sdk/test-fixtures
bundledPluginRoot / bundledPluginFile Resolve bundled plugin source or dist fixture paths. Import from plugin-sdk/test-fixtures
mockNodeBuiltinModule Install narrow Node builtin Vitest mocks. Import from plugin-sdk/test-node-mocks
createSandboxTestContext Build sandbox test contexts. Import from plugin-sdk/test-fixtures
writeSkill Write skill fixtures. Import from plugin-sdk/test-fixtures
makeAgentAssistantMessage Build agent transcript message fixtures. Import from plugin-sdk/test-fixtures
peekSystemEvents / resetSystemEventsForTest Inspect and reset system event fixtures. Import from plugin-sdk/test-fixtures
sanitizeTerminalText Sanitize terminal output for assertions. Import from plugin-sdk/test-fixtures
countLines / hasBalancedFences Assert chunking output shape. Import from plugin-sdk/test-fixtures
typedCases Preserve literal types for table-driven tests. Import from plugin-sdk/test-fixtures

Bundled-plugin contract suites also use these SDK testing subpaths for test-only registry, manifest, public-artifact, and runtime fixture helpers. Core-only suites that depend on bundled OpenClaw inventory stay under src/plugins/contracts instead.

For channel account-policy tests, createAccountPolicyInheritanceCases() from openclaw/plugin-sdk/channel-test-helpers returns four literal inheritance rows with fresh objects and arrays on each call, preserving omitted policy fields. Use it alongside validateTestChannelConfig(channelId, channelConfig), which validates schema-parsed channel data through the host config boundary. Each plugin test still owns its schema parsing, account resolver, and assertions, including checks that omitted account policies remain absent.

For complete zero-usage inputs, createZeroUsageFixture() from openclaw/plugin-sdk/test-fixtures returns fresh usage and nested cost objects without optional telemetry fields. Keep expected usage values explicit.

Types

Focused testing subpaths also re-export types useful in 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

Use installCommonResolveTargetErrorCases to add standard error cases for channel target resolution:

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

Unit tests that pass a hand-written api mock to register(api) do not exercise OpenClaw's loader acceptance gates. Add at least one loader-backed smoke test for each registration surface your plugin depends on, especially hooks and exclusive capabilities such as memory.

The real loader fails plugin registration when required metadata is missing or a plugin calls a capability API it does not own. For example, api.registerHook(...) requires a hook name, and api.registerMemoryCapability(...) requires the plugin manifest or exported entry to declare kind: "memory".

Testing runtime config access

Prefer the shared plugin runtime mock from openclaw/plugin-sdk/plugin-test-runtime. Its runtime config helpers model 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

For bundled catalog tests that resolve provider endpoint capabilities, call useProviderCatalogMetadata(new URL(".", import.meta.url)) from openclaw/plugin-sdk/plugin-test-runtime at file or suite scope. It prepares the plugin's manifest metadata once, installs and clears that snapshot around each test, and rejects Jiti loading during assertions. This keeps cold runtime discovery out of catalog test deadlines without changing provider behavior.

Pass additional manifest roots when a case exercises another provider's endpoints, for example useProviderCatalogMetadata(new URL(".", import.meta.url), new URL("../google/", import.meta.url)). Assert the endpoint class in route-specific cases so missing metadata cannot turn a provider route into an unintended custom-endpoint case.

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 uses createPluginRuntimeStore, mock the runtime in 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

Prefer per-instance stubs over prototype mutation:

// 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)

Bundled plugins have contract tests that verify registration ownership:

pnpm test src/plugins/contracts/

These tests assert:

  • 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 runs a set of lint:plugins:* import-boundary checks in CI; each can also be run standalone locally:

Command Enforces
pnpm run lint:plugins:no-monolithic-plugin-sdk-entry-imports Bundled plugins cannot import the monolithic openclaw/plugin-sdk root barrel.
pnpm run lint:plugins:no-extension-src-imports Production extension files cannot import the repo src/** tree directly (../../src/...).
pnpm run lint:plugins:no-extension-test-core-imports Extension test files cannot import removed SDK test aliases or other core-only test helpers.

External plugins are not subject to these lint rules, but following the same patterns is recommended.

Test configuration

OpenClaw uses Vitest 5 with informational V8 coverage reporting. For plugin tests:

# 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

If local runs cause memory pressure:

OPENCLAW_VITEST_MAX_WORKERS=1 pnpm test