diff --git a/CHANGELOG.md b/CHANGELOG.md index c71e22c72b0..80757c8fe71 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ Docs: https://docs.openclaw.ai - Channels/QQBot: add full group chat support (history tracking, @-mention gating, activation modes, per-group config, FIFO message queue with deliver debounce), C2C `stream_messages` streaming with a `StreamingController` lifecycle manager, unified `sendMedia` with chunked upload for large files, and refactor the engine into pipeline stages, focused outbound submodules, builtin slash-command modules, and explicit DI ports via `createEngineAdapters()`. (#70624) Thanks @cxyhhhhh. - Gateway/runtime: reuse the current plugin metadata snapshot for provider discovery so repeated model-provider discovery avoids rebuilding plugin manifest metadata. Thanks @shakkernerd. - Gateway/startup: pass the plugin metadata snapshot from config validation into plugin bootstrap so startup reuses one manifest product instead of rebuilding plugin metadata. Thanks @shakkernerd. +- Plugin SDK/testing: add a focused `plugin-sdk/plugin-test-api` helper subpath and move bundled plugin registration tests off the repo-only plugin API bridge. Thanks @vincentkoc. - Plugin SDK/testing: expose provider catalog, wizard, registry, manifest, public-artifact, outbound, and TTS contract helpers through documented SDK testing seams so bundled plugin tests no longer import repo `src/**` internals. Thanks @vincentkoc. - Matrix: attach versioned structured approval metadata to pending approval messages so capable Matrix clients can render richer approval UI while body text and reaction fallback keep working. (#72432) Thanks @kakahu2015. diff --git a/docs/.generated/plugin-sdk-api-baseline.sha256 b/docs/.generated/plugin-sdk-api-baseline.sha256 index e7ce9fedc45..2c3002c6cb3 100644 --- a/docs/.generated/plugin-sdk-api-baseline.sha256 +++ b/docs/.generated/plugin-sdk-api-baseline.sha256 @@ -1,2 +1,2 @@ -7a2039df8cfdcfea0fbc82bbe425c96631ab27b2210c932c6e3f17d380aff350 plugin-sdk-api-baseline.json -ea9f134bc2a1aa17e8abf8f1335daaa927b2272c972523b9d0bd05bf4ac8e149 plugin-sdk-api-baseline.jsonl +0736a1666860383e3e5f8ada181c016455d8304a2852ac6966355765f799add4 plugin-sdk-api-baseline.json +761cdb609547f5912513e5714d8b0ec8fff2b29905690af376cc5bdd74f2c279 plugin-sdk-api-baseline.jsonl diff --git a/docs/plugins/sdk-subpaths.md b/docs/plugins/sdk-subpaths.md index c8edeb62877..714beea859e 100644 --- a/docs/plugins/sdk-subpaths.md +++ b/docs/plugins/sdk-subpaths.md @@ -23,6 +23,7 @@ For the plugin authoring guide, see [Plugin SDK overview](/plugins/sdk-overview) | `plugin-sdk/config-schema` | `OpenClawSchema` | | `plugin-sdk/provider-entry` | `defineSingleProviderPluginEntry` | | `plugin-sdk/testing` | Public plugin test fixtures, provider registration/catalog helpers, wizard contract hooks, and bundled-plugin contract maintenance helpers | +| `plugin-sdk/plugin-test-api` | Minimal `OpenClawPluginApi` mock builder for direct plugin registration unit tests | | `plugin-sdk/migration` | Migration provider item helpers such as `createMigrationItem`, reason constants, item status markers, redaction helpers, and `summarizeMigrationItems` | | `plugin-sdk/migration-runtime` | Runtime migration helpers such as `copyMigrationFileItem` and `writeMigrationReport` | @@ -260,6 +261,7 @@ For the plugin authoring guide, see [Plugin SDK overview](/plugins/sdk-overview) | `plugin-sdk/web-media` | Shared remote/local media loading helpers | | `plugin-sdk/zod` | Re-exported `zod` for plugin SDK consumers | | `plugin-sdk/testing` | Public extension test helpers including plugin registry/runtime mocks, provider registration capture, setup-wizard helpers, fetch/env/temp/time fixtures, schema/media/live-test helpers, `installCommonResolveTargetErrorCases`, `writeSkill`, `createTestRegistry`, and live generation env loading. Extension `*.test-support.ts` helpers stay on this or focused SDK subpaths, not core internals | + | `plugin-sdk/plugin-test-api` | Minimal `createTestPluginApi` helper for direct plugin registration unit tests without importing repo test helper bridges | diff --git a/docs/plugins/sdk-testing.md b/docs/plugins/sdk-testing.md index e79c958cc81..95b410a90f0 100644 --- a/docs/plugins/sdk-testing.md +++ b/docs/plugins/sdk-testing.md @@ -19,7 +19,11 @@ plugins. ## Test utilities -**Import:** `openclaw/plugin-sdk/testing` +**General import:** `openclaw/plugin-sdk/testing` + +**Plugin API mock import:** `openclaw/plugin-sdk/plugin-test-api` + +**Channel contract import:** `openclaw/plugin-sdk/channel-contract-testing` The testing subpath exports a narrow set of helpers for plugin authors: @@ -29,44 +33,51 @@ import { shouldAckReaction, removeAckReactionAfterReply, } from "openclaw/plugin-sdk/testing"; +import { createTestPluginApi } from "openclaw/plugin-sdk/plugin-test-api"; +import { expectChannelInboundContextContract } from "openclaw/plugin-sdk/channel-contract-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 | -| `createTestRegistry` | Build a channel plugin registry fixture | -| `createEmptyPluginRegistry` | Build an empty plugin registry fixture | -| `setActivePluginRegistry` | Install a registry fixture for plugin runtime tests | -| `createRequestCaptureJsonFetch` | Capture JSON fetch requests in media helper tests | -| `withFetchPreconnect` | Run fetch tests with preconnect hooks installed | -| `withEnv` / `withEnvAsync` | Temporarily patch environment variables | -| `createTempHomeEnv` / `withTempDir` | Create isolated filesystem test fixtures | -| `createMockServerResponse` | Create a minimal HTTP server response mock | -| `registerSingleProviderPlugin` | Register one provider plugin in loader smoke tests | -| `registerProviderPlugin` | Capture all provider kinds from one plugin | -| `registerProviderPlugins` | Capture provider registrations across multiple plugins | -| `requireRegisteredProvider` | Assert that a provider collection contains an id | -| `runProviderCatalog` | Execute a provider catalog hook with test dependencies | -| `resolveProviderWizardOptions` | Resolve provider setup wizard choices in contract tests | -| `resolveProviderModelPickerEntries` | Resolve provider model-picker entries in contract tests | -| `buildProviderPluginMethodChoice` | Build provider wizard choice ids for assertions | -| `setProviderWizardProvidersResolverForTest` | Inject provider wizard providers for isolated tests | -| `createProviderUsageFetch` | Build provider usage fetch fixtures | -| `useFrozenTime` / `useRealTime` | Freeze and restore timers for time-sensitive tests | -| `createRuntimeEnv` | Build a mocked CLI/plugin runtime environment | -| `createTestWizardPrompter` | Build a mocked setup wizard prompter | -| `createPluginSetupWizardStatus` | Build setup status helpers for channel plugins | -| `createRuntimeTaskFlow` | Create isolated runtime task-flow state | -| `typedCases` | Preserve literal types for table-driven tests | +| Export | Purpose | +| -------------------------------------------- | ------------------------------------------------------------------------------------------------------------ | +| `createTestPluginApi` | Build a minimal plugin API mock for direct registration unit tests. Import from `plugin-sdk/plugin-test-api` | +| `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` | +| `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 | +| `createTestRegistry` | Build a channel plugin registry fixture | +| `createEmptyPluginRegistry` | Build an empty plugin registry fixture | +| `setActivePluginRegistry` | Install a registry fixture for plugin runtime tests | +| `createRequestCaptureJsonFetch` | Capture JSON fetch requests in media helper tests | +| `withFetchPreconnect` | Run fetch tests with preconnect hooks installed | +| `withEnv` / `withEnvAsync` | Temporarily patch environment variables | +| `createTempHomeEnv` / `withTempDir` | Create isolated filesystem test fixtures | +| `createMockServerResponse` | Create a minimal HTTP server response mock | +| `registerSingleProviderPlugin` | Register one provider plugin in loader smoke tests | +| `registerProviderPlugin` | Capture all provider kinds from one plugin | +| `registerProviderPlugins` | Capture provider registrations across multiple plugins | +| `requireRegisteredProvider` | Assert that a provider collection contains an id | +| `runProviderCatalog` | Execute a provider catalog hook with test dependencies | +| `resolveProviderWizardOptions` | Resolve provider setup wizard choices in contract tests | +| `resolveProviderModelPickerEntries` | Resolve provider model-picker entries in contract tests | +| `buildProviderPluginMethodChoice` | Build provider wizard choice ids for assertions | +| `setProviderWizardProvidersResolverForTest` | Inject provider wizard providers for isolated tests | +| `createProviderUsageFetch` | Build provider usage fetch fixtures | +| `useFrozenTime` / `useRealTime` | Freeze and restore timers for time-sensitive tests | +| `createRuntimeEnv` | Build a mocked CLI/plugin runtime environment | +| `createTestWizardPrompter` | Build a mocked setup wizard prompter | +| `createPluginSetupWizardStatus` | Build setup status helpers for channel plugins | +| `createRuntimeTaskFlow` | Create isolated runtime task-flow state | +| `typedCases` | Preserve literal types for table-driven tests | -Bundled-plugin contract suites also use this subpath for test-only registry, -manifest, public-artifact, and runtime fixture helpers. Keep new extension tests -on `openclaw/plugin-sdk/testing` or a narrower documented SDK subpath rather -than importing repo `src/**` files directly. +Bundled-plugin contract suites also use SDK testing subpaths for test-only +registry, manifest, public-artifact, and runtime fixture helpers. Keep new +extension tests on `openclaw/plugin-sdk/testing` or a narrower documented SDK +subpath such as `plugin-sdk/plugin-test-api` or +`plugin-sdk/channel-contract-testing` rather than importing repo `src/**` files +directly. ### Types diff --git a/extensions/acpx/index.test.ts b/extensions/acpx/index.test.ts index a5a6871e6ac..4e41a81ea19 100644 --- a/extensions/acpx/index.test.ts +++ b/extensions/acpx/index.test.ts @@ -1,6 +1,6 @@ import type { OpenClawPluginApi } from "openclaw/plugin-sdk/plugin-entry"; +import { createTestPluginApi } from "openclaw/plugin-sdk/plugin-test-api"; import { beforeEach, describe, expect, it, vi } from "vitest"; -import { createTestPluginApi } from "../../test/helpers/plugins/plugin-api.js"; import setupPlugin from "./setup-api.js"; const { createAcpxRuntimeServiceMock, tryDispatchAcpReplyHookMock } = vi.hoisted(() => ({ diff --git a/extensions/browser/index.test.ts b/extensions/browser/index.test.ts index 6f85c19e9f0..2844bca10b5 100644 --- a/extensions/browser/index.test.ts +++ b/extensions/browser/index.test.ts @@ -1,7 +1,7 @@ import fs from "node:fs"; import path from "node:path"; +import { createTestPluginApi } from "openclaw/plugin-sdk/plugin-test-api"; import { describe, expect, it, vi } from "vitest"; -import { createTestPluginApi } from "../../test/helpers/plugins/plugin-api.js"; import { browserPluginNodeHostCommands, browserPluginReload, diff --git a/extensions/codex/index.test.ts b/extensions/codex/index.test.ts index 3fde60cc904..f6faca4ac5b 100644 --- a/extensions/codex/index.test.ts +++ b/extensions/codex/index.test.ts @@ -1,6 +1,6 @@ import fs from "node:fs"; +import { createTestPluginApi } from "openclaw/plugin-sdk/plugin-test-api"; import { describe, expect, it, vi } from "vitest"; -import { createTestPluginApi } from "../../test/helpers/plugins/plugin-api.js"; import { createCodexAppServerAgentHarness } from "./harness.js"; import plugin from "./index.js"; diff --git a/extensions/comfy/comfy.live.test.ts b/extensions/comfy/comfy.live.test.ts index f7b4b7b3a99..bc6e7e0e3fc 100644 --- a/extensions/comfy/comfy.live.test.ts +++ b/extensions/comfy/comfy.live.test.ts @@ -1,9 +1,9 @@ import { resolveOpenClawAgentDir } from "openclaw/plugin-sdk/agent-runtime"; import type { OpenClawConfig } from "openclaw/plugin-sdk/config-types"; +import { createTestPluginApi } from "openclaw/plugin-sdk/plugin-test-api"; import { getRuntimeConfig } from "openclaw/plugin-sdk/runtime-config-snapshot"; import { isLiveTestEnabled } from "openclaw/plugin-sdk/testing"; import { beforeAll, describe, expect, it } from "vitest"; -import { createTestPluginApi } from "../../test/helpers/plugins/plugin-api.js"; import plugin from "./index.js"; import { getComfyConfig, isComfyCapabilityConfigured } from "./workflow-runtime.js"; diff --git a/extensions/device-pair/index.test.ts b/extensions/device-pair/index.test.ts index 23fea6accaa..db3078185da 100644 --- a/extensions/device-pair/index.test.ts +++ b/extensions/device-pair/index.test.ts @@ -5,8 +5,8 @@ import type { OpenClawPluginCommandDefinition, PluginCommandContext, } from "openclaw/plugin-sdk/core"; +import { createTestPluginApi } from "openclaw/plugin-sdk/plugin-test-api"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -import { createTestPluginApi } from "../../test/helpers/plugins/plugin-api.js"; import type { OpenClawPluginApi } from "./api.js"; import type { PendingPairingRequest } from "./notify.ts"; diff --git a/extensions/device-pair/notify.test.ts b/extensions/device-pair/notify.test.ts index cd6816ae697..2265ed74622 100644 --- a/extensions/device-pair/notify.test.ts +++ b/extensions/device-pair/notify.test.ts @@ -1,8 +1,8 @@ import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; +import { createTestPluginApi } from "openclaw/plugin-sdk/plugin-test-api"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -import { createTestPluginApi } from "../../test/helpers/plugins/plugin-api.js"; const listDevicePairingMock = vi.hoisted(() => vi.fn(async () => ({ pending: [] }))); diff --git a/extensions/diffs/src/browser.test.ts b/extensions/diffs/src/browser.test.ts index 538265aae6c..5db475787d6 100644 --- a/extensions/diffs/src/browser.test.ts +++ b/extensions/diffs/src/browser.test.ts @@ -1,9 +1,9 @@ import fs from "node:fs/promises"; import type { IncomingMessage, ServerResponse } from "node:http"; import path from "node:path"; +import { createTestPluginApi } from "openclaw/plugin-sdk/plugin-test-api"; import { createMockServerResponse } from "openclaw/plugin-sdk/testing"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; -import { createTestPluginApi } from "../../../test/helpers/plugins/plugin-api.js"; import type { OpenClawConfig } from "../api.js"; import type { OpenClawPluginApi, OpenClawPluginToolContext } from "../api.js"; import { registerDiffsPlugin } from "./plugin.js"; diff --git a/extensions/diffs/src/tool-render-output.test.ts b/extensions/diffs/src/tool-render-output.test.ts index da6831a95cb..a5956a63096 100644 --- a/extensions/diffs/src/tool-render-output.test.ts +++ b/extensions/diffs/src/tool-render-output.test.ts @@ -1,7 +1,7 @@ import fs from "node:fs/promises"; import path from "node:path"; +import { createTestPluginApi } from "openclaw/plugin-sdk/plugin-test-api"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; -import { createTestPluginApi } from "../../../test/helpers/plugins/plugin-api.js"; import type { OpenClawPluginApi } from "../api.js"; import type { DiffScreenshotter } from "./browser.js"; import { DEFAULT_DIFFS_TOOL_DEFAULTS } from "./config.js"; diff --git a/extensions/diffs/src/tool.test.ts b/extensions/diffs/src/tool.test.ts index 5370d2e0309..a4ad742c0f3 100644 --- a/extensions/diffs/src/tool.test.ts +++ b/extensions/diffs/src/tool.test.ts @@ -1,7 +1,7 @@ import fs from "node:fs/promises"; import path from "node:path"; +import { createTestPluginApi } from "openclaw/plugin-sdk/plugin-test-api"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -import { createTestPluginApi } from "../../../test/helpers/plugins/plugin-api.js"; import type { OpenClawPluginApi, OpenClawPluginToolContext } from "../api.js"; import type { DiffScreenshotter } from "./browser.js"; import { DEFAULT_DIFFS_TOOL_DEFAULTS } from "./config.js"; diff --git a/extensions/discord/src/inbound-context.contract.test.ts b/extensions/discord/src/inbound-context.contract.test.ts index de31ee5b943..89f7b68e41a 100644 --- a/extensions/discord/src/inbound-context.contract.test.ts +++ b/extensions/discord/src/inbound-context.contract.test.ts @@ -1,4 +1,4 @@ -import { expectChannelInboundContextContract } from "openclaw/plugin-sdk/testing"; +import { expectChannelInboundContextContract } from "openclaw/plugin-sdk/channel-contract-testing"; import { describe, it } from "vitest"; import { buildFinalizedDiscordDirectInboundContext } from "./monitor/inbound-context.test-helpers.js"; diff --git a/extensions/discord/src/monitor/message-handler.inbound-context.test.ts b/extensions/discord/src/monitor/message-handler.inbound-context.test.ts index 71cb9ecb0f9..932fe749670 100644 --- a/extensions/discord/src/monitor/message-handler.inbound-context.test.ts +++ b/extensions/discord/src/monitor/message-handler.inbound-context.test.ts @@ -1,5 +1,5 @@ +import { expectChannelInboundContextContract as expectInboundContextContract } from "openclaw/plugin-sdk/channel-contract-testing"; import { finalizeInboundContext } from "openclaw/plugin-sdk/reply-dispatch-runtime"; -import { expectChannelInboundContextContract as expectInboundContextContract } from "openclaw/plugin-sdk/testing"; import { describe, expect, it } from "vitest"; import { buildDiscordInboundAccessContext } from "./inbound-context.js"; import { buildFinalizedDiscordDirectInboundContext } from "./inbound-context.test-helpers.js"; diff --git a/extensions/feishu/src/chat.test.ts b/extensions/feishu/src/chat.test.ts index f6b38de6250..19395801225 100644 --- a/extensions/feishu/src/chat.test.ts +++ b/extensions/feishu/src/chat.test.ts @@ -1,5 +1,5 @@ +import { createTestPluginApi } from "openclaw/plugin-sdk/plugin-test-api"; import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; -import { createTestPluginApi } from "../../../test/helpers/plugins/plugin-api.js"; import type { OpenClawPluginApi, PluginRuntime } from "../runtime-api.js"; const createFeishuClientMock = vi.hoisted(() => vi.fn()); diff --git a/extensions/feishu/src/drive.test.ts b/extensions/feishu/src/drive.test.ts index 8933fafab03..744cc8c8f6a 100644 --- a/extensions/feishu/src/drive.test.ts +++ b/extensions/feishu/src/drive.test.ts @@ -1,5 +1,5 @@ +import { createTestPluginApi } from "openclaw/plugin-sdk/plugin-test-api"; import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; -import { createTestPluginApi } from "../../../test/helpers/plugins/plugin-api.js"; import type { OpenClawPluginApi, PluginRuntime } from "../runtime-api.js"; const createFeishuToolClientMock = vi.hoisted(() => vi.fn()); diff --git a/extensions/github-copilot/index.test.ts b/extensions/github-copilot/index.test.ts index 0d3f2576216..98efde04412 100644 --- a/extensions/github-copilot/index.test.ts +++ b/extensions/github-copilot/index.test.ts @@ -5,8 +5,8 @@ import { clearRuntimeAuthProfileStoreSnapshots, ensureAuthProfileStore, } from "openclaw/plugin-sdk/agent-runtime"; +import { createTestPluginApi } from "openclaw/plugin-sdk/plugin-test-api"; import { afterEach, describe, expect, it, vi } from "vitest"; -import { createTestPluginApi } from "../../test/helpers/plugins/plugin-api.js"; const resolveCopilotApiTokenMock = vi.hoisted(() => vi.fn()); diff --git a/extensions/google-meet/src/test-support/plugin-harness.ts b/extensions/google-meet/src/test-support/plugin-harness.ts index 16e9daff524..8c95618fb01 100644 --- a/extensions/google-meet/src/test-support/plugin-harness.ts +++ b/extensions/google-meet/src/test-support/plugin-harness.ts @@ -1,6 +1,6 @@ import type { OpenClawPluginApi } from "openclaw/plugin-sdk/plugin-entry"; +import { createTestPluginApi } from "openclaw/plugin-sdk/plugin-test-api"; import { vi } from "vitest"; -import { createTestPluginApi } from "../../../../test/helpers/plugins/plugin-api.ts"; type GoogleMeetTestPluginEntry = { register(api: OpenClawPluginApi): void; diff --git a/extensions/huggingface/index.test.ts b/extensions/huggingface/index.test.ts index f4748032099..0ba5f22caf8 100644 --- a/extensions/huggingface/index.test.ts +++ b/extensions/huggingface/index.test.ts @@ -1,5 +1,5 @@ +import { createTestPluginApi } from "openclaw/plugin-sdk/plugin-test-api"; import { describe, expect, it, vi } from "vitest"; -import { createTestPluginApi } from "../../test/helpers/plugins/plugin-api.js"; const buildHuggingfaceProviderMock = vi.hoisted(() => vi.fn(async () => ({ diff --git a/extensions/lobster/src/lobster-tool.test.ts b/extensions/lobster/src/lobster-tool.test.ts index 5fe715051d0..42c0365484b 100644 --- a/extensions/lobster/src/lobster-tool.test.ts +++ b/extensions/lobster/src/lobster-tool.test.ts @@ -1,5 +1,5 @@ +import { createTestPluginApi } from "openclaw/plugin-sdk/plugin-test-api"; import { describe, expect, it, vi } from "vitest"; -import { createTestPluginApi } from "../../../test/helpers/plugins/plugin-api.js"; import type { OpenClawPluginApi, OpenClawPluginToolContext } from "../runtime-api.js"; import { createLobsterTool } from "./lobster-tool.js"; import { createFakeTaskFlow } from "./taskflow-test-helpers.js"; diff --git a/extensions/matrix/index.test.ts b/extensions/matrix/index.test.ts index 736388e1c92..1358897de6b 100644 --- a/extensions/matrix/index.test.ts +++ b/extensions/matrix/index.test.ts @@ -1,5 +1,5 @@ +import { createTestPluginApi } from "openclaw/plugin-sdk/plugin-test-api"; import { describe, expect, it, vi } from "vitest"; -import { createTestPluginApi } from "../../test/helpers/plugins/plugin-api.js"; import { registerMatrixCliMetadata } from "./cli-metadata.js"; import entry, { registerMatrixFullRuntime } from "./index.js"; diff --git a/extensions/mattermost/src/setup.test.ts b/extensions/mattermost/src/setup.test.ts index 1e37b92a939..bc48c3d1669 100644 --- a/extensions/mattermost/src/setup.test.ts +++ b/extensions/mattermost/src/setup.test.ts @@ -1,6 +1,6 @@ +import { createTestPluginApi } from "openclaw/plugin-sdk/plugin-test-api"; import { DEFAULT_ACCOUNT_ID } from "openclaw/plugin-sdk/setup"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; -import { createTestPluginApi } from "../../../test/helpers/plugins/plugin-api.js"; import type { OpenClawConfig, OpenClawPluginApi } from "../runtime-api.js"; vi.mock("../../../test/helpers/config/bundled-channel-config-runtime.js", () => ({ diff --git a/extensions/memory-wiki/cli-metadata.test.ts b/extensions/memory-wiki/cli-metadata.test.ts index 8aa36f253be..eb8b3c4d5a3 100644 --- a/extensions/memory-wiki/cli-metadata.test.ts +++ b/extensions/memory-wiki/cli-metadata.test.ts @@ -1,6 +1,6 @@ import { Command } from "commander"; +import { createTestPluginApi } from "openclaw/plugin-sdk/plugin-test-api"; import { beforeEach, describe, expect, it, vi } from "vitest"; -import { createTestPluginApi } from "../../test/helpers/plugins/plugin-api.js"; const mocks = vi.hoisted(() => ({ registerWikiCli: vi.fn(), diff --git a/extensions/memory-wiki/src/test-helpers.ts b/extensions/memory-wiki/src/test-helpers.ts index 3b9521e12f3..019cf133ba1 100644 --- a/extensions/memory-wiki/src/test-helpers.ts +++ b/extensions/memory-wiki/src/test-helpers.ts @@ -1,8 +1,8 @@ import fs from "node:fs/promises"; import path from "node:path"; +import { createTestPluginApi } from "openclaw/plugin-sdk/plugin-test-api"; import { resolvePreferredOpenClawTmpDir } from "openclaw/plugin-sdk/temp-path"; import { afterEach, vi } from "vitest"; -import { createTestPluginApi } from "../../../test/helpers/plugins/plugin-api.js"; import type { OpenClawPluginApi } from "../api.js"; import { resolveMemoryWikiConfig, diff --git a/extensions/microsoft-foundry/index.test.ts b/extensions/microsoft-foundry/index.test.ts index 8f19f4e9eb6..fb96bfc8b02 100644 --- a/extensions/microsoft-foundry/index.test.ts +++ b/extensions/microsoft-foundry/index.test.ts @@ -1,6 +1,6 @@ import type { OpenClawConfig } from "openclaw/plugin-sdk/config-types"; +import { createTestPluginApi } from "openclaw/plugin-sdk/plugin-test-api"; import { beforeEach, describe, expect, it, vi } from "vitest"; -import { createTestPluginApi } from "../../test/helpers/plugins/plugin-api.js"; import { getAccessTokenResultAsync } from "./cli.js"; import plugin from "./index.js"; import { buildFoundryConnectionTest, isValidTenantIdentifier } from "./onboard.js"; diff --git a/extensions/ollama/index.test.ts b/extensions/ollama/index.test.ts index d4b4560b37c..46db238f598 100644 --- a/extensions/ollama/index.test.ts +++ b/extensions/ollama/index.test.ts @@ -1,5 +1,5 @@ +import { createTestPluginApi } from "openclaw/plugin-sdk/plugin-test-api"; import { beforeEach, describe, expect, it, vi } from "vitest"; -import { createTestPluginApi } from "../../test/helpers/plugins/plugin-api.js"; import plugin from "./index.js"; const promptAndConfigureOllamaMock = vi.hoisted(() => diff --git a/extensions/openai/index.test.ts b/extensions/openai/index.test.ts index 58a51676c05..ef1b02abb41 100644 --- a/extensions/openai/index.test.ts +++ b/extensions/openai/index.test.ts @@ -1,10 +1,10 @@ import type { OpenClawConfig } from "openclaw/plugin-sdk/config-types"; +import { createTestPluginApi } from "openclaw/plugin-sdk/plugin-test-api"; import * as providerAuth from "openclaw/plugin-sdk/provider-auth-runtime"; import * as providerHttp from "openclaw/plugin-sdk/provider-http"; import type { ProviderPlugin } from "openclaw/plugin-sdk/provider-model-shared"; import { registerProviderPlugin, requireRegisteredProvider } from "openclaw/plugin-sdk/testing"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -import { createTestPluginApi } from "../../test/helpers/plugins/plugin-api.js"; import { buildOpenAIImageGenerationProvider } from "./image-generation-provider.js"; import plugin from "./index.js"; import { diff --git a/extensions/phone-control/index.test.ts b/extensions/phone-control/index.test.ts index 8f46929d72c..4e1b5ad7a60 100644 --- a/extensions/phone-control/index.test.ts +++ b/extensions/phone-control/index.test.ts @@ -1,8 +1,8 @@ import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; +import { createTestPluginApi } from "openclaw/plugin-sdk/plugin-test-api"; import { describe, expect, it, vi } from "vitest"; -import { createTestPluginApi } from "../../test/helpers/plugins/plugin-api.js"; import registerPhoneControl from "./index.js"; import type { OpenClawPluginApi, diff --git a/extensions/signal/src/inbound-context.contract.test.ts b/extensions/signal/src/inbound-context.contract.test.ts index d7071609778..7a8a7361f70 100644 --- a/extensions/signal/src/inbound-context.contract.test.ts +++ b/extensions/signal/src/inbound-context.contract.test.ts @@ -1,5 +1,5 @@ +import { expectChannelInboundContextContract } from "openclaw/plugin-sdk/channel-contract-testing"; import { finalizeInboundContext } from "openclaw/plugin-sdk/reply-runtime"; -import { expectChannelInboundContextContract } from "openclaw/plugin-sdk/testing"; import { describe, it } from "vitest"; describe("Signal inbound context contract", () => { diff --git a/extensions/signal/src/monitor/event-handler.inbound-context.test.ts b/extensions/signal/src/monitor/event-handler.inbound-context.test.ts index ece9ae1521e..c3b74db6ec2 100644 --- a/extensions/signal/src/monitor/event-handler.inbound-context.test.ts +++ b/extensions/signal/src/monitor/event-handler.inbound-context.test.ts @@ -1,5 +1,5 @@ +import { expectChannelInboundContextContract as expectInboundContextContract } from "openclaw/plugin-sdk/channel-contract-testing"; import type { MsgContext } from "openclaw/plugin-sdk/reply-runtime"; -import { expectChannelInboundContextContract as expectInboundContextContract } from "openclaw/plugin-sdk/testing"; import { beforeEach, describe, expect, it, vi } from "vitest"; vi.useRealTimers(); const [ diff --git a/extensions/signal/src/monitor/event-handler.mention-gating.test.ts b/extensions/signal/src/monitor/event-handler.mention-gating.test.ts index eca2eacd095..2033ea2562d 100644 --- a/extensions/signal/src/monitor/event-handler.mention-gating.test.ts +++ b/extensions/signal/src/monitor/event-handler.mention-gating.test.ts @@ -1,6 +1,6 @@ +import { buildDispatchInboundCaptureMock } from "openclaw/plugin-sdk/channel-contract-testing"; import type { OpenClawConfig } from "openclaw/plugin-sdk/config-types"; import type { MsgContext } from "openclaw/plugin-sdk/reply-runtime"; -import { buildDispatchInboundCaptureMock } from "openclaw/plugin-sdk/testing"; import { beforeEach, describe, expect, it, vi } from "vitest"; type SignalMsgContext = Pick & { diff --git a/extensions/skill-workshop/index.test.ts b/extensions/skill-workshop/index.test.ts index c322c6c9f6f..68fe1fc5d73 100644 --- a/extensions/skill-workshop/index.test.ts +++ b/extensions/skill-workshop/index.test.ts @@ -2,8 +2,8 @@ import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; import type { AnyAgentTool } from "openclaw/plugin-sdk/agent-runtime"; +import { createTestPluginApi } from "openclaw/plugin-sdk/plugin-test-api"; import { afterEach, describe, expect, it, vi } from "vitest"; -import { createTestPluginApi } from "../../test/helpers/plugins/plugin-api.js"; import plugin, { applyProposalToWorkspace, createProposalFromMessages, diff --git a/extensions/slack/src/http/plugin-routes.test.ts b/extensions/slack/src/http/plugin-routes.test.ts index 5e47e3ee98c..d809197a1b9 100644 --- a/extensions/slack/src/http/plugin-routes.test.ts +++ b/extensions/slack/src/http/plugin-routes.test.ts @@ -1,6 +1,6 @@ import type { IncomingMessage, ServerResponse } from "node:http"; +import { createTestPluginApi } from "openclaw/plugin-sdk/plugin-test-api"; import { describe, expect, it, vi } from "vitest"; -import { createTestPluginApi } from "../../../../test/helpers/plugins/plugin-api.js"; import type { OpenClawConfig, OpenClawPluginApi } from "../runtime-api.js"; import { registerSlackPluginHttpRoutes } from "./plugin-routes.js"; import { registerSlackHttpHandler } from "./registry.js"; diff --git a/extensions/telegram/src/inbound-context.contract.test.ts b/extensions/telegram/src/inbound-context.contract.test.ts index bbc05b72430..c5aec97766e 100644 --- a/extensions/telegram/src/inbound-context.contract.test.ts +++ b/extensions/telegram/src/inbound-context.contract.test.ts @@ -1,7 +1,5 @@ -import { - expectChannelInboundContextContract, - type OpenClawConfig, -} from "openclaw/plugin-sdk/testing"; +import { expectChannelInboundContextContract } from "openclaw/plugin-sdk/channel-contract-testing"; +import { type OpenClawConfig } from "openclaw/plugin-sdk/testing"; import { describe, it } from "vitest"; import { buildTelegramMessageContextForTest } from "./bot-message-context.test-harness.js"; diff --git a/extensions/telegram/src/test-support/inbound-context-contract.ts b/extensions/telegram/src/test-support/inbound-context-contract.ts index 124067f12ca..2b43b722b43 100644 --- a/extensions/telegram/src/test-support/inbound-context-contract.ts +++ b/extensions/telegram/src/test-support/inbound-context-contract.ts @@ -1 +1 @@ -export { expectChannelInboundContextContract } from "openclaw/plugin-sdk/testing"; +export { expectChannelInboundContextContract } from "openclaw/plugin-sdk/channel-contract-testing"; diff --git a/extensions/tokenjuice/index.test.ts b/extensions/tokenjuice/index.test.ts index a6da8e58532..ede7c7769c9 100644 --- a/extensions/tokenjuice/index.test.ts +++ b/extensions/tokenjuice/index.test.ts @@ -1,6 +1,6 @@ import fs from "node:fs"; +import { createTestPluginApi } from "openclaw/plugin-sdk/plugin-test-api"; import { beforeEach, describe, expect, it, vi } from "vitest"; -import { createTestPluginApi } from "../../test/helpers/plugins/plugin-api.js"; const { tokenjuiceFactory, createTokenjuiceOpenClawEmbeddedExtension } = vi.hoisted(() => { const tokenjuiceFactory = vi.fn(); diff --git a/extensions/voice-call/index.test.ts b/extensions/voice-call/index.test.ts index cd17af27fc1..79f12ed15d3 100644 --- a/extensions/voice-call/index.test.ts +++ b/extensions/voice-call/index.test.ts @@ -2,8 +2,8 @@ import fs from "node:fs"; import os from "node:os"; import path from "node:path"; import { Command } from "commander"; +import { createTestPluginApi } from "openclaw/plugin-sdk/plugin-test-api"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -import { createTestPluginApi } from "../../test/helpers/plugins/plugin-api.ts"; import type { OpenClawPluginApi } from "./api.js"; import type { VoiceCallRuntime } from "./runtime-entry.js"; diff --git a/extensions/webhooks/index.test.ts b/extensions/webhooks/index.test.ts index 14e60b0d099..65be0461a8c 100644 --- a/extensions/webhooks/index.test.ts +++ b/extensions/webhooks/index.test.ts @@ -1,5 +1,5 @@ +import { createTestPluginApi } from "openclaw/plugin-sdk/plugin-test-api"; import { describe, expect, it, vi } from "vitest"; -import { createTestPluginApi } from "../../test/helpers/plugins/plugin-api.js"; import type { OpenClawPluginApi } from "./api.js"; import plugin from "./index.js"; diff --git a/extensions/xai/index.test.ts b/extensions/xai/index.test.ts index f9b40f549fb..9fbe33cbe24 100644 --- a/extensions/xai/index.test.ts +++ b/extensions/xai/index.test.ts @@ -1,7 +1,7 @@ import type { OpenClawPluginApi } from "openclaw/plugin-sdk/plugin-entry"; +import { createTestPluginApi } from "openclaw/plugin-sdk/plugin-test-api"; import { registerProviderPlugin, registerSingleProviderPlugin } from "openclaw/plugin-sdk/testing"; import { describe, expect, it } from "vitest"; -import { createTestPluginApi } from "../../test/helpers/plugins/plugin-api.js"; import plugin from "./index.js"; import setupPlugin from "./setup-api.js"; import { diff --git a/package.json b/package.json index 2ca48bf6b0a..e734a806536 100644 --- a/package.json +++ b/package.json @@ -502,6 +502,10 @@ "types": "./dist/plugin-sdk/lazy-runtime.d.ts", "default": "./dist/plugin-sdk/lazy-runtime.js" }, + "./plugin-sdk/plugin-test-api": { + "types": "./dist/plugin-sdk/plugin-test-api.d.ts", + "default": "./dist/plugin-sdk/plugin-test-api.js" + }, "./plugin-sdk/testing": { "types": "./dist/plugin-sdk/testing.d.ts", "default": "./dist/plugin-sdk/testing.js" diff --git a/scripts/check-no-extension-test-core-imports.ts b/scripts/check-no-extension-test-core-imports.ts index 397cd25dd1d..75c49613084 100644 --- a/scripts/check-no-extension-test-core-imports.ts +++ b/scripts/check-no-extension-test-core-imports.ts @@ -1,6 +1,6 @@ import fs from "node:fs"; import path from "node:path"; -import { collectFilesSync, relativeToCwd } from "./check-file-utils.js"; +import { collectFilesSync, isCodeFile, relativeToCwd } from "./check-file-utils.js"; type Offender = { file: string; hint: string; line?: number; specifier?: string }; @@ -49,6 +49,7 @@ const RETIRED_EXTENSION_TEST_HELPER_BRIDGE_FILES = [ "test/helpers/plugins/frozen-time.ts", "test/helpers/plugins/media-understanding.ts", "test/helpers/plugins/mock-http-response.ts", + "test/helpers/plugins/plugin-api.ts", "test/helpers/plugins/plugin-registration.ts", "test/helpers/plugins/plugin-registry.ts", "test/helpers/plugins/provider-registration.ts", @@ -80,6 +81,12 @@ function collectExtensionTestFiles(rootDir: string): string[] { }); } +function collectPluginHelperFiles(rootDir: string): string[] { + return collectFilesSync(rootDir, { + includeFile: isCodeFile, + }); +} + function lineNumberForOffset(content: string, offset: number): number { let line = 1; for (let index = 0; index < offset; index += 1) { @@ -127,7 +134,9 @@ function collectRelativeCoreImportOffenders( function main() { const extensionsDir = path.join(process.cwd(), "extensions"); + const pluginHelpersDir = path.join(process.cwd(), "test/helpers/plugins"); const files = collectExtensionTestFiles(extensionsDir); + const pluginHelperFiles = collectPluginHelperFiles(pluginHelpersDir); const offenders: Offender[] = []; for (const file of RETIRED_EXTENSION_TEST_HELPER_BRIDGE_FILES) { @@ -137,7 +146,7 @@ function main() { } offenders.push({ file: filePath, - hint: "Import the helper directly from openclaw/plugin-sdk/testing instead of recreating this bridge.", + hint: "Import the helper directly from a documented openclaw/plugin-sdk testing subpath instead of recreating this bridge.", }); } @@ -157,8 +166,19 @@ function main() { ); } + for (const file of pluginHelperFiles) { + const content = fs.readFileSync(file, "utf8"); + offenders.push( + ...collectRelativeCoreImportOffenders(file, content, { + includeDynamic: true, + }), + ); + } + if (offenders.length > 0) { - console.error("Extension test files must stay on public plugin-sdk surfaces."); + console.error( + "Extension test files and plugin test helpers must stay on public plugin-sdk surfaces.", + ); for (const offender of offenders.toSorted((a, b) => a.file.localeCompare(b.file))) { const location = offender.line ? `${relativeToCwd(offender.file)}:${offender.line}` @@ -170,7 +190,7 @@ function main() { } console.log( - `OK: extension test files and support helpers avoid direct core test/internal imports (${files.length} checked).`, + `OK: extension test files, support helpers, and plugin test helpers avoid direct core test/internal imports (${files.length} extension files, ${pluginHelperFiles.length} plugin helpers checked).`, ); } diff --git a/scripts/lib/plugin-sdk-entrypoints.json b/scripts/lib/plugin-sdk-entrypoints.json index 8b4ec34338d..dc6fbe8366e 100644 --- a/scripts/lib/plugin-sdk-entrypoints.json +++ b/scripts/lib/plugin-sdk-entrypoints.json @@ -109,6 +109,7 @@ "acp-binding-runtime", "acp-binding-resolve-runtime", "lazy-runtime", + "plugin-test-api", "testing", "temp-path", "logging-core", diff --git a/src/plugin-sdk/channel-contract-testing.ts b/src/plugin-sdk/channel-contract-testing.ts index c5c87d46bb6..9104ff87b2d 100644 --- a/src/plugin-sdk/channel-contract-testing.ts +++ b/src/plugin-sdk/channel-contract-testing.ts @@ -2,6 +2,7 @@ export { expectChannelInboundContextContract, primeChannelOutboundSendMock, } from "../channels/plugins/contracts/test-helpers.js"; +export { buildDispatchInboundCaptureMock } from "../channels/plugins/contracts/inbound-testkit.js"; export { installChannelOutboundPayloadContractSuite, type OutboundPayloadHarnessParams, diff --git a/test/helpers/plugins/plugin-api.ts b/src/plugin-sdk/plugin-test-api.ts similarity index 93% rename from test/helpers/plugins/plugin-api.ts rename to src/plugin-sdk/plugin-test-api.ts index 68db5993506..671f6947257 100644 --- a/test/helpers/plugins/plugin-api.ts +++ b/src/plugin-sdk/plugin-test-api.ts @@ -1,6 +1,6 @@ -import type { OpenClawPluginApi } from "openclaw/plugin-sdk/plugin-runtime"; +import type { OpenClawPluginApi } from "./plugin-runtime.js"; -type TestPluginApiInput = Partial; +export type TestPluginApiInput = Partial; export function createTestPluginApi(api: TestPluginApiInput = {}): OpenClawPluginApi { return {