Files
openclaw/docs/plugins/sdk-testing.md
Vincent Koc 28838802d4 docs(plugins): add SDK reference and how-to guide pages (#52366)
* docs(plugins): add SDK reference and how-to guide pages

Create 7 new plugin SDK documentation pages:
- sdk-overview: import map, registration API reference
- sdk-entrypoints: definePluginEntry/defineChannelPluginEntry reference
- sdk-runtime: api.runtime namespace reference
- sdk-setup: packaging, manifests, config schemas reference
- sdk-channel-plugins: step-by-step channel plugin how-to
- sdk-provider-plugins: step-by-step provider plugin how-to
- sdk-testing: test utilities and patterns reference

Restructure plugin docs navigation with nested groups:
- Top-level: user-facing pages (Install, Community, Bundles)
- Building Plugins: Getting Started, Channel, Provider
- SDK Reference: Overview, Entry Points, Runtime, Setup, Testing, Migration, Manifest, Internals

Revise existing pages for new IA:
- building-plugins.md: tightened as quick-start, routes to detailed guides
- architecture.md: updated info box with links to new guides
- sdk-migration.md: expanded Related section

* docs(plugins): add Mintlify components (Steps, CodeGroup, Tabs, Accordion, CardGroup)

- Channel plugin guide: wrap walkthrough in Steps, use CodeGroup for
  package.json/manifest, Accordion for createChatChannelPlugin details,
  CardGroup for advanced topics
- Provider plugin guide: wrap walkthrough in Steps, use CodeGroup for
  package files, Tabs for hook examples, Accordion for all-hooks reference
- Getting started: use CardGroup for plugin-type picker and next steps,
  CodeGroup for package/manifest
- SDK Overview: wrap subpath tables in AccordionGroup for scannability

* fix(docs): address PR review feedback on plugin SDK pages

- Remove nonexistent api.runtime.channel.handleInboundMessage call,
  replace with realistic webhook pattern and note about channel-specific
  inbound handling (issue a)
- Fix registrationMode values: 'setup' -> 'setup-only' and 'setup-runtime'
  matching actual PluginRegistrationMode type (issue b)
- Fix createOptionalChannelSetupSurface params: channelId -> channel,
  add required label field (issue c)
- Fix broken anchor links: #multi-capability-providers ->
  #step-5-add-extra-capabilities, #plugin-kinds -> #registration-api (issue d)
- Add missing acmeChatApi import in channel plugin example (issue e)
- Fix undefined provider variable in provider test example (issue f)

* fix(docs): use correct createProviderApiKeyAuthMethod options

Replace incorrect params (provider, validate) with actual required fields
(providerId, methodId, optionKey, flagName, promptMessage) matching
src/plugins/provider-api-key-auth.ts.

* fix(docs): address second round of PR review feedback

- Add required model fields (reasoning, input, cost, contextWindow,
  maxTokens) to catalog example (issue b)
- Fix buildChannelConfigSchema to take a Zod schema argument (issue c)
- Replace fabricated setupWizard steps/run with real ChannelSetupWizard
  contract (channel, status, credentials) (issue d)
- Add required sessionFile/workspaceDir to runEmbeddedPiAgent (issue e)
- Fix wrapStreamFn to return StreamFn from ctx.streamFn (issue f)
2026-03-22 11:35:53 -07:00

6.8 KiB

title, sidebarTitle, summary, read_when
title sidebarTitle summary read_when
SDK Testing Testing Testing utilities and patterns for OpenClaw plugins
You are writing tests for a plugin
You need test utilities from the plugin SDK
You want to understand contract tests for bundled plugins

Plugin Testing

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

Import: openclaw/plugin-sdk/testing

The testing subpath exports a narrow set of helpers for plugin authors:

import {
  installCommonResolveTargetErrorCases,
  shouldAckReaction,
  removeAckReactionAfterReply,
} from "openclaw/plugin-sdk/testing";

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

Types

The testing subpath also re-exports types useful in test files:

import type {
  ChannelAccountSnapshot,
  ChannelGatewayContext,
  OpenClawConfig,
  PluginRuntime,
  RuntimeEnv,
  MockFn,
} from "openclaw/plugin-sdk/testing";

Testing target resolution

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

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

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 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>("test runtime not set");

// In test setup
const 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 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 -- extensions/my-channel/

For contract tests only:

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 enforcement (in-repo plugins)

Three rules are enforced by pnpm check for in-repo plugins:

  1. No monolithic root imports -- openclaw/plugin-sdk root barrel is rejected
  2. No direct src/ imports -- plugins cannot import ../../src/ directly
  3. No self-imports -- plugins cannot import their own plugin-sdk/<name> subpath

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

Test configuration

OpenClaw uses Vitest with V8 coverage thresholds. For plugin tests:

# Run all tests
pnpm test

# Run specific plugin tests
pnpm test -- extensions/my-channel/src/channel.test.ts

# Run with a specific test name filter
pnpm test -- extensions/my-channel/ -t "resolves account"

# Run with coverage
pnpm test:coverage

If local runs cause memory pressure:

OPENCLAW_TEST_PROFILE=low OPENCLAW_TEST_SERIAL_GATEWAY=1 pnpm test