mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 06:20:43 +00:00
refactor(test): remove legacy extension test seams
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { makeCfg } from "./reply.triggers.trigger-handling.test-harness.js";
|
||||
import { makeCfg } from "../../test/helpers/auto-reply/trigger-handling-test-harness.js";
|
||||
import { buildGroupChatContext, buildGroupIntro } from "./reply/groups.js";
|
||||
|
||||
type GetReplyFromConfig = typeof import("./reply.js").getReplyFromConfig;
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
makeCfg,
|
||||
requireSessionStorePath,
|
||||
withTempHome,
|
||||
} from "./reply.triggers.trigger-handling.test-harness.js";
|
||||
} from "../../test/helpers/auto-reply/trigger-handling-test-harness.js";
|
||||
|
||||
type GetReplyFromConfig = typeof import("./reply.js").getReplyFromConfig;
|
||||
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
import fs from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { loadSessionStore, resolveSessionKey } from "../config/sessions.js";
|
||||
import { registerGroupIntroPromptCases } from "./reply.triggers.group-intro-prompts.cases.js";
|
||||
import { registerTriggerHandlingUsageSummaryCases } from "./reply.triggers.trigger-handling.filters-usage-summary-current-model-provider.cases.js";
|
||||
import {
|
||||
expectInlineCommandHandledAndStripped,
|
||||
getAbortEmbeddedPiRunMock,
|
||||
@@ -16,7 +13,10 @@ import {
|
||||
requireSessionStorePath,
|
||||
runGreetingPromptForBareNewOrReset,
|
||||
withTempHome,
|
||||
} from "./reply.triggers.trigger-handling.test-harness.js";
|
||||
} from "../../test/helpers/auto-reply/trigger-handling-test-harness.js";
|
||||
import { loadSessionStore, resolveSessionKey } from "../config/sessions.js";
|
||||
import { registerGroupIntroPromptCases } from "./reply.triggers.group-intro-prompts.cases.js";
|
||||
import { registerTriggerHandlingUsageSummaryCases } from "./reply.triggers.trigger-handling.filters-usage-summary-current-model-provider.cases.js";
|
||||
import { withFullRuntimeReplyConfig } from "./reply/get-reply-fast-path.js";
|
||||
import { enqueueFollowupRun, getFollowupQueueDepth, type FollowupRun } from "./reply/queue.js";
|
||||
import { HEARTBEAT_TOKEN } from "./tokens.js";
|
||||
|
||||
@@ -44,9 +44,6 @@ const GUARDED_CHANNEL_EXTENSIONS = new Set([
|
||||
"zalo",
|
||||
"zalouser",
|
||||
]);
|
||||
// Shared config validation intentionally consumes this curated Telegram contract.
|
||||
const ALLOWED_CORE_CHANNEL_SDK_SUBPATHS = new Set(["telegram-command-config"]);
|
||||
|
||||
function bundledPluginFile(pluginId: string, relativePath: string): string {
|
||||
const rootDir = bundledPluginRoots.get(pluginId);
|
||||
if (!rootDir) {
|
||||
@@ -509,9 +506,6 @@ function expectCoreSourceStaysOffPluginSpecificSdkFacades(file: string, imports:
|
||||
continue;
|
||||
}
|
||||
const targetSubpath = specifier.split("/plugin-sdk/")[1]?.replace(/\.[cm]?[jt]sx?$/u, "") ?? "";
|
||||
if (ALLOWED_CORE_CHANNEL_SDK_SUBPATHS.has(targetSubpath)) {
|
||||
continue;
|
||||
}
|
||||
const targetExtensionId =
|
||||
[...GUARDED_CHANNEL_EXTENSIONS].find(
|
||||
(extensionId) =>
|
||||
|
||||
@@ -52,11 +52,6 @@ export { installCommonResolveTargetErrorCases } from "../test-helpers/resolve-ta
|
||||
export { sanitizeTerminalText } from "../terminal/safe-text.js";
|
||||
export { withStateDirEnv } from "../test-helpers/state-dir-env.js";
|
||||
export { countLines, hasBalancedFences } from "../test-utils/chunk-test-helpers.js";
|
||||
export {
|
||||
loadBundledPluginPublicSurfaceSync,
|
||||
loadBundledPluginTestApiSync,
|
||||
resolveRelativeBundledPluginPublicModuleId,
|
||||
} from "../test-utils/bundled-plugin-public-surface.js";
|
||||
export { expectGeneratedTokenPersistedToGatewayAuth } from "../test-utils/auth-token-assertions.js";
|
||||
export { captureEnv, withEnv, withEnvAsync } from "../test-utils/env.js";
|
||||
export { withFetchPreconnect, type FetchMock } from "../test-utils/fetch-mock.js";
|
||||
|
||||
@@ -8,33 +8,6 @@ const REPO_ROOT = resolve(SRC_ROOT, "..");
|
||||
const sourceCache = new Map<string, string>();
|
||||
const tsFilesCache = new Map<string, string[]>();
|
||||
|
||||
const ALLOWED_BUNDLED_CAPABILITY_METADATA_CONSUMERS = new Set([
|
||||
"src/media-generation/provider-capabilities.contract.test.ts",
|
||||
"src/plugins/bundled-capability-metadata.test.ts",
|
||||
"src/plugins/contracts/boundary-invariants.test.ts",
|
||||
]);
|
||||
|
||||
const ALLOWED_EXTENSION_PATH_STRING_TESTS = new Set([
|
||||
"src/plugin-sdk/browser-maintenance.test.ts",
|
||||
"src/channels/plugins/bundled.shape-guard.test.ts",
|
||||
"src/cli/capability-cli.test.ts",
|
||||
"src/commands/doctor-legacy-config.migrations.test.ts",
|
||||
"src/plugins/contracts/bundled-extension-config-api-guardrails.test.ts",
|
||||
"src/scripts/test-projects.test.ts",
|
||||
]);
|
||||
|
||||
const ALLOWED_CONTRACT_BUNDLED_PATH_HELPERS = new Set([
|
||||
"src/plugins/contracts/boundary-invariants.test.ts",
|
||||
"src/plugins/contracts/plugin-sdk-index.bundle.test.ts",
|
||||
"src/plugins/contracts/plugin-sdk-runtime-api-guardrails.test.ts",
|
||||
]);
|
||||
|
||||
const ALLOWED_CHANNEL_BUNDLED_METADATA_CONSUMERS = new Set([
|
||||
"src/channels/plugins/bundled.ts",
|
||||
"src/channels/plugins/contracts/runtime-artifacts.ts",
|
||||
"src/channels/plugins/session-conversation.bundled-fallback.test.ts",
|
||||
]);
|
||||
|
||||
type FileFilter = {
|
||||
excludeTests?: boolean;
|
||||
testOnly?: boolean;
|
||||
@@ -90,7 +63,11 @@ describe("plugin contract boundary invariants", () => {
|
||||
it("keeps bundled-capability-metadata confined to contract/test inventory", () => {
|
||||
const files = listTsFiles("src");
|
||||
const offenders = files.filter((file) => {
|
||||
if (ALLOWED_BUNDLED_CAPABILITY_METADATA_CONSUMERS.has(file)) {
|
||||
if (
|
||||
file === "src/plugins/contracts/boundary-invariants.test.ts" ||
|
||||
file.endsWith(".contract.test.ts") ||
|
||||
file.endsWith("-capability-metadata.test.ts")
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return readRepoSource(file).includes("contracts/inventory/bundled-capability-metadata");
|
||||
@@ -109,9 +86,6 @@ describe("plugin contract boundary invariants", () => {
|
||||
it("keeps core tests off bundled extension deep imports", () => {
|
||||
const files = listTsFiles("src", { testOnly: true });
|
||||
const offenders = files.filter((file) => {
|
||||
if (ALLOWED_EXTENSION_PATH_STRING_TESTS.has(file)) {
|
||||
return false;
|
||||
}
|
||||
const source = readRepoSource(file);
|
||||
return (
|
||||
/from\s+["'][^"']*extensions\/.+(?:api|runtime-api|test-api)\.js["']/u.test(source) ||
|
||||
@@ -125,7 +99,7 @@ describe("plugin contract boundary invariants", () => {
|
||||
it("keeps plugin contract tests off bundled path helpers unless the test is explicitly about paths", () => {
|
||||
const files = listTsFiles("src/plugins/contracts", { testOnly: true });
|
||||
const offenders = files.filter((file) => {
|
||||
if (ALLOWED_CONTRACT_BUNDLED_PATH_HELPERS.has(file)) {
|
||||
if (file === "src/plugins/contracts/boundary-invariants.test.ts") {
|
||||
return false;
|
||||
}
|
||||
return readRepoSource(file).includes("test/helpers/bundled-plugin-paths");
|
||||
@@ -136,9 +110,6 @@ describe("plugin contract boundary invariants", () => {
|
||||
it("keeps channel production code off bundled-plugin-metadata helpers", () => {
|
||||
const files = listTsFiles("src/channels", { excludeTests: true });
|
||||
const offenders = files.filter((file) => {
|
||||
if (ALLOWED_CHANNEL_BUNDLED_METADATA_CONSUMERS.has(file)) {
|
||||
return false;
|
||||
}
|
||||
return readRepoSource(file).includes("plugins/bundled-plugin-metadata");
|
||||
});
|
||||
expect(offenders).toEqual([]);
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
import { loadBundledPluginApiSync } from "../../test-utils/bundled-plugin-public-surface.js";
|
||||
import type { ProviderPlugin } from "../types.js";
|
||||
|
||||
export type ProviderContractEntry = {
|
||||
pluginId: string;
|
||||
provider: ProviderPlugin;
|
||||
};
|
||||
|
||||
let providerContractRegistryCache: ProviderContractEntry[] | null = null;
|
||||
|
||||
type ProviderApiSurface<TFactoryName extends string> = Record<TFactoryName, () => ProviderPlugin>;
|
||||
type AnthropicApiSurface = ProviderApiSurface<"buildAnthropicProvider">;
|
||||
type GoogleApiSurface = ProviderApiSurface<"buildGoogleProvider" | "buildGoogleGeminiCliProvider">;
|
||||
type OpenAIApiSurface = ProviderApiSurface<
|
||||
"buildOpenAIProvider" | "buildOpenAICodexProviderPlugin"
|
||||
>;
|
||||
|
||||
export function loadVitestProviderContractRegistry(): ProviderContractEntry[] {
|
||||
const anthropicApi = loadBundledPluginApiSync<AnthropicApiSurface>("anthropic");
|
||||
const googleApi = loadBundledPluginApiSync<GoogleApiSurface>("google");
|
||||
const openAIApi = loadBundledPluginApiSync<OpenAIApiSurface>("openai");
|
||||
providerContractRegistryCache ??= [
|
||||
{ pluginId: "anthropic", provider: anthropicApi.buildAnthropicProvider() },
|
||||
{ pluginId: "google", provider: googleApi.buildGoogleProvider() },
|
||||
{ pluginId: "google", provider: googleApi.buildGoogleGeminiCliProvider() },
|
||||
{ pluginId: "openai", provider: openAIApi.buildOpenAIProvider() },
|
||||
{ pluginId: "openai", provider: openAIApi.buildOpenAICodexProviderPlugin() },
|
||||
];
|
||||
return providerContractRegistryCache;
|
||||
}
|
||||
@@ -46,32 +46,32 @@ describe("plugin contract registry scoped retries", () => {
|
||||
.mockReturnValueOnce(
|
||||
createMockRuntimeRegistry({
|
||||
plugin: {
|
||||
id: "xai",
|
||||
id: "arcee",
|
||||
status: "error",
|
||||
error: "transient xai load failure",
|
||||
error: "transient arcee load failure",
|
||||
providerIds: [],
|
||||
webFetchProviderIds: [],
|
||||
webSearchProviderIds: [],
|
||||
},
|
||||
diagnostics: [{ pluginId: "xai", message: "transient xai load failure" }],
|
||||
diagnostics: [{ pluginId: "arcee", message: "transient arcee load failure" }],
|
||||
}),
|
||||
)
|
||||
.mockReturnValueOnce(
|
||||
createMockRuntimeRegistry({
|
||||
plugin: {
|
||||
id: "xai",
|
||||
id: "arcee",
|
||||
status: "loaded",
|
||||
providerIds: ["xai"],
|
||||
providerIds: ["arcee"],
|
||||
webFetchProviderIds: [],
|
||||
webSearchProviderIds: ["grok"],
|
||||
webSearchProviderIds: [],
|
||||
},
|
||||
providers: [
|
||||
{
|
||||
pluginId: "xai",
|
||||
pluginId: "arcee",
|
||||
provider: {
|
||||
id: "xai",
|
||||
label: "xAI",
|
||||
docsPath: "/providers/xai",
|
||||
id: "arcee",
|
||||
label: "Arcee",
|
||||
docsPath: "/providers/arcee",
|
||||
auth: [],
|
||||
} as ProviderPlugin,
|
||||
},
|
||||
@@ -82,12 +82,15 @@ describe("plugin contract registry scoped retries", () => {
|
||||
vi.doMock("../bundled-capability-runtime.js", () => ({
|
||||
loadBundledCapabilityRuntimeRegistry,
|
||||
}));
|
||||
vi.doMock("../provider-contract-public-artifacts.js", () => ({
|
||||
resolveBundledExplicitProviderContractsFromPublicArtifacts: () => null,
|
||||
}));
|
||||
|
||||
const { resolveProviderContractProvidersForPluginIds } = await import("./registry.js");
|
||||
|
||||
expect(
|
||||
resolveProviderContractProvidersForPluginIds(["xai"]).map((provider) => provider.id),
|
||||
).toEqual(["xai"]);
|
||||
resolveProviderContractProvidersForPluginIds(["arcee"]).map((provider) => provider.id),
|
||||
).toEqual(["arcee"]);
|
||||
expect(loadBundledCapabilityRuntimeRegistry).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
@@ -97,36 +100,36 @@ describe("plugin contract registry scoped retries", () => {
|
||||
.mockReturnValueOnce(
|
||||
createMockRuntimeRegistry({
|
||||
plugin: {
|
||||
id: "xai",
|
||||
id: "searxng",
|
||||
status: "error",
|
||||
error: "transient grok load failure",
|
||||
error: "transient searxng load failure",
|
||||
providerIds: [],
|
||||
webFetchProviderIds: [],
|
||||
webSearchProviderIds: [],
|
||||
},
|
||||
diagnostics: [{ pluginId: "xai", message: "transient grok load failure" }],
|
||||
diagnostics: [{ pluginId: "searxng", message: "transient searxng load failure" }],
|
||||
}),
|
||||
)
|
||||
.mockReturnValueOnce(
|
||||
createMockRuntimeRegistry({
|
||||
plugin: {
|
||||
id: "xai",
|
||||
id: "searxng",
|
||||
status: "loaded",
|
||||
providerIds: ["xai"],
|
||||
providerIds: [],
|
||||
webFetchProviderIds: [],
|
||||
webSearchProviderIds: ["grok"],
|
||||
webSearchProviderIds: ["searxng"],
|
||||
},
|
||||
webSearchProviders: [
|
||||
{
|
||||
pluginId: "xai",
|
||||
pluginId: "searxng",
|
||||
provider: {
|
||||
id: "grok",
|
||||
label: "Grok Search",
|
||||
hint: "Search the web with Grok",
|
||||
envVars: ["XAI_API_KEY"],
|
||||
placeholder: "XAI_API_KEY",
|
||||
signupUrl: "https://x.ai",
|
||||
credentialPath: "plugins.entries.xai.config.webSearch.apiKey",
|
||||
id: "searxng",
|
||||
label: "SearXNG",
|
||||
hint: "Search the web with SearXNG",
|
||||
envVars: ["SEARXNG_URL"],
|
||||
placeholder: "https://search.example.test",
|
||||
signupUrl: "https://docs.searxng.org",
|
||||
credentialPath: "plugins.entries.searxng.config.webSearch.url",
|
||||
requiresCredential: true,
|
||||
getCredentialValue: () => undefined,
|
||||
setCredentialValue() {},
|
||||
@@ -144,12 +147,17 @@ describe("plugin contract registry scoped retries", () => {
|
||||
vi.doMock("../bundled-capability-runtime.js", () => ({
|
||||
loadBundledCapabilityRuntimeRegistry,
|
||||
}));
|
||||
vi.doMock("../web-provider-public-artifacts.explicit.js", () => ({
|
||||
resolveBundledExplicitWebSearchProvidersFromPublicArtifacts: () => null,
|
||||
}));
|
||||
|
||||
const { resolveWebSearchProviderContractEntriesForPluginId } = await import("./registry.js");
|
||||
|
||||
expect(
|
||||
resolveWebSearchProviderContractEntriesForPluginId("xai").map((entry) => entry.provider.id),
|
||||
).toEqual(["grok"]);
|
||||
resolveWebSearchProviderContractEntriesForPluginId("searxng").map(
|
||||
(entry) => entry.provider.id,
|
||||
),
|
||||
).toEqual(["searxng"]);
|
||||
expect(loadBundledCapabilityRuntimeRegistry).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
@@ -180,6 +188,9 @@ describe("plugin contract registry scoped retries", () => {
|
||||
vi.doMock("../bundled-capability-runtime.js", () => ({
|
||||
loadBundledCapabilityRuntimeRegistry,
|
||||
}));
|
||||
vi.doMock("../provider-contract-public-artifacts.js", () => ({
|
||||
resolveBundledExplicitProviderContractsFromPublicArtifacts: () => null,
|
||||
}));
|
||||
|
||||
const { requireProviderContractProvider } = await import("./registry.js");
|
||||
|
||||
@@ -189,9 +200,9 @@ describe("plugin contract registry scoped retries", () => {
|
||||
|
||||
it("uses provider public artifacts before falling back to the bundled runtime registry", async () => {
|
||||
const loadBundledCapabilityRuntimeRegistry = vi.fn(() => {
|
||||
throw new Error("provider contract vitest fast path should not hit bundled runtime registry");
|
||||
throw new Error("provider contract public artifact should not hit bundled runtime registry");
|
||||
});
|
||||
const loadVitestProviderContractRegistry = vi.fn(() => [
|
||||
const resolveBundledExplicitProviderContractsFromPublicArtifacts = vi.fn(() => [
|
||||
{
|
||||
pluginId: "openai",
|
||||
provider: {
|
||||
@@ -229,8 +240,8 @@ describe("plugin contract registry scoped retries", () => {
|
||||
vi.doMock("../bundled-capability-runtime.js", () => ({
|
||||
loadBundledCapabilityRuntimeRegistry,
|
||||
}));
|
||||
vi.doMock("./provider-vitest-registry.js", () => ({
|
||||
loadVitestProviderContractRegistry,
|
||||
vi.doMock("../provider-contract-public-artifacts.js", () => ({
|
||||
resolveBundledExplicitProviderContractsFromPublicArtifacts,
|
||||
}));
|
||||
|
||||
const { resolveProviderContractProvidersForPluginIds } = await import("./registry.js");
|
||||
@@ -238,24 +249,34 @@ describe("plugin contract registry scoped retries", () => {
|
||||
expect(
|
||||
resolveProviderContractProvidersForPluginIds(["openai"]).map((provider) => provider.id),
|
||||
).toEqual(["openai", "openai-codex"]);
|
||||
expect(loadVitestProviderContractRegistry).toHaveBeenCalledTimes(1);
|
||||
expect(resolveBundledExplicitProviderContractsFromPublicArtifacts).toHaveBeenCalledTimes(1);
|
||||
expect(loadBundledCapabilityRuntimeRegistry).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("uses web search public artifacts before falling back to the bundled runtime registry", async () => {
|
||||
const loadBundledCapabilityRuntimeRegistry = vi.fn(() => {
|
||||
throw new Error(
|
||||
"web search contract vitest fast path should not hit bundled runtime registry",
|
||||
"web search contract public artifact should not hit bundled runtime registry",
|
||||
);
|
||||
});
|
||||
const loadVitestWebSearchProviderContractRegistry = vi.fn(() => [
|
||||
const resolveBundledExplicitWebSearchProvidersFromPublicArtifacts = vi.fn(() => [
|
||||
{
|
||||
pluginId: "google",
|
||||
provider: {
|
||||
id: "gemini",
|
||||
label: "Gemini",
|
||||
credentialPath: "plugins.entries.google.config.webSearch.apiKey",
|
||||
} as WebSearchProviderPlugin,
|
||||
id: "gemini",
|
||||
label: "Gemini",
|
||||
hint: "Search with Gemini",
|
||||
envVars: ["GEMINI_API_KEY"],
|
||||
placeholder: "GEMINI_API_KEY",
|
||||
signupUrl: "https://aistudio.google.com",
|
||||
credentialPath: "plugins.entries.google.config.webSearch.apiKey",
|
||||
requiresCredential: true,
|
||||
getCredentialValue: () => undefined,
|
||||
setCredentialValue() {},
|
||||
createTool: () => ({
|
||||
description: "search",
|
||||
parameters: {},
|
||||
execute: async () => ({}),
|
||||
}),
|
||||
credentialValue: "AIzaSyDUMMY",
|
||||
},
|
||||
]);
|
||||
@@ -263,8 +284,8 @@ describe("plugin contract registry scoped retries", () => {
|
||||
vi.doMock("../bundled-capability-runtime.js", () => ({
|
||||
loadBundledCapabilityRuntimeRegistry,
|
||||
}));
|
||||
vi.doMock("./web-provider-vitest-registry.js", () => ({
|
||||
loadVitestWebSearchProviderContractRegistry,
|
||||
vi.doMock("../web-provider-public-artifacts.explicit.js", () => ({
|
||||
resolveBundledExplicitWebSearchProvidersFromPublicArtifacts,
|
||||
}));
|
||||
|
||||
const { resolveWebSearchProviderContractEntriesForPluginId } = await import("./registry.js");
|
||||
@@ -274,7 +295,7 @@ describe("plugin contract registry scoped retries", () => {
|
||||
(entry) => entry.provider.id,
|
||||
),
|
||||
).toEqual(["gemini"]);
|
||||
expect(loadVitestWebSearchProviderContractRegistry).toHaveBeenCalledTimes(1);
|
||||
expect(resolveBundledExplicitWebSearchProvidersFromPublicArtifacts).toHaveBeenCalledTimes(1);
|
||||
expect(loadBundledCapabilityRuntimeRegistry).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
loadPluginManifestRegistry,
|
||||
resolveManifestContractPluginIds,
|
||||
} from "../manifest-registry.js";
|
||||
import { resolveBundledExplicitProviderContractsFromPublicArtifacts } from "../provider-contract-public-artifacts.js";
|
||||
import type {
|
||||
ImageGenerationProviderPlugin,
|
||||
MediaUnderstandingProviderPlugin,
|
||||
@@ -16,8 +17,8 @@ import type {
|
||||
WebFetchProviderPlugin,
|
||||
WebSearchProviderPlugin,
|
||||
} from "../types.js";
|
||||
import { resolveBundledExplicitWebSearchProvidersFromPublicArtifacts } from "../web-provider-public-artifacts.explicit.js";
|
||||
import { BUNDLED_PLUGIN_CONTRACT_SNAPSHOTS } from "./inventory/bundled-capability-metadata.js";
|
||||
import { loadVitestProviderContractRegistry } from "./provider-vitest-registry.js";
|
||||
import { uniqueStrings } from "./shared.js";
|
||||
import {
|
||||
loadVitestImageGenerationProviderContractRegistry,
|
||||
@@ -28,7 +29,6 @@ import {
|
||||
loadVitestSpeechProviderContractRegistry,
|
||||
loadVitestVideoGenerationProviderContractRegistry,
|
||||
} from "./speech-vitest-registry.js";
|
||||
import { loadVitestWebSearchProviderContractRegistry } from "./web-provider-vitest-registry.js";
|
||||
|
||||
type BundledCapabilityRuntimeRegistry = ReturnType<typeof loadBundledCapabilityRuntimeRegistry>;
|
||||
type CapabilityContractEntry<T> = {
|
||||
@@ -316,14 +316,12 @@ function loadProviderContractEntriesForPluginId(pluginId: string): ProviderContr
|
||||
return cached;
|
||||
}
|
||||
|
||||
if (process.env.VITEST) {
|
||||
const vitestEntries = loadVitestProviderContractRegistry().filter(
|
||||
(entry) => entry.pluginId === pluginId,
|
||||
);
|
||||
if (vitestEntries.length > 0) {
|
||||
cache.set(pluginId, vitestEntries);
|
||||
return vitestEntries;
|
||||
}
|
||||
const publicArtifactEntries = resolveBundledExplicitProviderContractsFromPublicArtifacts({
|
||||
onlyPluginIds: [pluginId],
|
||||
});
|
||||
if (publicArtifactEntries) {
|
||||
cache.set(pluginId, publicArtifactEntries);
|
||||
return publicArtifactEntries;
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -356,8 +354,14 @@ function loadProviderContractRegistry(): ProviderContractEntry[] {
|
||||
if (!providerContractRegistryCache) {
|
||||
try {
|
||||
providerContractLoadError = undefined;
|
||||
const vitestEntries = process.env.VITEST ? loadVitestProviderContractRegistry() : [];
|
||||
const coveredPluginIds = new Set(vitestEntries.map((entry) => entry.pluginId));
|
||||
const pluginIds = resolveBundledProviderContractPluginIds();
|
||||
const publicArtifactEntries = pluginIds.flatMap(
|
||||
(pluginId) =>
|
||||
resolveBundledExplicitProviderContractsFromPublicArtifacts({
|
||||
onlyPluginIds: [pluginId],
|
||||
}) ?? [],
|
||||
);
|
||||
const coveredPluginIds = new Set(publicArtifactEntries.map((entry) => entry.pluginId));
|
||||
const remainingPluginIds = resolveBundledProviderContractPluginIds().filter(
|
||||
(pluginId) => !coveredPluginIds.has(pluginId),
|
||||
);
|
||||
@@ -371,7 +375,7 @@ function loadProviderContractRegistry(): ProviderContractEntry[] {
|
||||
provider: entry.provider,
|
||||
}))
|
||||
: [];
|
||||
providerContractRegistryCache = [...vitestEntries, ...runtimeEntries];
|
||||
providerContractRegistryCache = [...publicArtifactEntries, ...runtimeEntries];
|
||||
} catch (error) {
|
||||
providerContractLoadError = error instanceof Error ? error : new Error(String(error));
|
||||
providerContractRegistryCache = [];
|
||||
@@ -475,8 +479,19 @@ export function resolveWebFetchProviderContractEntriesForPluginId(
|
||||
|
||||
function loadWebSearchProviderContractRegistry(): WebSearchProviderContractEntry[] {
|
||||
if (!webSearchProviderContractRegistryCache) {
|
||||
const vitestEntries = process.env.VITEST ? loadVitestWebSearchProviderContractRegistry() : [];
|
||||
const coveredPluginIds = new Set(vitestEntries.map((entry) => entry.pluginId));
|
||||
const pluginIds = resolveBundledManifestContractPluginIds("webSearchProviders");
|
||||
const publicArtifactEntries = pluginIds.flatMap((pluginId) =>
|
||||
(
|
||||
resolveBundledExplicitWebSearchProvidersFromPublicArtifacts({
|
||||
onlyPluginIds: [pluginId],
|
||||
}) ?? []
|
||||
).map((provider) => ({
|
||||
pluginId: provider.pluginId,
|
||||
provider,
|
||||
credentialValue: resolveWebSearchCredentialValue(provider),
|
||||
})),
|
||||
);
|
||||
const coveredPluginIds = new Set(publicArtifactEntries.map((entry) => entry.pluginId));
|
||||
const remainingPluginIds = resolveBundledManifestContractPluginIds("webSearchProviders").filter(
|
||||
(pluginId) => !coveredPluginIds.has(pluginId),
|
||||
);
|
||||
@@ -491,7 +506,7 @@ function loadWebSearchProviderContractRegistry(): WebSearchProviderContractEntry
|
||||
credentialValue: resolveWebSearchCredentialValue(entry.provider),
|
||||
}))
|
||||
: [];
|
||||
webSearchProviderContractRegistryCache = [...vitestEntries, ...runtimeEntries];
|
||||
webSearchProviderContractRegistryCache = [...publicArtifactEntries, ...runtimeEntries];
|
||||
}
|
||||
return webSearchProviderContractRegistryCache;
|
||||
}
|
||||
@@ -512,14 +527,16 @@ export function resolveWebSearchProviderContractEntriesForPluginId(
|
||||
return cached;
|
||||
}
|
||||
|
||||
if (process.env.VITEST) {
|
||||
const vitestEntries = loadVitestWebSearchProviderContractRegistry().filter(
|
||||
(entry) => entry.pluginId === pluginId,
|
||||
);
|
||||
if (vitestEntries.length > 0) {
|
||||
cache.set(pluginId, vitestEntries);
|
||||
return vitestEntries;
|
||||
}
|
||||
const publicArtifactEntries = resolveBundledExplicitWebSearchProvidersFromPublicArtifacts({
|
||||
onlyPluginIds: [pluginId],
|
||||
})?.map((provider) => ({
|
||||
pluginId: provider.pluginId,
|
||||
provider,
|
||||
credentialValue: resolveWebSearchCredentialValue(provider),
|
||||
}));
|
||||
if (publicArtifactEntries) {
|
||||
cache.set(pluginId, publicArtifactEntries);
|
||||
return publicArtifactEntries;
|
||||
}
|
||||
|
||||
const entries = loadScopedCapabilityRuntimeRegistryEntries({
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
import { loadBundledPluginPublicSurfaceSync } from "../../test-utils/bundled-plugin-public-surface.js";
|
||||
import type { WebSearchProviderPlugin } from "../types.js";
|
||||
|
||||
export type WebSearchProviderContractEntry = {
|
||||
pluginId: string;
|
||||
provider: WebSearchProviderPlugin;
|
||||
credentialValue: unknown;
|
||||
};
|
||||
|
||||
let webSearchProviderContractRegistryCache: WebSearchProviderContractEntry[] | null = null;
|
||||
|
||||
type GoogleWebSearchContractApiSurface = {
|
||||
createGeminiWebSearchProvider: () => WebSearchProviderPlugin;
|
||||
};
|
||||
|
||||
export function loadVitestWebSearchProviderContractRegistry(): WebSearchProviderContractEntry[] {
|
||||
const googleWebSearchContractApi =
|
||||
loadBundledPluginPublicSurfaceSync<GoogleWebSearchContractApiSurface>({
|
||||
pluginId: "google",
|
||||
artifactBasename: "web-search-contract-api.js",
|
||||
});
|
||||
webSearchProviderContractRegistryCache ??= [
|
||||
{
|
||||
pluginId: "google",
|
||||
provider: googleWebSearchContractApi.createGeminiWebSearchProvider(),
|
||||
credentialValue: "AIzaSyDUMMY",
|
||||
},
|
||||
];
|
||||
return webSearchProviderContractRegistryCache;
|
||||
}
|
||||
81
src/plugins/provider-contract-public-artifacts.ts
Normal file
81
src/plugins/provider-contract-public-artifacts.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
import { loadBundledPluginPublicArtifactModuleSync } from "./public-surface-loader.js";
|
||||
import type { ProviderPlugin } from "./types.js";
|
||||
|
||||
type ProviderContractEntry = {
|
||||
pluginId: string;
|
||||
provider: ProviderPlugin;
|
||||
};
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null && !Array.isArray(value);
|
||||
}
|
||||
|
||||
function isProviderPlugin(value: unknown): value is ProviderPlugin {
|
||||
return (
|
||||
isRecord(value) &&
|
||||
typeof value.id === "string" &&
|
||||
typeof value.label === "string" &&
|
||||
Array.isArray(value.auth)
|
||||
);
|
||||
}
|
||||
|
||||
function tryLoadProviderContractApi(pluginId: string): Record<string, unknown> | null {
|
||||
try {
|
||||
return loadBundledPluginPublicArtifactModuleSync<Record<string, unknown>>({
|
||||
dirName: pluginId,
|
||||
artifactBasename: "provider-contract-api.js",
|
||||
});
|
||||
} catch (error) {
|
||||
if (
|
||||
error instanceof Error &&
|
||||
error.message.startsWith("Unable to resolve bundled plugin public surface ")
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
function collectProviderContractEntries(params: {
|
||||
pluginId: string;
|
||||
mod: Record<string, unknown>;
|
||||
}): ProviderContractEntry[] {
|
||||
const providers: ProviderContractEntry[] = [];
|
||||
for (const [name, exported] of Object.entries(params.mod).toSorted(([left], [right]) =>
|
||||
left.localeCompare(right),
|
||||
)) {
|
||||
if (
|
||||
typeof exported !== "function" ||
|
||||
exported.length !== 0 ||
|
||||
!name.startsWith("create") ||
|
||||
!name.endsWith("Provider")
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
const candidate = exported();
|
||||
if (isProviderPlugin(candidate)) {
|
||||
providers.push({ pluginId: params.pluginId, provider: candidate });
|
||||
}
|
||||
}
|
||||
return providers;
|
||||
}
|
||||
|
||||
export function resolveBundledExplicitProviderContractsFromPublicArtifacts(params: {
|
||||
onlyPluginIds: readonly string[];
|
||||
}): ProviderContractEntry[] | null {
|
||||
const providers: ProviderContractEntry[] = [];
|
||||
for (const pluginId of [...new Set(params.onlyPluginIds)].toSorted((left, right) =>
|
||||
left.localeCompare(right),
|
||||
)) {
|
||||
const mod = tryLoadProviderContractApi(pluginId);
|
||||
if (!mod) {
|
||||
return null;
|
||||
}
|
||||
const entries = collectProviderContractEntries({ pluginId, mod });
|
||||
if (entries.length === 0) {
|
||||
return null;
|
||||
}
|
||||
providers.push(...entries);
|
||||
}
|
||||
return providers;
|
||||
}
|
||||
@@ -9,32 +9,6 @@ const ALLOWED_EXTENSION_PUBLIC_SURFACE_BASENAMES = new Set(
|
||||
GUARDED_EXTENSION_PUBLIC_SURFACE_BASENAMES,
|
||||
);
|
||||
|
||||
const allowedNonExtensionTests = new Set<string>([
|
||||
"src/agents/pi-embedded-runner-extraparams-moonshot.test.ts",
|
||||
"src/agents/pi-embedded-runner-extraparams.test.ts",
|
||||
"src/agents/pi-embedded-runner-extraparams-moonshot.test.ts",
|
||||
"src/channels/plugins/contracts/dm-policy.contract.test.ts",
|
||||
"src/channels/plugins/contracts/group-policy.contract.test.ts",
|
||||
"src/commands/channels.surfaces-signal-runtime-errors-channels-status-output.test.ts",
|
||||
"src/commands/onboard-channels.e2e.test.ts",
|
||||
"src/gateway/hooks.test.ts",
|
||||
"src/infra/outbound/deliver.test.ts",
|
||||
"src/plugins/interactive.test.ts",
|
||||
"src/plugins/contracts/discovery.contract.test.ts",
|
||||
"src/plugin-sdk/telegram-command-config.test.ts",
|
||||
"src/security/audit-channel-slack-command-findings.test.ts",
|
||||
"src/security/audit-feishu-doc-risk.test.ts",
|
||||
"src/secrets/runtime-channel-inactive-variants.test.ts",
|
||||
"src/secrets/runtime-discord-surface.test.ts",
|
||||
"src/secrets/runtime-inactive-telegram-surfaces.test.ts",
|
||||
"src/secrets/runtime-legacy-x-search.test.ts",
|
||||
"src/secrets/runtime-matrix-shadowing.test.ts",
|
||||
"src/secrets/runtime-matrix-top-level.test.ts",
|
||||
"src/secrets/runtime-nextcloud-talk-file-precedence.test.ts",
|
||||
"src/secrets/runtime-telegram-token-inheritance.test.ts",
|
||||
"src/secrets/runtime-zalo-token-activity.test.ts",
|
||||
]);
|
||||
|
||||
function walk(dir: string, entries: string[] = []): string[] {
|
||||
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
||||
const fullPath = path.join(dir, entry.name);
|
||||
@@ -136,7 +110,7 @@ describe("non-extension test boundaries", () => {
|
||||
if (imports.length === 0) {
|
||||
return null;
|
||||
}
|
||||
if (allowedNonExtensionTests.has(file) || isAllowedCoreContractSuite(file, imports)) {
|
||||
if (isAllowedCoreContractSuite(file, imports)) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
@@ -170,20 +144,12 @@ describe("non-extension test boundaries", () => {
|
||||
expect(imports).toEqual([]);
|
||||
});
|
||||
|
||||
it("keeps bundled plugin public-surface imports on an explicit core allowlist", () => {
|
||||
const allowed = new Set([
|
||||
"src/auto-reply/reply.triggers.trigger-handling.test-harness.ts",
|
||||
"src/agents/models-config.providers.ollama.test.ts",
|
||||
"src/commands/channel-test-registry.ts",
|
||||
"src/plugins/contracts/provider-vitest-registry.ts",
|
||||
"src/plugins/contracts/web-provider-vitest-registry.ts",
|
||||
"src/plugin-sdk/testing.ts",
|
||||
]);
|
||||
it("keeps bundled plugin public-surface imports out of core source", () => {
|
||||
const files = walkCode(path.join(repoRoot, "src"));
|
||||
|
||||
const offenders = files.filter((file) => {
|
||||
const source = fs.readFileSync(path.join(repoRoot, file), "utf8");
|
||||
return findBundledPluginPublicSurfaceImports(source).length > 0 && !allowed.has(file);
|
||||
return findBundledPluginPublicSurfaceImports(source).length > 0;
|
||||
});
|
||||
|
||||
expect(offenders).toEqual([]);
|
||||
|
||||
@@ -3,11 +3,11 @@ import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { afterAll, afterEach, beforeAll, expect, vi } from "vitest";
|
||||
import { clearRuntimeAuthProfileStoreSnapshots } from "../agents/auth-profiles.js";
|
||||
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
import { resetProviderRuntimeHookCacheForTest } from "../plugins/provider-runtime.js";
|
||||
import { resolveRelativeBundledPluginPublicModuleId } from "../test-utils/bundled-plugin-public-surface.js";
|
||||
import { withFastReplyConfig } from "./reply/get-reply-fast-path.js";
|
||||
import { clearRuntimeAuthProfileStoreSnapshots } from "../../../src/agents/auth-profiles.js";
|
||||
import { withFastReplyConfig } from "../../../src/auto-reply/reply/get-reply-fast-path.js";
|
||||
import type { OpenClawConfig } from "../../../src/config/types.openclaw.js";
|
||||
import { resetProviderRuntimeHookCacheForTest } from "../../../src/plugins/provider-runtime.js";
|
||||
import { resolveRelativeBundledPluginPublicModuleId } from "../../../src/test-utils/bundled-plugin-public-surface.js";
|
||||
|
||||
// Avoid exporting vitest mock types (TS2742 under pnpm + d.ts emit).
|
||||
type AnyMock = any;
|
||||
@@ -49,7 +49,7 @@ export function getQueueEmbeddedPiMessageMock(): AnyMock {
|
||||
}
|
||||
|
||||
const installPiEmbeddedMock = () =>
|
||||
vi.doMock("../agents/pi-embedded.js", () => ({
|
||||
vi.doMock("../../../src/agents/pi-embedded.js", () => ({
|
||||
abortEmbeddedPiRun: (...args: unknown[]) => piEmbeddedMocks.abortEmbeddedPiRun(...args),
|
||||
compactEmbeddedPiSession: (...args: unknown[]) =>
|
||||
piEmbeddedMocks.compactEmbeddedPiSession(...args),
|
||||
@@ -65,7 +65,7 @@ const installPiEmbeddedMock = () =>
|
||||
|
||||
installPiEmbeddedMock();
|
||||
|
||||
vi.doMock("../agents/pi-embedded-runner/runs.js", () => ({
|
||||
vi.doMock("../../../src/agents/pi-embedded-runner/runs.js", () => ({
|
||||
abortEmbeddedPiRun: (...args: unknown[]) => piEmbeddedMocks.abortEmbeddedPiRun(...args),
|
||||
}));
|
||||
|
||||
@@ -83,7 +83,7 @@ export function getProviderUsageMocks(): AnyMocks {
|
||||
return providerUsageMocks;
|
||||
}
|
||||
|
||||
vi.mock("../infra/provider-usage.js", () => providerUsageMocks);
|
||||
vi.mock("../../../src/infra/provider-usage.js", () => providerUsageMocks);
|
||||
|
||||
const modelCatalogMocks = getSharedMocks("openclaw.trigger-handling.model-catalog-mocks", () => ({
|
||||
loadModelCatalog: vi.fn().mockResolvedValue([
|
||||
@@ -112,15 +112,15 @@ export function getModelCatalogMocks(): AnyMocks {
|
||||
}
|
||||
|
||||
const installModelCatalogMock = () =>
|
||||
vi.doMock("../agents/model-catalog.js", () => modelCatalogMocks);
|
||||
vi.doMock("../../../src/agents/model-catalog.js", () => modelCatalogMocks);
|
||||
|
||||
installModelCatalogMock();
|
||||
|
||||
vi.doMock("../agents/model-catalog.runtime.js", () => ({
|
||||
vi.doMock("../../../src/agents/model-catalog.runtime.js", () => ({
|
||||
loadModelCatalog: (...args: unknown[]) => modelCatalogMocks.loadModelCatalog(...args),
|
||||
}));
|
||||
|
||||
vi.doMock("../plugins/provider-runtime.runtime.js", () => ({
|
||||
vi.doMock("../../../src/plugins/provider-runtime.runtime.js", () => ({
|
||||
augmentModelCatalogWithProviderPlugins: async (params: { catalog?: unknown[] }) =>
|
||||
params.catalog ?? [],
|
||||
buildProviderAuthDoctorHintWithPlugin: () => undefined,
|
||||
@@ -150,11 +150,11 @@ export function getModelFallbackMocks(): AnyMocks {
|
||||
}
|
||||
|
||||
const installModelFallbackMock = () =>
|
||||
vi.doMock("../agents/model-fallback.js", () => modelFallbackMocks);
|
||||
vi.doMock("../../../src/agents/model-fallback.js", () => modelFallbackMocks);
|
||||
|
||||
installModelFallbackMock();
|
||||
|
||||
vi.doMock("../infra/git-commit.js", () => ({
|
||||
vi.doMock("../../../src/infra/git-commit.js", () => ({
|
||||
resolveCommitHash: vi.fn(() => "abcdef0"),
|
||||
}));
|
||||
|
||||
@@ -302,12 +302,12 @@ export function makeCfg(home: string): OpenClawConfig {
|
||||
}
|
||||
|
||||
export async function loadGetReplyFromConfig() {
|
||||
return (await import("./reply.js")).getReplyFromConfig;
|
||||
return (await import("../../../src/auto-reply/reply.js")).getReplyFromConfig;
|
||||
}
|
||||
|
||||
export function installTriggerHandlingReplyHarness(
|
||||
setGetReplyFromConfig: (
|
||||
getReplyFromConfig: typeof import("./reply.js").getReplyFromConfig,
|
||||
getReplyFromConfig: typeof import("../../../src/auto-reply/reply.js").getReplyFromConfig,
|
||||
) => void,
|
||||
): void {
|
||||
beforeAll(async () => {
|
||||
@@ -357,7 +357,7 @@ export function makeWhatsAppElevatedCfg(
|
||||
|
||||
export async function runDirectElevatedToggleAndLoadStore(params: {
|
||||
cfg: OpenClawConfig;
|
||||
getReplyFromConfig: typeof import("./reply.js").getReplyFromConfig;
|
||||
getReplyFromConfig: typeof import("../../../src/auto-reply/reply.js").getReplyFromConfig;
|
||||
body?: string;
|
||||
}): Promise<{
|
||||
text: string | undefined;
|
||||
@@ -386,7 +386,7 @@ export async function runDirectElevatedToggleAndLoadStore(params: {
|
||||
|
||||
export async function expectInlineCommandHandledAndStripped(params: {
|
||||
home: string;
|
||||
getReplyFromConfig: typeof import("./reply.js").getReplyFromConfig;
|
||||
getReplyFromConfig: typeof import("../../../src/auto-reply/reply.js").getReplyFromConfig;
|
||||
body: string;
|
||||
stripToken: string;
|
||||
blockReplyContains: string;
|
||||
@@ -419,7 +419,7 @@ export async function expectInlineCommandHandledAndStripped(params: {
|
||||
export async function runGreetingPromptForBareNewOrReset(params: {
|
||||
home: string;
|
||||
body: "/new" | "/reset";
|
||||
getReplyFromConfig: typeof import("./reply.js").getReplyFromConfig;
|
||||
getReplyFromConfig: typeof import("../../../src/auto-reply/reply.js").getReplyFromConfig;
|
||||
}) {
|
||||
const runEmbeddedPiAgentMock = getRunEmbeddedPiAgentMock();
|
||||
runEmbeddedPiAgentMock.mockClear();
|
||||
@@ -4,7 +4,7 @@ import {
|
||||
providerContractLoadError,
|
||||
resolveProviderContractProvidersForPluginIds,
|
||||
} from "../../../src/plugins/contracts/registry.js";
|
||||
import { loadBundledPluginPublicArtifactModuleSync } from "../../../src/plugins/public-surface-loader.js";
|
||||
import { resolveBundledExplicitProviderContractsFromPublicArtifacts } from "../../../src/plugins/provider-contract-public-artifacts.js";
|
||||
import type { ProviderPlugin } from "../../../src/plugins/types.js";
|
||||
import { installProviderPluginContractSuite } from "./provider-contract-suites.js";
|
||||
|
||||
@@ -13,56 +13,10 @@ type ProviderContractEntry = {
|
||||
provider: ProviderPlugin;
|
||||
};
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null && !Array.isArray(value);
|
||||
}
|
||||
|
||||
function isProviderPlugin(value: unknown): value is ProviderPlugin {
|
||||
return (
|
||||
isRecord(value) &&
|
||||
typeof value.id === "string" &&
|
||||
typeof value.label === "string" &&
|
||||
Array.isArray(value.auth)
|
||||
);
|
||||
}
|
||||
|
||||
function resolveProviderContractProvidersFromPublicArtifact(
|
||||
pluginId: string,
|
||||
): ProviderContractEntry[] | null {
|
||||
let mod: Record<string, unknown>;
|
||||
try {
|
||||
mod = loadBundledPluginPublicArtifactModuleSync<Record<string, unknown>>({
|
||||
dirName: pluginId,
|
||||
artifactBasename: "provider-contract-api.js",
|
||||
});
|
||||
} catch (error) {
|
||||
if (
|
||||
error instanceof Error &&
|
||||
error.message.startsWith("Unable to resolve bundled plugin public surface ")
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
const providers: ProviderContractEntry[] = [];
|
||||
for (const [name, exported] of Object.entries(mod).toSorted(([left], [right]) =>
|
||||
left.localeCompare(right),
|
||||
)) {
|
||||
if (
|
||||
typeof exported !== "function" ||
|
||||
exported.length !== 0 ||
|
||||
!name.startsWith("create") ||
|
||||
!name.endsWith("Provider")
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
const provider = exported();
|
||||
if (isProviderPlugin(provider)) {
|
||||
providers.push({ pluginId, provider });
|
||||
}
|
||||
}
|
||||
return providers.length > 0 ? providers : null;
|
||||
return resolveBundledExplicitProviderContractsFromPublicArtifacts({ onlyPluginIds: [pluginId] });
|
||||
}
|
||||
|
||||
export function describeProviderContracts(pluginId: string) {
|
||||
|
||||
Reference in New Issue
Block a user