test: keep extension mocks on sdk seams

This commit is contained in:
Peter Steinberger
2026-04-27 22:55:01 +01:00
parent c35a96bcbc
commit 8599fdda4a
26 changed files with 292 additions and 145 deletions

View File

@@ -99,6 +99,7 @@ For the plugin authoring guide, see [Plugin SDK overview](/plugins/sdk-overview)
| `plugin-sdk/provider-env-vars` | Provider auth env-var lookup helpers |
| `plugin-sdk/provider-auth` | `createProviderApiKeyAuthMethod`, `ensureApiKeyFromOptionEnvOrPrompt`, `upsertAuthProfile`, `upsertApiKeyProfile`, `writeOAuthCredentials` |
| `plugin-sdk/provider-model-shared` | `ProviderReplayFamily`, `buildProviderReplayFamilyHooks`, `normalizeModelCompat`, shared replay-policy builders, provider-endpoint helpers, and model-id normalization helpers such as `normalizeNativeXaiModelId` |
| `plugin-sdk/provider-catalog-runtime` | Provider catalog runtime hook and plugin-provider registry seams for contract tests |
| `plugin-sdk/provider-catalog-shared` | `findCatalogTemplate`, `buildSingleProviderApiKeyCatalog`, `supportsNativeStreamingUsageCompat`, `applyProviderNativeStreamingUsageCompat` |
| `plugin-sdk/provider-http` | Generic provider HTTP/endpoint capability helpers, provider HTTP errors, and audio transcription multipart form helpers |
| `plugin-sdk/provider-web-fetch-contract` | Narrow web-fetch config/selection contract helpers such as `enablePluginInConfig` and `WebFetchProviderPlugin` |

View File

@@ -10,11 +10,6 @@ vi.mock("./channel.runtime.js", () => ({
},
}));
vi.mock("../../../src/channels/plugins/bundled.js", () => ({
bundledChannelPlugins: [],
bundledChannelSetupPlugins: [],
}));
let bluebubblesPlugin: typeof import("./channel.js").bluebubblesPlugin;
describe("bluebubblesPlugin.status.probeAccount", () => {

View File

@@ -16,9 +16,15 @@ const gatewayMocks = vi.hoisted(() => ({
})),
}));
vi.mock("../../../../src/cli/gateway-rpc.js", () => ({
callGatewayFromCli: gatewayMocks.callGatewayFromCli,
}));
vi.mock("openclaw/plugin-sdk/browser-node-runtime", async () => {
const actual = await vi.importActual<typeof import("openclaw/plugin-sdk/browser-node-runtime")>(
"openclaw/plugin-sdk/browser-node-runtime",
);
return {
...actual,
callGatewayFromCli: gatewayMocks.callGatewayFromCli,
};
});
const configMocks = vi.hoisted(() => {
const loadConfig = vi.fn(() => ({ browser: {} }));

View File

@@ -18,10 +18,16 @@ vi.mock("openclaw/plugin-sdk/runtime-config-snapshot", async () => {
};
});
vi.mock("../../../../src/gateway/node-command-policy.js", () => ({
isNodeCommandAllowed: isNodeCommandAllowedMock,
resolveNodeCommandAllowlist: resolveNodeCommandAllowlistMock,
}));
vi.mock("openclaw/plugin-sdk/browser-node-runtime", async () => {
const actual = await vi.importActual<typeof import("openclaw/plugin-sdk/browser-node-runtime")>(
"openclaw/plugin-sdk/browser-node-runtime",
);
return {
...actual,
isNodeCommandAllowed: isNodeCommandAllowedMock,
resolveNodeCommandAllowlist: resolveNodeCommandAllowlistMock,
};
});
import { browserHandlers } from "./browser-request.js";

View File

@@ -16,7 +16,7 @@ const oauthMocks = vi.hoisted(() => ({
const providerRuntimeMocks = vi.hoisted(() => ({
formatProviderAuthProfileApiKeyWithPlugin: vi.fn(),
refreshProviderOAuthCredentialWithPlugin: vi.fn(
async (params: { context: { refresh: string } }) => {
async (params: { provider?: string; context: { refresh: string } }) => {
const refreshed = await oauthMocks.refreshOpenAICodexToken(params.context.refresh);
return refreshed
? {
@@ -37,12 +37,57 @@ vi.mock("@mariozechner/pi-ai/oauth", () => ({
refreshOpenAICodexToken: oauthMocks.refreshOpenAICodexToken,
}));
vi.mock("../../../../src/plugins/provider-runtime.runtime.js", () => ({
formatProviderAuthProfileApiKeyWithPlugin:
providerRuntimeMocks.formatProviderAuthProfileApiKeyWithPlugin,
refreshProviderOAuthCredentialWithPlugin:
providerRuntimeMocks.refreshProviderOAuthCredentialWithPlugin,
}));
vi.mock("openclaw/plugin-sdk/agent-runtime", async (importOriginal) => {
const actual = await importOriginal<typeof import("openclaw/plugin-sdk/agent-runtime")>();
return {
...actual,
resolveApiKeyForProfile: async (
params: Parameters<typeof actual.resolveApiKeyForProfile>[0],
) => {
const credential = params.store.profiles[params.profileId];
if (!credential) {
return null;
}
if (credential.type === "api_key") {
const apiKey =
credential.key?.trim() ||
(credential.keyRef?.source === "env" ? process.env[credential.keyRef.id]?.trim() : "");
return apiKey ? { apiKey, provider: credential.provider } : null;
}
if (credential.type === "token") {
const apiKey =
credential.token?.trim() ||
(credential.tokenRef?.source === "env"
? process.env[credential.tokenRef.id]?.trim()
: "");
return apiKey ? { apiKey, provider: credential.provider, email: credential.email } : null;
}
let oauthCredential = credential;
if ((oauthCredential.expires ?? 0) <= Date.now()) {
const refreshed = await providerRuntimeMocks.refreshProviderOAuthCredentialWithPlugin({
provider: oauthCredential.provider,
context: oauthCredential,
});
if (refreshed?.access) {
oauthCredential = refreshed as typeof oauthCredential;
params.store.profiles[params.profileId] = oauthCredential;
if (params.agentDir) {
actual.saveAuthProfileStore(params.store, params.agentDir);
}
}
}
const formatted = await providerRuntimeMocks.formatProviderAuthProfileApiKeyWithPlugin({
provider: oauthCredential.provider,
context: oauthCredential,
});
const apiKey =
typeof formatted === "string" && formatted ? formatted : oauthCredential.access;
return apiKey
? { apiKey, provider: oauthCredential.provider, email: oauthCredential.email }
: null;
},
};
});
afterEach(() => {
vi.unstubAllEnvs();

View File

@@ -25,9 +25,15 @@ type DiscordReactionClient = Parameters<
const readAllowFromStoreMock = vi.hoisted(() => vi.fn());
vi.mock("../../../src/pairing/pairing-store.js", () => ({
readChannelAllowFromStore: (...args: unknown[]) => readAllowFromStoreMock(...args),
}));
vi.mock("openclaw/plugin-sdk/conversation-runtime", async () => {
const actual = await vi.importActual<typeof import("openclaw/plugin-sdk/conversation-runtime")>(
"openclaw/plugin-sdk/conversation-runtime",
);
return {
...actual,
readChannelAllowFromStore: (...args: unknown[]) => readAllowFromStoreMock(...args),
};
});
const fakeGuild = (id: string, name: string) => ({ id, name }) as Guild;

View File

@@ -4,6 +4,7 @@ import path from "node:path";
import { ChannelType, type AutocompleteInteraction } from "@buape/carbon";
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-types";
import { clearSessionStoreCacheForTest } from "openclaw/plugin-sdk/session-store-runtime";
import { createEmptyPluginRegistry, setActivePluginRegistry } from "openclaw/plugin-sdk/testing";
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import { createNoopThreadBindingManager } from "./thread-bindings.js";
@@ -125,14 +126,44 @@ let findCommandByNativeName: typeof import("openclaw/plugin-sdk/command-auth").f
let resolveCommandArgChoices: typeof import("openclaw/plugin-sdk/command-auth").resolveCommandArgChoices;
let resolveDiscordNativeChoiceContext: typeof import("./native-command-ui.js").resolveDiscordNativeChoiceContext;
function installProviderThinkingRegistryForTest(): void {
const registry = createEmptyPluginRegistry();
registry.providers.push({
pluginId: "discord-test",
source: "test",
provider: {
id: "discord-test-thinking",
label: "Discord Test Thinking",
aliases: ["anthropic", "openai-codex"],
auth: [],
isBinaryThinking: (context) =>
providerThinkingMocks.resolveProviderBinaryThinking({
provider: context.provider,
context,
}),
supportsXHighThinking: (context) =>
providerThinkingMocks.resolveProviderXHighThinking({
provider: context.provider,
context,
}),
resolveThinkingProfile: (context) =>
providerThinkingMocks.resolveProviderThinkingProfile({
provider: context.provider,
context,
}),
resolveDefaultThinkingLevel: (context) =>
providerThinkingMocks.resolveProviderDefaultThinkingLevel({
provider: context.provider,
context,
}),
},
});
setActivePluginRegistry(registry);
}
async function loadDiscordThinkAutocompleteModulesForTest() {
vi.resetModules();
vi.doMock("../../../../src/plugins/provider-thinking.js", () => ({
resolveProviderBinaryThinking: providerThinkingMocks.resolveProviderBinaryThinking,
resolveProviderDefaultThinkingLevel: providerThinkingMocks.resolveProviderDefaultThinkingLevel,
resolveProviderThinkingProfile: providerThinkingMocks.resolveProviderThinkingProfile,
resolveProviderXHighThinking: providerThinkingMocks.resolveProviderXHighThinking,
}));
installProviderThinkingRegistryForTest();
const commandAuth = await import("openclaw/plugin-sdk/command-auth");
const nativeCommandUi = await import("./native-command-ui.js");
return {
@@ -183,6 +214,7 @@ describe("discord native /think autocomplete", () => {
? true
: undefined,
);
installProviderThinkingRegistryForTest();
fs.mkdirSync(path.dirname(STORE_PATH), { recursive: true });
fs.writeFileSync(
STORE_PATH,

View File

@@ -55,11 +55,6 @@ vi.mock("./channel.runtime.js", () => ({
},
}));
vi.mock("../../../src/channels/plugins/bundled.js", () => ({
bundledChannelPlugins: [],
bundledChannelSetupPlugins: [],
}));
function getDescribedActions(cfg: OpenClawConfig, accountId?: string): string[] {
return [...(feishuPlugin.actions?.describeMessageTool?.({ cfg, accountId })?.actions ?? [])];
}

View File

@@ -91,11 +91,6 @@ vi.mock("./subagent-hooks.js", () => ({
registerFeishuSubagentHooks: registerFeishuSubagentHooksMock,
}));
vi.mock("../../../src/channels/plugins/bundled.js", () => ({
bundledChannelPlugins: [],
bundledChannelSetupPlugins: [],
}));
const baseAccount: ResolvedFeishuAccount = {
accountId: "main",
selectionSource: "explicit",

View File

@@ -218,10 +218,3 @@ vi.mock("openclaw/plugin-sdk/conversation-runtime", async () => {
}),
};
});
vi.mock("../../../src/infra/outbound/session-binding-service.js", () => ({
getSessionBindingService: () => ({
resolveByConversation: resolveBoundConversationMock,
touch: touchBindingMock,
}),
}));

View File

@@ -51,11 +51,6 @@ vi.mock("openclaw/plugin-sdk/media-runtime", async (importOriginal) => {
};
});
vi.mock("../../../src/channels/plugins/bundled.js", () => ({
bundledChannelPlugins: [],
bundledChannelSetupPlugins: [],
}));
let downloadImageFeishu: typeof import("./media.js").downloadImageFeishu;
let downloadMessageResourceFeishu: typeof import("./media.js").downloadMessageResourceFeishu;
let sanitizeFileNameForUpload: typeof import("./media.js").sanitizeFileNameForUpload;

View File

@@ -20,11 +20,6 @@ vi.mock("./runtime.js", () => ({
}),
}));
vi.mock("../../../src/channels/plugins/bundled.js", () => ({
bundledChannelPlugins: [],
bundledChannelSetupPlugins: [],
}));
let sendCardFeishu: typeof import("./send.js").sendCardFeishu;
let sendMessageFeishu: typeof import("./send.js").sendMessageFeishu;

View File

@@ -1,25 +1,37 @@
import { join, parse } from "node:path";
import { describe, expect, it, vi, beforeAll, beforeEach, afterEach } from "vitest";
vi.mock("../../src/infra/wsl.js", () => ({
isWSL2Sync: () => false,
}));
vi.mock("openclaw/plugin-sdk/runtime-env", async () => {
const actual = await vi.importActual<typeof import("openclaw/plugin-sdk/runtime-env")>(
"openclaw/plugin-sdk/runtime-env",
);
return {
...actual,
isWSL2Sync: () => false,
};
});
vi.mock("../../src/infra/net/fetch-guard.js", () => ({
fetchWithSsrFGuard: async (params: {
url: string;
init?: RequestInit;
fetchImpl?: (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
}) => {
const fetchImpl = params.fetchImpl ?? globalThis.fetch;
const response = await fetchImpl(params.url, params.init);
return {
response,
finalUrl: params.url,
release: async () => {},
};
},
}));
vi.mock("openclaw/plugin-sdk/ssrf-runtime", async () => {
const actual = await vi.importActual<typeof import("openclaw/plugin-sdk/ssrf-runtime")>(
"openclaw/plugin-sdk/ssrf-runtime",
);
return {
...actual,
fetchWithSsrFGuard: async (params: {
url: string;
init?: RequestInit;
fetchImpl?: (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
}) => {
const fetchImpl = params.fetchImpl ?? globalThis.fetch;
const response = await fetchImpl(params.url, params.init);
return {
response,
finalUrl: params.url,
release: async () => {},
};
},
};
});
const mockExistsSync = vi.fn();
const mockReadFileSync = vi.fn();

View File

@@ -12,11 +12,6 @@ vi.mock("../../../test/helpers/config/bundled-channel-config-runtime.js", () =>
getBundledChannelConfigSchemaMap: () => new Map(),
}));
vi.mock("../../../src/channels/plugins/bundled.js", () => ({
bundledChannelPlugins: [],
bundledChannelSetupPlugins: [],
}));
describe("nextcloud talk channel core", () => {
it("accepts SecretRef botSecret and apiPassword at top-level", () => {
const result = NextcloudTalkConfigSchema.safeParse({

View File

@@ -27,17 +27,47 @@ const resolveCatalogHookProviderPluginIdsMock = vi.hoisted(() =>
vi.fn<ResolveCatalogHookProviderPluginIds>((_) => [] as string[]),
);
vi.mock("../../../src/plugins/providers.js", () => ({
resolveOwningPluginIdsForProvider: (params: unknown) =>
resolveOwningPluginIdsForProviderMock(params as never),
resolveCatalogHookProviderPluginIds: (params: unknown) =>
resolveCatalogHookProviderPluginIdsMock(params as never),
}));
vi.mock("../../../src/plugins/providers.runtime.js", () => ({
isPluginProvidersLoadInFlight: () => false,
resolvePluginProviders: (params: unknown) => resolvePluginProvidersMock(params as never),
}));
vi.mock("openclaw/plugin-sdk/provider-catalog-runtime", async () => {
const actual = await vi.importActual<
typeof import("openclaw/plugin-sdk/provider-catalog-runtime")
>("openclaw/plugin-sdk/provider-catalog-runtime");
const resolveCatalogHookProviders = (params: unknown) =>
resolvePluginProvidersMock({
onlyPluginIds: resolveCatalogHookProviderPluginIdsMock(params),
});
return {
...actual,
augmentModelCatalogWithProviderPlugins: async (params: {
context: Parameters<NonNullable<ProviderPlugin["augmentModelCatalog"]>>[0];
}) => {
const supplemental = [];
for (const provider of resolveCatalogHookProviders(params)) {
const entries = await provider.augmentModelCatalog?.(params.context);
if (entries?.length) {
supplemental.push(...entries);
}
}
return supplemental;
},
resolveProviderBuiltInModelSuppression: (params: {
context: Parameters<NonNullable<ProviderPlugin["suppressBuiltInModel"]>>[0];
}) => {
for (const provider of resolveCatalogHookProviders(params)) {
const result = provider.suppressBuiltInModel?.(params.context);
if (result?.suppress) {
return result;
}
}
return undefined;
},
resolveOwningPluginIdsForProvider: (params: unknown) =>
resolveOwningPluginIdsForProviderMock(params as never),
resolveCatalogHookProviderPluginIds: (params: unknown) =>
resolveCatalogHookProviderPluginIdsMock(params as never),
isPluginProvidersLoadInFlight: () => false,
resolvePluginProviders: (params: unknown) => resolvePluginProvidersMock(params as never),
};
});
export function describeOpenAIProviderCatalogContract() {
const contractDepsPromise = (async () => {

View File

@@ -46,10 +46,16 @@ vi.mock("openclaw/plugin-sdk/reply-runtime", async () => {
};
});
vi.mock("../../../../src/pairing/pairing-store.js", () => ({
readChannelAllowFromStore: vi.fn().mockResolvedValue([]),
upsertChannelPairingRequest: vi.fn(),
}));
vi.mock("openclaw/plugin-sdk/conversation-runtime", async () => {
const actual = await vi.importActual<typeof import("openclaw/plugin-sdk/conversation-runtime")>(
"openclaw/plugin-sdk/conversation-runtime",
);
return {
...actual,
readChannelAllowFromStore: vi.fn().mockResolvedValue([]),
upsertChannelPairingRequest: vi.fn(),
};
});
describe("signal createSignalEventHandler inbound context", () => {
beforeEach(() => {

View File

@@ -8,28 +8,34 @@ const prepareSlackMessageMock =
>();
const dispatchPreparedSlackMessageMock = vi.fn<(prepared: unknown) => Promise<void>>();
vi.mock("../../../../src/channels/inbound-debounce-policy.js", () => ({
shouldDebounceTextInbound: () => false,
createChannelInboundDebouncer: (params: {
onFlush: (
entries: Array<{
message: Record<string, unknown>;
opts: { source: "message" | "app_mention"; wasMentioned?: boolean };
}>,
) => Promise<void>;
}) => ({
debounceMs: 0,
debouncer: {
enqueue: async (entry: {
message: Record<string, unknown>;
opts: { source: "message" | "app_mention"; wasMentioned?: boolean };
}) => {
await params.onFlush([entry]);
vi.mock("openclaw/plugin-sdk/channel-inbound", async () => {
const actual = await vi.importActual<typeof import("openclaw/plugin-sdk/channel-inbound")>(
"openclaw/plugin-sdk/channel-inbound",
);
return {
...actual,
shouldDebounceTextInbound: () => false,
createChannelInboundDebouncer: (params: {
onFlush: (
entries: Array<{
message: Record<string, unknown>;
opts: { source: "message" | "app_mention"; wasMentioned?: boolean };
}>,
) => Promise<void>;
}) => ({
debounceMs: 0,
debouncer: {
enqueue: async (entry: {
message: Record<string, unknown>;
opts: { source: "message" | "app_mention"; wasMentioned?: boolean };
}) => {
await params.onFlush([entry]);
},
flushKey: async (_key: string) => {},
},
flushKey: async (_key: string) => {},
},
}),
}));
}),
};
});
vi.mock("./thread-resolution.js", () => ({
createSlackThreadTsResolver: () => ({

View File

@@ -21,15 +21,24 @@ const fetchWithSsrFGuard = vi.fn(
}) as const,
);
vi.mock("../../../src/infra/net/fetch-guard.js", () => ({
vi.mock("openclaw/plugin-sdk/ssrf-runtime", () => ({
fetchWithSsrFGuard: (...args: unknown[]) =>
fetchWithSsrFGuard(...(args as [params: { url: string; init?: RequestInit }])),
withTrustedEnvProxyGuardedFetchMode: (params: Record<string, unknown>) => ({
...params,
mode: "trusted_env_proxy",
}),
}));
vi.mock("openclaw/plugin-sdk/fetch-runtime", async () => {
const actual = await vi.importActual<typeof import("openclaw/plugin-sdk/fetch-runtime")>(
"openclaw/plugin-sdk/fetch-runtime",
);
return {
...actual,
withTrustedEnvProxyGuardedFetchMode: (params: Record<string, unknown>) => ({
...params,
mode: "trusted_env_proxy",
}),
};
});
vi.mock("./runtime-api.js", async () => {
const actual = await vi.importActual<typeof import("./runtime-api.js")>("./runtime-api.js");
const mockedLoadOutboundMediaFromUrl =

View File

@@ -7,32 +7,12 @@ import type { OpenClawConfig } from "../runtime-api.js";
import { createTaskFlowWebhookRequestHandler, type TaskFlowWebhookTarget } from "./http.js";
const hoisted = vi.hoisted(() => {
const sendMessageMock = vi.fn();
const cancelSessionMock = vi.fn();
const killSubagentRunAdminMock = vi.fn();
const resolveConfiguredSecretInputStringMock = vi.fn();
return {
sendMessageMock,
cancelSessionMock,
killSubagentRunAdminMock,
resolveConfiguredSecretInputStringMock,
};
});
vi.mock("../../../src/tasks/task-registry-delivery-runtime.js", () => ({
sendMessage: hoisted.sendMessageMock,
}));
vi.mock("../../../src/acp/control-plane/manager.js", () => ({
getAcpSessionManager: () => ({
cancelSession: hoisted.cancelSessionMock,
}),
}));
vi.mock("../../../src/agents/subagent-control.js", () => ({
killSubagentRunAdmin: (params: unknown) => hoisted.killSubagentRunAdminMock(params),
}));
vi.mock("../runtime-api.js", async (importOriginal) => {
const actual = await importOriginal<typeof import("../runtime-api.js")>();
hoisted.resolveConfiguredSecretInputStringMock.mockImplementation(

View File

@@ -49,8 +49,12 @@ vi.mock("openclaw/plugin-sdk/runtime-config-snapshot", async () => {
};
});
vi.mock("../../../src/pairing/pairing-store.js", () => {
vi.mock("openclaw/plugin-sdk/conversation-runtime", async () => {
const actual = await vi.importActual<typeof import("openclaw/plugin-sdk/conversation-runtime")>(
"openclaw/plugin-sdk/conversation-runtime",
);
return {
...actual,
readChannelAllowFromStore(...args: unknown[]) {
return readAllowFromStoreMock(...args);
},
@@ -60,6 +64,18 @@ vi.mock("../../../src/pairing/pairing-store.js", () => {
};
});
vi.mock("openclaw/plugin-sdk/channel-pairing", async () => {
const actual = await vi.importActual<typeof import("openclaw/plugin-sdk/channel-pairing")>(
"openclaw/plugin-sdk/channel-pairing",
);
return {
...actual,
readChannelAllowFromStore(...args: unknown[]) {
return readAllowFromStoreMock(...args);
},
};
});
vi.mock("openclaw/plugin-sdk/media-store", async () => {
const actual = await vi.importActual<typeof import("openclaw/plugin-sdk/media-store")>(
"openclaw/plugin-sdk/media-store",

View File

@@ -6,9 +6,15 @@ const hoisted = vi.hoisted(() => ({
sendReactionWhatsApp: vi.fn(async () => undefined),
}));
vi.mock("../../../src/globals.js", () => ({
shouldLogVerbose: () => false,
}));
vi.mock("openclaw/plugin-sdk/runtime-env", async () => {
const actual = await vi.importActual<typeof import("openclaw/plugin-sdk/runtime-env")>(
"openclaw/plugin-sdk/runtime-env",
);
return {
...actual,
shouldLogVerbose: () => false,
};
});
vi.mock("./send.js", () => ({
sendPollWhatsApp: hoisted.sendPollWhatsApp,

View File

@@ -1162,6 +1162,10 @@
"types": "./dist/plugin-sdk/plugin-entry.d.ts",
"default": "./dist/plugin-sdk/plugin-entry.js"
},
"./plugin-sdk/provider-catalog-runtime": {
"types": "./dist/plugin-sdk/provider-catalog-runtime.d.ts",
"default": "./dist/plugin-sdk/provider-catalog-runtime.js"
},
"./plugin-sdk/provider-catalog-shared": {
"types": "./dist/plugin-sdk/provider-catalog-shared.d.ts",
"default": "./dist/plugin-sdk/provider-catalog-shared.js"

View File

@@ -37,6 +37,8 @@ const FORBIDDEN_PATTERNS: Array<{ pattern: RegExp; hint: string }> = [
const STATIC_RELATIVE_MODULE_PATTERN = /\b(?:import|export)\b[\s\S]*?\bfrom\s*["']([^"']+)["']/g;
const DYNAMIC_RELATIVE_MODULE_PATTERN = /\bimport\s*\(\s*["']([^"']+)["']\s*\)/g;
const MOCK_RELATIVE_MODULE_PATTERN =
/\bvi\.(?:mock|doMock|unmock|doUnmock)\s*\(\s*["']([^"']+)["']/g;
const RELATIVE_CORE_HINT =
"Use openclaw/plugin-sdk/testing or a focused plugin-sdk test/runtime subpath instead of core internals.";
@@ -88,6 +90,7 @@ function collectRelativeCoreImportOffenders(
const matches = [
...content.matchAll(STATIC_RELATIVE_MODULE_PATTERN),
...(opts.includeDynamic ? [...content.matchAll(DYNAMIC_RELATIVE_MODULE_PATTERN)] : []),
...content.matchAll(MOCK_RELATIVE_MODULE_PATTERN),
];
for (const match of matches) {
const specifier = match[1];

View File

@@ -274,6 +274,7 @@
"provider-auth-login",
"provider-selection-runtime",
"plugin-entry",
"provider-catalog-runtime",
"provider-catalog-shared",
"provider-entry",
"provider-env-vars",

View File

@@ -0,0 +1,15 @@
// Public provider-catalog runtime seams for provider plugin contract tests.
export {
augmentModelCatalogWithProviderPlugins,
resetProviderRuntimeHookCacheForTest,
resolveProviderBuiltInModelSuppression,
} from "../plugins/provider-runtime.js";
export {
resolveCatalogHookProviderPluginIds,
resolveOwningPluginIdsForProvider,
} from "../plugins/providers.js";
export {
isPluginProvidersLoadInFlight,
resolvePluginProviders,
} from "../plugins/providers.runtime.js";

View File

@@ -11,7 +11,7 @@ export {
} from "../../../src/test-utils/bundled-plugin-public-surface.js";
type ProviderRuntimeCatalogModule = Pick<
typeof import("../../../src/plugins/provider-runtime.js"),
typeof import("openclaw/plugin-sdk/provider-catalog-runtime"),
| "augmentModelCatalogWithProviderPlugins"
| "resetProviderRuntimeHookCacheForTest"
| "resolveProviderBuiltInModelSuppression"
@@ -22,7 +22,7 @@ export async function importProviderRuntimeCatalogModule(): Promise<ProviderRunt
augmentModelCatalogWithProviderPlugins,
resetProviderRuntimeHookCacheForTest,
resolveProviderBuiltInModelSuppression,
} = await import("../../../src/plugins/provider-runtime.js");
} = await import("openclaw/plugin-sdk/provider-catalog-runtime");
return {
augmentModelCatalogWithProviderPlugins,
resetProviderRuntimeHookCacheForTest,