diff --git a/extensions/whatsapp/contract-api.ts b/extensions/whatsapp/contract-api.ts index 1e0dc98c8c1..7ba0e12d044 100644 --- a/extensions/whatsapp/contract-api.ts +++ b/extensions/whatsapp/contract-api.ts @@ -5,6 +5,10 @@ import { isWhatsAppGroupJid as isWhatsAppGroupJidImpl, normalizeWhatsAppTarget as normalizeWhatsAppTargetImpl, } from "./src/normalize-target.js"; +export { + listWhatsAppDirectoryGroupsFromConfig, + listWhatsAppDirectoryPeersFromConfig, +} from "./src/directory-config.js"; import { resolveWhatsAppRuntimeGroupPolicy as resolveWhatsAppRuntimeGroupPolicyImpl } from "./src/runtime-group-policy.js"; import { canonicalizeLegacySessionKey as canonicalizeLegacySessionKeyImpl, diff --git a/src/plugins/contracts/bundled-web-search.brave.contract.test.ts b/src/plugins/contracts/bundled-web-search.brave.contract.test.ts deleted file mode 100644 index 7ae67d2c669..00000000000 --- a/src/plugins/contracts/bundled-web-search.brave.contract.test.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { describeBundledWebSearchFastPathContract } from "../../../test/helpers/plugins/bundled-web-search-fast-path-contract.js"; - -describeBundledWebSearchFastPathContract("brave"); diff --git a/src/plugins/contracts/bundled-web-search.duckduckgo.contract.test.ts b/src/plugins/contracts/bundled-web-search.duckduckgo.contract.test.ts deleted file mode 100644 index a8b12f8058f..00000000000 --- a/src/plugins/contracts/bundled-web-search.duckduckgo.contract.test.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { describeBundledWebSearchFastPathContract } from "../../../test/helpers/plugins/bundled-web-search-fast-path-contract.js"; - -describeBundledWebSearchFastPathContract("duckduckgo"); diff --git a/src/plugins/contracts/bundled-web-search.exa.contract.test.ts b/src/plugins/contracts/bundled-web-search.exa.contract.test.ts deleted file mode 100644 index 59744936d96..00000000000 --- a/src/plugins/contracts/bundled-web-search.exa.contract.test.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { describeBundledWebSearchFastPathContract } from "../../../test/helpers/plugins/bundled-web-search-fast-path-contract.js"; - -describeBundledWebSearchFastPathContract("exa"); diff --git a/src/plugins/contracts/bundled-web-search.firecrawl.contract.test.ts b/src/plugins/contracts/bundled-web-search.firecrawl.contract.test.ts deleted file mode 100644 index 514c469a768..00000000000 --- a/src/plugins/contracts/bundled-web-search.firecrawl.contract.test.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { describeBundledWebSearchFastPathContract } from "../../../test/helpers/plugins/bundled-web-search-fast-path-contract.js"; - -describeBundledWebSearchFastPathContract("firecrawl"); diff --git a/src/plugins/contracts/bundled-web-search.google.contract.test.ts b/src/plugins/contracts/bundled-web-search.google.contract.test.ts deleted file mode 100644 index d2b4e0fd2a2..00000000000 --- a/src/plugins/contracts/bundled-web-search.google.contract.test.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { describeBundledWebSearchFastPathContract } from "../../../test/helpers/plugins/bundled-web-search-fast-path-contract.js"; - -describeBundledWebSearchFastPathContract("google"); diff --git a/src/plugins/contracts/bundled-web-search.minimax.contract.test.ts b/src/plugins/contracts/bundled-web-search.minimax.contract.test.ts deleted file mode 100644 index f4b5fcd81a2..00000000000 --- a/src/plugins/contracts/bundled-web-search.minimax.contract.test.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { describeBundledWebSearchFastPathContract } from "../../../test/helpers/plugins/bundled-web-search-fast-path-contract.js"; - -describeBundledWebSearchFastPathContract("minimax"); diff --git a/src/plugins/contracts/bundled-web-search.moonshot.contract.test.ts b/src/plugins/contracts/bundled-web-search.moonshot.contract.test.ts deleted file mode 100644 index e5ede65aa65..00000000000 --- a/src/plugins/contracts/bundled-web-search.moonshot.contract.test.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { describeBundledWebSearchFastPathContract } from "../../../test/helpers/plugins/bundled-web-search-fast-path-contract.js"; - -describeBundledWebSearchFastPathContract("moonshot"); diff --git a/src/plugins/contracts/bundled-web-search.perplexity.contract.test.ts b/src/plugins/contracts/bundled-web-search.perplexity.contract.test.ts deleted file mode 100644 index 127315ec5da..00000000000 --- a/src/plugins/contracts/bundled-web-search.perplexity.contract.test.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { describeBundledWebSearchFastPathContract } from "../../../test/helpers/plugins/bundled-web-search-fast-path-contract.js"; - -describeBundledWebSearchFastPathContract("perplexity"); diff --git a/src/plugins/contracts/bundled-web-search.searxng.contract.test.ts b/src/plugins/contracts/bundled-web-search.searxng.contract.test.ts deleted file mode 100644 index d26351d5e62..00000000000 --- a/src/plugins/contracts/bundled-web-search.searxng.contract.test.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { describeBundledWebSearchFastPathContract } from "../../../test/helpers/plugins/bundled-web-search-fast-path-contract.js"; - -describeBundledWebSearchFastPathContract("searxng"); diff --git a/src/plugins/contracts/bundled-web-search.tavily.contract.test.ts b/src/plugins/contracts/bundled-web-search.tavily.contract.test.ts deleted file mode 100644 index d642a631be5..00000000000 --- a/src/plugins/contracts/bundled-web-search.tavily.contract.test.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { describeBundledWebSearchFastPathContract } from "../../../test/helpers/plugins/bundled-web-search-fast-path-contract.js"; - -describeBundledWebSearchFastPathContract("tavily"); diff --git a/src/plugins/contracts/bundled-web-search.xai.contract.test.ts b/src/plugins/contracts/bundled-web-search.xai.contract.test.ts deleted file mode 100644 index 2528ab62d7d..00000000000 --- a/src/plugins/contracts/bundled-web-search.xai.contract.test.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { describeBundledWebSearchFastPathContract } from "../../../test/helpers/plugins/bundled-web-search-fast-path-contract.js"; - -describeBundledWebSearchFastPathContract("xai"); diff --git a/src/plugins/contracts/registry.contract.test.ts b/src/plugins/contracts/registry.contract.test.ts index 95669f96c11..1e3da039060 100644 --- a/src/plugins/contracts/registry.contract.test.ts +++ b/src/plugins/contracts/registry.contract.test.ts @@ -10,8 +10,6 @@ import { providerContractPluginIds, } from "./registry.js"; -const REGISTRY_CONTRACT_TIMEOUT_MS = 300_000; - describe("plugin contract registry", () => { function expectUniqueIds(ids: readonly string[]) { expect(ids).toEqual([...new Set(ids)]); @@ -95,15 +93,9 @@ describe("plugin contract registry", () => { expectUniqueIds(ids()); }); - it( - "does not duplicate bundled speech provider ids", - { timeout: REGISTRY_CONTRACT_TIMEOUT_MS }, - () => { - expectUniqueIds( - pluginRegistrationContractRegistry.flatMap((entry) => entry.speechProviderIds), - ); - }, - ); + it("does not duplicate bundled speech provider ids", () => { + expectUniqueIds(pluginRegistrationContractRegistry.flatMap((entry) => entry.speechProviderIds)); + }); it("covers every bundled provider plugin discovered from manifests", () => { expectRegistryPluginIds({ @@ -158,24 +150,6 @@ describe("plugin contract registry", () => { ).toEqual(bundledWebFetchPluginIds); }); - it( - "loads bundled web fetch providers for each shared-resolver plugin", - { timeout: REGISTRY_CONTRACT_TIMEOUT_MS }, - () => { - const entriesByPluginId = new Map( - pluginRegistrationContractRegistry - .filter((entry) => entry.webFetchProviderIds.length > 0) - .map((entry) => [entry.pluginId, entry.webFetchProviderIds] as const), - ); - for (const pluginId of resolveManifestContractPluginIds({ - contract: "webFetchProviders", - origin: "bundled", - })) { - expect(entriesByPluginId.get(pluginId)?.length ?? 0).toBeGreaterThan(0); - } - }, - ); - it("covers every bundled web search plugin from the shared resolver", () => { const bundledWebSearchPluginIds = resolveManifestContractPluginIds({ contract: "webSearchProviders", @@ -190,22 +164,4 @@ describe("plugin contract registry", () => { ), ).toEqual(bundledWebSearchPluginIds); }); - - it( - "loads bundled web search providers for each shared-resolver plugin", - { timeout: REGISTRY_CONTRACT_TIMEOUT_MS }, - () => { - const entriesByPluginId = new Map( - pluginRegistrationContractRegistry - .filter((entry) => entry.webSearchProviderIds.length > 0) - .map((entry) => [entry.pluginId, entry.webSearchProviderIds] as const), - ); - for (const pluginId of resolveManifestContractPluginIds({ - contract: "webSearchProviders", - origin: "bundled", - })) { - expect(entriesByPluginId.get(pluginId)?.length ?? 0).toBeGreaterThan(0); - } - }, - ); }); diff --git a/src/plugins/web-provider-public-artifacts.test.ts b/src/plugins/web-provider-public-artifacts.test.ts index fb04f8329bc..f2b349d2f1c 100644 --- a/src/plugins/web-provider-public-artifacts.test.ts +++ b/src/plugins/web-provider-public-artifacts.test.ts @@ -1,8 +1,12 @@ import { describe, expect, it } from "vitest"; -import { resolveManifestContractPluginIds } from "./manifest-registry.js"; +import { + resolveManifestContractOwnerPluginId, + resolveManifestContractPluginIds, +} from "./manifest-registry.js"; import { hasBundledWebFetchProviderPublicArtifact, hasBundledWebSearchProviderPublicArtifact, + resolveBundledExplicitWebSearchProvidersFromPublicArtifacts, } from "./web-provider-public-artifacts.explicit.js"; describe("web provider public artifacts", () => { @@ -18,6 +22,28 @@ describe("web provider public artifacts", () => { } }); + it("keeps public web search artifacts mapped to their manifest owner plugin", () => { + const pluginIds = resolveManifestContractPluginIds({ + contract: "webSearchProviders", + origin: "bundled", + }); + + const providers = resolveBundledExplicitWebSearchProvidersFromPublicArtifacts({ + onlyPluginIds: pluginIds, + }); + + expect(providers).not.toBeNull(); + for (const provider of providers ?? []) { + expect( + resolveManifestContractOwnerPluginId({ + contract: "webSearchProviders", + value: provider.id, + origin: "bundled", + }), + ).toBe(provider.pluginId); + } + }); + it("has a public artifact for every bundled web fetch provider declared in manifests", () => { const pluginIds = resolveManifestContractPluginIds({ contract: "webFetchProviders", diff --git a/src/test-utils/bundled-plugin-public-surface.ts b/src/test-utils/bundled-plugin-public-surface.ts index 17434394dcc..1e27415fe40 100644 --- a/src/test-utils/bundled-plugin-public-surface.ts +++ b/src/test-utils/bundled-plugin-public-surface.ts @@ -65,6 +65,43 @@ function findBundledPluginMetadata(pluginId: string): BundledPluginPublicSurface return metadata; } +function readPackageName(packageDir: string): string | undefined { + try { + const packageJsonPath = path.join(packageDir, "package.json"); + const parsed = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8")) as { name?: unknown }; + return typeof parsed.name === "string" ? parsed.name : undefined; + } catch { + return undefined; + } +} + +function resolveWorkspacePackageDir(packageName: string): string { + const roots = [ + resolveBundledPluginsDir(), + path.resolve(OPENCLAW_PACKAGE_ROOT, "extensions"), + path.resolve(OPENCLAW_PACKAGE_ROOT, "dist-runtime", "extensions"), + path.resolve(OPENCLAW_PACKAGE_ROOT, "dist", "extensions"), + ].filter( + (entry, index, values): entry is string => Boolean(entry) && values.indexOf(entry) === index, + ); + + for (const root of roots) { + let entries: string[]; + try { + entries = fs.readdirSync(root); + } catch { + continue; + } + for (const entry of entries) { + const packageDir = path.join(root, entry); + if (readPackageName(packageDir) === packageName) { + return packageDir; + } + } + } + throw new Error(`Unknown workspace package: ${packageName}`); +} + export function loadBundledPluginPublicSurfaceSync(params: { pluginId: string; artifactBasename: string; @@ -165,3 +202,21 @@ export function resolveRelativeExtensionPublicModuleId(params: { .replaceAll(path.sep, "/"); return relativePath.startsWith(".") ? relativePath : `./${relativePath}`; } + +export function resolveRelativeWorkspacePackagePublicModuleId(params: { + fromModuleUrl: string; + packageName: string; + artifactBasename: string; +}): string { + const fromFilePath = fileURLToPath(params.fromModuleUrl); + const targetPath = resolveVitestSourceModulePath( + path.resolve( + resolveWorkspacePackageDir(params.packageName), + normalizeBundledPluginArtifactSubpath(params.artifactBasename), + ), + ); + const relativePath = path + .relative(path.dirname(fromFilePath), targetPath) + .replaceAll(path.sep, "/"); + return relativePath.startsWith(".") ? relativePath : `./${relativePath}`; +} diff --git a/test/helpers/channels/outbound-payload-contract.ts b/test/helpers/channels/outbound-payload-contract.ts index 785a2c194da..751d7e3468c 100644 --- a/test/helpers/channels/outbound-payload-contract.ts +++ b/test/helpers/channels/outbound-payload-contract.ts @@ -7,7 +7,6 @@ import { sendPayloadWithChunkedTextAndMedia } from "../../../src/plugin-sdk/repl import { chunkTextForOutbound } from "../../../src/plugin-sdk/text-chunking.js"; import { resetGlobalHookRunner } from "../../../src/plugins/hook-runner-global.js"; import { resolveRelativeBundledPluginPublicModuleId } from "../../../src/test-utils/bundled-plugin-public-surface.js"; -type ParseZalouserOutboundTarget = (raw: string) => { threadId: string; isGroup: boolean }; type CreateSlackOutboundPayloadHarness = (params: PayloadHarnessParams) => { run: () => Promise>; sendMock: Mock; @@ -29,18 +28,8 @@ const whatsappTestApiModuleId = resolveRelativeBundledPluginPublicModuleId({ pluginId: "whatsapp", artifactBasename: "test-api.js", }); -const zalouserSessionRouteModuleId = resolveRelativeBundledPluginPublicModuleId({ - fromModuleUrl: import.meta.url, - pluginId: "zalouser", - artifactBasename: "src/session-route.js", -}); let discordOutboundCache: Promise | undefined; -let parseZalouserOutboundTargetPromise: - | Promise<{ - parseZalouserOutboundTarget: ParseZalouserOutboundTarget; - }> - | undefined; let slackTestApiPromise: | Promise<{ createSlackOutboundPayloadHarness: CreateSlackOutboundPayloadHarness; @@ -78,14 +67,6 @@ async function getWhatsAppOutboundAsync(): Promise { return whatsappOutbound; } -async function getParseZalouserOutboundTarget(): Promise { - parseZalouserOutboundTargetPromise ??= import(zalouserSessionRouteModuleId) as Promise<{ - parseZalouserOutboundTarget: ParseZalouserOutboundTarget; - }>; - const { parseZalouserOutboundTarget } = await parseZalouserOutboundTargetPromise; - return parseZalouserOutboundTarget; -} - type PayloadHarnessParams = { payload: ReplyPayload; sendResults?: Array<{ messageId: string }>; @@ -339,7 +320,7 @@ function createZalouserHarness(params: PayloadHarnessParams) { primeChannelOutboundSendMock(sendZalouser, { ok: true, messageId: "zlu-1" }, params.sendResults); const ctx = { cfg: {}, - to: "user:987654321", + to: "987654321", text: "", payload: params.payload, }; @@ -348,12 +329,11 @@ function createZalouserHarness(params: PayloadHarnessParams) { await sendPayloadWithChunkedTextAndMedia({ ctx, sendText: async (nextCtx) => { - const target = (await getParseZalouserOutboundTarget())(nextCtx.to); return buildChannelSendResult( "zalouser", - await sendZalouser(target.threadId, nextCtx.text, { + await sendZalouser(nextCtx.to, nextCtx.text, { profile: "default", - isGroup: target.isGroup, + isGroup: false, textMode: "markdown", textChunkMode: "length", textChunkLimit: 1200, @@ -361,12 +341,11 @@ function createZalouserHarness(params: PayloadHarnessParams) { ); }, sendMedia: async (nextCtx) => { - const target = (await getParseZalouserOutboundTarget())(nextCtx.to); return buildChannelSendResult( "zalouser", - await sendZalouser(target.threadId, nextCtx.text, { + await sendZalouser(nextCtx.to, nextCtx.text, { profile: "default", - isGroup: target.isGroup, + isGroup: false, mediaUrl: nextCtx.mediaUrl, textMode: "markdown", textChunkMode: "length", @@ -377,7 +356,7 @@ function createZalouserHarness(params: PayloadHarnessParams) { emptyResult: { channel: "zalouser", messageId: "" }, }), sendMock: sendZalouser, - to: "987654321", + to: ctx.to, }; } diff --git a/test/helpers/channels/plugins-core-extension-contract.ts b/test/helpers/channels/plugins-core-extension-contract.ts index 4604ad4cfd6..f1cb1678300 100644 --- a/test/helpers/channels/plugins-core-extension-contract.ts +++ b/test/helpers/channels/plugins-core-extension-contract.ts @@ -6,10 +6,7 @@ import type { } from "../../../src/channels/plugins/types.js"; import type { OpenClawConfig } from "../../../src/config/config.js"; import type { LineProbeResult } from "../../../src/plugin-sdk/line.js"; -import { - loadBundledPluginApiSync, - loadBundledPluginContractApiSync, -} from "../../../src/test-utils/bundled-plugin-public-surface.js"; +import { loadBundledPluginContractApiSync } from "../../../src/test-utils/bundled-plugin-public-surface.js"; import { withEnvAsync } from "../../../src/test-utils/env.js"; type DiscordContractApiSurface = Pick< @@ -31,12 +28,15 @@ type TelegramContractApiSurface = Pick< >; type TelegramProbe = import("@openclaw/telegram/api.js").TelegramProbe; type TelegramTokenResolution = import("@openclaw/telegram/api.js").TelegramTokenResolution; -type WhatsAppApiSurface = typeof import("@openclaw/whatsapp/api.js"); +type WhatsAppContractApiSurface = Pick< + typeof import("@openclaw/whatsapp/contract-api.js"), + "listWhatsAppDirectoryPeersFromConfig" | "listWhatsAppDirectoryGroupsFromConfig" +>; let discordContractApi: DiscordContractApiSurface | undefined; let slackContractApi: SlackContractApiSurface | undefined; let telegramContractApi: TelegramContractApiSurface | undefined; -let whatsappApi: WhatsAppApiSurface | undefined; +let whatsappContractApi: WhatsAppContractApiSurface | undefined; function getDiscordContractApi(): DiscordContractApiSurface { discordContractApi ??= loadBundledPluginContractApiSync("discord"); @@ -53,9 +53,9 @@ function getTelegramContractApi(): TelegramContractApiSurface { return telegramContractApi; } -function getWhatsAppApi(): WhatsAppApiSurface { - whatsappApi ??= loadBundledPluginApiSync("whatsapp"); - return whatsappApi; +function getWhatsAppContractApi(): WhatsAppContractApiSurface { + whatsappContractApi ??= loadBundledPluginContractApiSync("whatsapp"); + return whatsappContractApi; } type DirectoryListFn = (params: { @@ -359,8 +359,8 @@ export function describeTelegramPluginsCoreExtensionContract() { export function describeWhatsAppPluginsCoreExtensionContract() { describe("whatsapp plugins-core extension contract", () => { - const listPeers = () => getWhatsAppApi().listWhatsAppDirectoryPeersFromConfig; - const listGroups = () => getWhatsAppApi().listWhatsAppDirectoryGroupsFromConfig; + const listPeers = () => getWhatsAppContractApi().listWhatsAppDirectoryPeersFromConfig; + const listGroups = () => getWhatsAppContractApi().listWhatsAppDirectoryGroupsFromConfig; it("lists peers/groups from config", async () => { const cfg = { diff --git a/test/helpers/plugins/bundled-web-search-fast-path-contract.ts b/test/helpers/plugins/bundled-web-search-fast-path-contract.ts deleted file mode 100644 index ba197c71141..00000000000 --- a/test/helpers/plugins/bundled-web-search-fast-path-contract.ts +++ /dev/null @@ -1,281 +0,0 @@ -import fs from "node:fs"; -import path from "node:path"; -import { describe, expect, it } from "vitest"; -import type { OpenClawConfig } from "../../../src/config/config.js"; -import { resolveBundledPluginsDir } from "../../../src/plugins/bundled-dir.js"; -import { - resolveBundledExplicitRuntimeWebSearchProvidersFromPublicArtifacts, - resolveBundledExplicitWebSearchProvidersFromPublicArtifacts, -} from "../../../src/plugins/web-provider-public-artifacts.explicit.js"; -import { normalizeOptionalLowercaseString } from "../../../src/shared/string-coerce.js"; - -type ComparableProvider = { - pluginId: string; - id: string; - label: string; - hint: string; - envVars: string[]; - placeholder: string; - signupUrl: string; - docsUrl?: string; - autoDetectOrder?: number; - requiresCredential?: boolean; - credentialPath: string; - inactiveSecretPaths?: string[]; - hasConfiguredCredentialAccessors: boolean; - hasApplySelectionConfig: boolean; - hasResolveRuntimeMetadata: boolean; -}; - -type MinimalBundledPluginManifest = { - id?: unknown; - contracts?: { - webSearchProviders?: unknown; - }; -}; - -const bundledWebSearchManifestContracts = new Map< - string, - { pluginId: string; webSearchProviderIds: string[] } | null ->(); - -function readBundledWebSearchManifestContract(pluginId: string) { - if (bundledWebSearchManifestContracts.has(pluginId)) { - return bundledWebSearchManifestContracts.get(pluginId) ?? null; - } - - const bundledPluginsDir = resolveBundledPluginsDir(); - if (!bundledPluginsDir) { - bundledWebSearchManifestContracts.set(pluginId, null); - return null; - } - - const manifestPath = path.join(bundledPluginsDir, pluginId, "openclaw.plugin.json"); - const manifest = JSON.parse( - fs.readFileSync(manifestPath, "utf8"), - ) as MinimalBundledPluginManifest; - const manifestPluginId = typeof manifest.id === "string" ? manifest.id : ""; - const webSearchProviderIds = Array.isArray(manifest.contracts?.webSearchProviders) - ? manifest.contracts.webSearchProviders.filter( - (providerId): providerId is string => typeof providerId === "string", - ) - : []; - const contract = { pluginId: manifestPluginId, webSearchProviderIds }; - bundledWebSearchManifestContracts.set(pluginId, contract); - return contract; -} - -function resolveBundledManifestWebSearchOwnerPluginId(params: { - pluginId: string; - providerId: string; -}): string | undefined { - const normalizedProviderId = normalizeOptionalLowercaseString(params.providerId); - if (!normalizedProviderId) { - return undefined; - } - - const contract = readBundledWebSearchManifestContract(params.pluginId); - if ( - !contract?.webSearchProviderIds.some( - (candidate) => normalizeOptionalLowercaseString(candidate) === normalizedProviderId, - ) - ) { - return undefined; - } - return contract.pluginId || undefined; -} - -function toComparableEntry(params: { - pluginId: string; - provider: { - id: string; - label: string; - hint: string; - envVars: string[]; - placeholder: string; - signupUrl: string; - docsUrl?: string; - autoDetectOrder?: number; - requiresCredential?: boolean; - credentialPath: string; - inactiveSecretPaths?: string[]; - getConfiguredCredentialValue?: unknown; - setConfiguredCredentialValue?: unknown; - applySelectionConfig?: unknown; - resolveRuntimeMetadata?: unknown; - }; -}): ComparableProvider { - return { - pluginId: params.pluginId, - id: params.provider.id, - label: params.provider.label, - hint: params.provider.hint, - envVars: params.provider.envVars, - placeholder: params.provider.placeholder, - signupUrl: params.provider.signupUrl, - docsUrl: params.provider.docsUrl, - autoDetectOrder: params.provider.autoDetectOrder, - requiresCredential: params.provider.requiresCredential, - credentialPath: params.provider.credentialPath, - inactiveSecretPaths: params.provider.inactiveSecretPaths, - hasConfiguredCredentialAccessors: - typeof params.provider.getConfiguredCredentialValue === "function" && - typeof params.provider.setConfiguredCredentialValue === "function", - hasApplySelectionConfig: typeof params.provider.applySelectionConfig === "function", - hasResolveRuntimeMetadata: typeof params.provider.resolveRuntimeMetadata === "function", - }; -} - -function sortComparableEntries(entries: ComparableProvider[]): ComparableProvider[] { - return [...entries].toSorted((left, right) => { - const leftOrder = left.autoDetectOrder ?? Number.MAX_SAFE_INTEGER; - const rightOrder = right.autoDetectOrder ?? Number.MAX_SAFE_INTEGER; - return ( - leftOrder - rightOrder || - left.id.localeCompare(right.id) || - left.pluginId.localeCompare(right.pluginId) - ); - }); -} - -export function describeBundledWebSearchFastPathContract(pluginId: string) { - describe(`${pluginId} bundled web search fast-path contract`, () => { - it("keeps provider-to-plugin ids aligned with bundled contracts", () => { - const providers = - resolveBundledExplicitWebSearchProvidersFromPublicArtifacts({ - onlyPluginIds: [pluginId], - }) ?? []; - expect(providers.length).toBeGreaterThan(0); - for (const provider of providers) { - expect( - resolveBundledManifestWebSearchOwnerPluginId({ - pluginId, - providerId: provider.id, - }), - ).toBe(pluginId); - } - }); - - it("keeps fast-path provider metadata aligned with the bundled runtime artifact", async () => { - const fastPathProviders = - resolveBundledExplicitWebSearchProvidersFromPublicArtifacts({ - onlyPluginIds: [pluginId], - })?.filter((provider) => provider.pluginId === pluginId) ?? []; - const bundledProviderEntries = - resolveBundledExplicitRuntimeWebSearchProvidersFromPublicArtifacts({ - onlyPluginIds: [pluginId], - })?.filter((entry) => entry.pluginId === pluginId) ?? []; - - expect( - sortComparableEntries( - fastPathProviders.map((provider) => - toComparableEntry({ - pluginId: provider.pluginId, - provider, - }), - ), - ), - ).toEqual( - sortComparableEntries( - bundledProviderEntries.map(({ pluginId: entryPluginId, ...provider }) => - toComparableEntry({ - pluginId: entryPluginId, - provider, - }), - ), - ), - ); - - for (const fastPathProvider of fastPathProviders) { - const bundledEntry = bundledProviderEntries.find( - (entry) => entry.id === fastPathProvider.id, - ); - expect(bundledEntry).toBeDefined(); - const contractProvider = bundledEntry!; - - const fastSearchConfig: Record = {}; - const contractSearchConfig: Record = {}; - fastPathProvider.setCredentialValue(fastSearchConfig, "test-key"); - contractProvider.setCredentialValue(contractSearchConfig, "test-key"); - expect(fastSearchConfig).toEqual(contractSearchConfig); - expect(fastPathProvider.getCredentialValue(fastSearchConfig)).toEqual( - contractProvider.getCredentialValue(contractSearchConfig), - ); - - const fastConfig = {} as OpenClawConfig; - const contractConfig = {} as OpenClawConfig; - fastPathProvider.setConfiguredCredentialValue?.(fastConfig, "test-key"); - contractProvider.setConfiguredCredentialValue?.(contractConfig, "test-key"); - expect(fastConfig).toEqual(contractConfig); - expect(fastPathProvider.getConfiguredCredentialValue?.(fastConfig)).toEqual( - contractProvider.getConfiguredCredentialValue?.(contractConfig), - ); - - if (fastPathProvider.applySelectionConfig || contractProvider.applySelectionConfig) { - expect(fastPathProvider.applySelectionConfig?.({} as OpenClawConfig)).toEqual( - contractProvider.applySelectionConfig?.({} as OpenClawConfig), - ); - } - - if (fastPathProvider.resolveRuntimeMetadata || contractProvider.resolveRuntimeMetadata) { - const metadataCases = [ - { - searchConfig: fastSearchConfig, - resolvedCredential: { - value: "pplx-test", - source: "secretRef" as const, - fallbackEnvVar: undefined, - }, - }, - { - searchConfig: fastSearchConfig, - resolvedCredential: { - value: undefined, - source: "env" as const, - fallbackEnvVar: "OPENROUTER_API_KEY", - }, - }, - { - searchConfig: { - ...fastSearchConfig, - perplexity: { - ...(fastSearchConfig.perplexity as Record | undefined), - model: "custom-model", - }, - }, - resolvedCredential: { - value: "pplx-test", - source: "secretRef" as const, - fallbackEnvVar: undefined, - }, - }, - ]; - - for (const testCase of metadataCases) { - expect( - await fastPathProvider.resolveRuntimeMetadata?.({ - config: fastConfig, - searchConfig: testCase.searchConfig, - runtimeMetadata: { - diagnostics: [], - providerSource: "configured", - }, - resolvedCredential: testCase.resolvedCredential, - }), - ).toEqual( - await contractProvider.resolveRuntimeMetadata?.({ - config: contractConfig, - searchConfig: testCase.searchConfig, - runtimeMetadata: { - diagnostics: [], - providerSource: "configured", - }, - resolvedCredential: testCase.resolvedCredential, - }), - ); - } - } - } - }); - }); -} diff --git a/test/helpers/plugins/tts-contract-suites.ts b/test/helpers/plugins/tts-contract-suites.ts index fca91bd5200..fec4d1d1dd6 100644 --- a/test/helpers/plugins/tts-contract-suites.ts +++ b/test/helpers/plugins/tts-contract-suites.ts @@ -1,33 +1,33 @@ import type { AssistantMessage } from "@mariozechner/pi-ai"; import { beforeEach, describe, expect, it, vi } from "vitest"; import type { OpenClawConfig } from "../../../src/config/config.js"; -import { __testing as pluginLoaderTesting } from "../../../src/plugins/loader.js"; import { createEmptyPluginRegistry } from "../../../src/plugins/registry-empty.js"; import { setActivePluginRegistry } from "../../../src/plugins/runtime.js"; import type { SpeechProviderPlugin } from "../../../src/plugins/types.js"; -import { resolveRelativeExtensionPublicModuleId } from "../../../src/test-utils/bundled-plugin-public-surface.js"; +import { resolveRelativeWorkspacePackagePublicModuleId } from "../../../src/test-utils/bundled-plugin-public-surface.js"; import { withEnv } from "../../../src/test-utils/env.js"; -import { summarizeText as summarizeTextCore } from "../../../src/tts/tts-core.js"; import type { ResolvedTtsConfig } from "../../../src/tts/tts-types.js"; type TtsRuntimeModule = typeof import("../../../src/tts/tts.js"); +type TtsCoreModule = typeof import("../../../src/tts/tts-core.js"); -const speechCoreRuntimeApiModuleId = resolveRelativeExtensionPublicModuleId({ +const speechCoreRuntimeApiModuleId = resolveRelativeWorkspacePackagePublicModuleId({ fromModuleUrl: import.meta.url, - dirName: "speech-core", + packageName: "@openclaw/speech-core", artifactBasename: "runtime-api.js", }); let ttsRuntime: TtsRuntimeModule; let ttsRuntimePromise: Promise | null = null; let ttsRuntimeInitialized = false; -let ttsPluginRegistryCacheKey: string | null = null; +let ttsCorePromise: Promise | null = null; let completeSimple: typeof import("@mariozechner/pi-ai").completeSimple; let getApiKeyForModelMock: typeof import("../../../src/agents/model-auth.js").getApiKeyForModel; let requireApiKeyMock: typeof import("../../../src/agents/model-auth.js").requireApiKey; let resolveModelAsyncMock: typeof import("../../../src/agents/pi-embedded-runner/model.js").resolveModelAsync; let ensureCustomApiRegisteredMock: typeof import("../../../src/agents/custom-api-registry.js").ensureCustomApiRegistered; let prepareModelForSimpleCompletionMock: typeof import("../../../src/agents/simple-completion-transport.js").prepareModelForSimpleCompletion; +let summarizeTextCore: TtsCoreModule["summarizeText"]; let resolveTtsConfig: TtsRuntimeModule["resolveTtsConfig"]; let maybeApplyTtsToPayload: TtsRuntimeModule["maybeApplyTtsToPayload"]; let getTtsProvider: TtsRuntimeModule["getTtsProvider"]; @@ -37,14 +37,27 @@ let getResolvedSpeechProviderConfig: TtsRuntimeModule["_test"]["getResolvedSpeec let formatTtsProviderError: TtsRuntimeModule["_test"]["formatTtsProviderError"]; let sanitizeTtsErrorForLog: TtsRuntimeModule["_test"]["sanitizeTtsErrorForLog"]; -vi.mock("@mariozechner/pi-ai", () => ({ - completeSimple: vi.fn(), -})); +vi.mock("@mariozechner/pi-ai", () => { + const getApiProvider = vi.fn(() => undefined); + return { + completeSimple: vi.fn(), + createAssistantMessageEventStream: vi.fn(), + getApiProvider, + getModel: vi.fn(), + registerApiProvider: vi.fn(), + streamAnthropic: vi.fn(), + streamSimple: vi.fn(), + streamSimpleOpenAICompletions: vi.fn(), + }; +}); -vi.mock("@mariozechner/pi-ai/oauth", () => ({ - getOAuthProviders: () => [], - getOAuthApiKey: vi.fn(async () => null), -})); +vi.mock("@mariozechner/pi-ai/oauth", () => { + return { + getOAuthProviders: () => [], + getOAuthApiKey: vi.fn(async () => null), + loginOpenAICodex: vi.fn(), + }; +}); function createResolvedModel(provider: string, modelId: string, api = "openai-completions") { return { @@ -399,11 +412,9 @@ async function loadTtsRuntime(): Promise { return await ttsRuntimePromise; } -function getTtsPluginRegistryCacheKey(): string { - ttsPluginRegistryCacheKey ??= pluginLoaderTesting.resolvePluginLoadCacheContext({ - config: {}, - }).cacheKey; - return ttsPluginRegistryCacheKey; +async function loadTtsCore(): Promise { + ttsCorePromise ??= import("../../../src/tts/tts-core.js"); + return await ttsCorePromise; } async function setupTtsRuntime() { @@ -433,7 +444,7 @@ function setupTestSpeechProviderRegistry() { { pluginId: "elevenlabs", provider: buildTestElevenLabsSpeechProvider(), source: "test" }, { pluginId: "google", provider: buildTestGoogleSpeechProvider(), source: "test" }, ]; - setActivePluginRegistry(registry, getTtsPluginRegistryCacheKey()); + setActivePluginRegistry(registry); } function createResolvedSummarizationConfig(cfg: OpenClawConfig): ResolvedTtsConfig { @@ -466,6 +477,8 @@ function createResolvedSummarizationConfig(cfg: OpenClawConfig): ResolvedTtsConf } async function setupSummarizationMocks() { + ({ summarizeText: summarizeTextCore } = await loadTtsCore()); + prepareModelForSimpleCompletionMock = vi.fn(({ model }) => model); ({ completeSimple } = await import("@mariozechner/pi-ai")); ({ getApiKeyForModel: getApiKeyForModelMock, requireApiKey: requireApiKeyMock } = await import("../../../src/agents/model-auth.js")); @@ -992,7 +1005,7 @@ export function describeTtsProviderRuntimeContract() { { pluginId: "openai", provider: throwingPrimary, source: "test" }, { pluginId: "microsoft", provider: fallback, source: "test" }, ]; - setActivePluginRegistry(registry, getTtsPluginRegistryCacheKey()); + setActivePluginRegistry(registry); const result = await ttsRuntime.synthesizeSpeech({ text: "hello fallback", @@ -1060,7 +1073,7 @@ export function describeTtsProviderRuntimeContract() { { pluginId: "primary-throws", provider: throwingPrimary, source: "test" }, { pluginId: "microsoft", provider: fallback, source: "test" }, ]; - setActivePluginRegistry(registry, getTtsPluginRegistryCacheKey()); + setActivePluginRegistry(registry); const result = await ttsRuntime.textToSpeechTelephony({ text: "hello telephony fallback", @@ -1107,7 +1120,7 @@ export function describeTtsProviderRuntimeContract() { registry.speechProviders = [ { pluginId: "openai", provider: failingProvider, source: "test" }, ]; - setActivePluginRegistry(registry, getTtsPluginRegistryCacheKey()); + setActivePluginRegistry(registry); const result = await ttsRuntime.textToSpeech({ text: "hello",