diff --git a/src/auto-reply/reply.triggers.group-intro-prompts.cases.ts b/src/auto-reply/reply.triggers.group-intro-prompts.cases.ts index 860c023db1e..2ef22af5a40 100644 --- a/src/auto-reply/reply.triggers.group-intro-prompts.cases.ts +++ b/src/auto-reply/reply.triggers.group-intro-prompts.cases.ts @@ -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; diff --git a/src/auto-reply/reply.triggers.trigger-handling.filters-usage-summary-current-model-provider.cases.ts b/src/auto-reply/reply.triggers.trigger-handling.filters-usage-summary-current-model-provider.cases.ts index d0f6d3edb1f..13a1087fcc9 100644 --- a/src/auto-reply/reply.triggers.trigger-handling.filters-usage-summary-current-model-provider.cases.ts +++ b/src/auto-reply/reply.triggers.trigger-handling.filters-usage-summary-current-model-provider.cases.ts @@ -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; diff --git a/src/auto-reply/reply.triggers.trigger-handling.targets-active-session-native-stop.e2e.test.ts b/src/auto-reply/reply.triggers.trigger-handling.targets-active-session-native-stop.e2e.test.ts index 57dbaec15ef..7ae187d02d4 100644 --- a/src/auto-reply/reply.triggers.trigger-handling.targets-active-session-native-stop.e2e.test.ts +++ b/src/auto-reply/reply.triggers.trigger-handling.targets-active-session-native-stop.e2e.test.ts @@ -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"; diff --git a/src/channels/plugins/contracts/channel-import-guardrails.test.ts b/src/channels/plugins/contracts/channel-import-guardrails.test.ts index 3bac99b3b6b..07a6d8a8a5f 100644 --- a/src/channels/plugins/contracts/channel-import-guardrails.test.ts +++ b/src/channels/plugins/contracts/channel-import-guardrails.test.ts @@ -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) => diff --git a/src/plugin-sdk/testing.ts b/src/plugin-sdk/testing.ts index b8d677fb277..f2fc5290e92 100644 --- a/src/plugin-sdk/testing.ts +++ b/src/plugin-sdk/testing.ts @@ -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"; diff --git a/src/plugins/contracts/boundary-invariants.test.ts b/src/plugins/contracts/boundary-invariants.test.ts index 7508e234ddd..1f23bbe2e93 100644 --- a/src/plugins/contracts/boundary-invariants.test.ts +++ b/src/plugins/contracts/boundary-invariants.test.ts @@ -8,33 +8,6 @@ const REPO_ROOT = resolve(SRC_ROOT, ".."); const sourceCache = new Map(); const tsFilesCache = new Map(); -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([]); diff --git a/src/plugins/contracts/provider-vitest-registry.ts b/src/plugins/contracts/provider-vitest-registry.ts deleted file mode 100644 index 061a786be10..00000000000 --- a/src/plugins/contracts/provider-vitest-registry.ts +++ /dev/null @@ -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 = Record ProviderPlugin>; -type AnthropicApiSurface = ProviderApiSurface<"buildAnthropicProvider">; -type GoogleApiSurface = ProviderApiSurface<"buildGoogleProvider" | "buildGoogleGeminiCliProvider">; -type OpenAIApiSurface = ProviderApiSurface< - "buildOpenAIProvider" | "buildOpenAICodexProviderPlugin" ->; - -export function loadVitestProviderContractRegistry(): ProviderContractEntry[] { - const anthropicApi = loadBundledPluginApiSync("anthropic"); - const googleApi = loadBundledPluginApiSync("google"); - const openAIApi = loadBundledPluginApiSync("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; -} diff --git a/src/plugins/contracts/registry.retry.test.ts b/src/plugins/contracts/registry.retry.test.ts index 3421aedda94..4a3244ad635 100644 --- a/src/plugins/contracts/registry.retry.test.ts +++ b/src/plugins/contracts/registry.retry.test.ts @@ -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(); }); diff --git a/src/plugins/contracts/registry.ts b/src/plugins/contracts/registry.ts index 3a82a78beb3..f6ea0be6ee8 100644 --- a/src/plugins/contracts/registry.ts +++ b/src/plugins/contracts/registry.ts @@ -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; type CapabilityContractEntry = { @@ -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({ diff --git a/src/plugins/contracts/web-provider-vitest-registry.ts b/src/plugins/contracts/web-provider-vitest-registry.ts deleted file mode 100644 index 416513ba520..00000000000 --- a/src/plugins/contracts/web-provider-vitest-registry.ts +++ /dev/null @@ -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({ - pluginId: "google", - artifactBasename: "web-search-contract-api.js", - }); - webSearchProviderContractRegistryCache ??= [ - { - pluginId: "google", - provider: googleWebSearchContractApi.createGeminiWebSearchProvider(), - credentialValue: "AIzaSyDUMMY", - }, - ]; - return webSearchProviderContractRegistryCache; -} diff --git a/src/plugins/provider-contract-public-artifacts.ts b/src/plugins/provider-contract-public-artifacts.ts new file mode 100644 index 00000000000..bcd9cf1dcbe --- /dev/null +++ b/src/plugins/provider-contract-public-artifacts.ts @@ -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 { + 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 | null { + try { + return loadBundledPluginPublicArtifactModuleSync>({ + 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; +}): 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; +} diff --git a/test/extension-test-boundary.test.ts b/test/extension-test-boundary.test.ts index bd8647b1d1c..69ce50079da 100644 --- a/test/extension-test-boundary.test.ts +++ b/test/extension-test-boundary.test.ts @@ -9,32 +9,6 @@ const ALLOWED_EXTENSION_PUBLIC_SURFACE_BASENAMES = new Set( GUARDED_EXTENSION_PUBLIC_SURFACE_BASENAMES, ); -const allowedNonExtensionTests = new Set([ - "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([]); diff --git a/src/auto-reply/reply.triggers.trigger-handling.test-harness.ts b/test/helpers/auto-reply/trigger-handling-test-harness.ts similarity index 90% rename from src/auto-reply/reply.triggers.trigger-handling.test-harness.ts rename to test/helpers/auto-reply/trigger-handling-test-harness.ts index a688c57f1e0..fdac3c99da1 100644 --- a/src/auto-reply/reply.triggers.trigger-handling.test-harness.ts +++ b/test/helpers/auto-reply/trigger-handling-test-harness.ts @@ -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(); diff --git a/test/helpers/plugins/provider-contract.ts b/test/helpers/plugins/provider-contract.ts index a907bf422bf..846f7097232 100644 --- a/test/helpers/plugins/provider-contract.ts +++ b/test/helpers/plugins/provider-contract.ts @@ -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 { - 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; - try { - mod = loadBundledPluginPublicArtifactModuleSync>({ - 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) {