mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 05:20:43 +00:00
test: slim contract suite imports
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
import { describeBundledWebSearchFastPathContract } from "../../../test/helpers/plugins/bundled-web-search-fast-path-contract.js";
|
||||
|
||||
describeBundledWebSearchFastPathContract("brave");
|
||||
@@ -1,3 +0,0 @@
|
||||
import { describeBundledWebSearchFastPathContract } from "../../../test/helpers/plugins/bundled-web-search-fast-path-contract.js";
|
||||
|
||||
describeBundledWebSearchFastPathContract("duckduckgo");
|
||||
@@ -1,3 +0,0 @@
|
||||
import { describeBundledWebSearchFastPathContract } from "../../../test/helpers/plugins/bundled-web-search-fast-path-contract.js";
|
||||
|
||||
describeBundledWebSearchFastPathContract("exa");
|
||||
@@ -1,3 +0,0 @@
|
||||
import { describeBundledWebSearchFastPathContract } from "../../../test/helpers/plugins/bundled-web-search-fast-path-contract.js";
|
||||
|
||||
describeBundledWebSearchFastPathContract("firecrawl");
|
||||
@@ -1,3 +0,0 @@
|
||||
import { describeBundledWebSearchFastPathContract } from "../../../test/helpers/plugins/bundled-web-search-fast-path-contract.js";
|
||||
|
||||
describeBundledWebSearchFastPathContract("google");
|
||||
@@ -1,3 +0,0 @@
|
||||
import { describeBundledWebSearchFastPathContract } from "../../../test/helpers/plugins/bundled-web-search-fast-path-contract.js";
|
||||
|
||||
describeBundledWebSearchFastPathContract("minimax");
|
||||
@@ -1,3 +0,0 @@
|
||||
import { describeBundledWebSearchFastPathContract } from "../../../test/helpers/plugins/bundled-web-search-fast-path-contract.js";
|
||||
|
||||
describeBundledWebSearchFastPathContract("moonshot");
|
||||
@@ -1,3 +0,0 @@
|
||||
import { describeBundledWebSearchFastPathContract } from "../../../test/helpers/plugins/bundled-web-search-fast-path-contract.js";
|
||||
|
||||
describeBundledWebSearchFastPathContract("perplexity");
|
||||
@@ -1,3 +0,0 @@
|
||||
import { describeBundledWebSearchFastPathContract } from "../../../test/helpers/plugins/bundled-web-search-fast-path-contract.js";
|
||||
|
||||
describeBundledWebSearchFastPathContract("searxng");
|
||||
@@ -1,3 +0,0 @@
|
||||
import { describeBundledWebSearchFastPathContract } from "../../../test/helpers/plugins/bundled-web-search-fast-path-contract.js";
|
||||
|
||||
describeBundledWebSearchFastPathContract("tavily");
|
||||
@@ -1,3 +0,0 @@
|
||||
import { describeBundledWebSearchFastPathContract } from "../../../test/helpers/plugins/bundled-web-search-fast-path-contract.js";
|
||||
|
||||
describeBundledWebSearchFastPathContract("xai");
|
||||
@@ -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);
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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<T extends object>(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}`;
|
||||
}
|
||||
|
||||
@@ -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<Record<string, unknown>>;
|
||||
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<ChannelOutboundAdapter> | undefined;
|
||||
let parseZalouserOutboundTargetPromise:
|
||||
| Promise<{
|
||||
parseZalouserOutboundTarget: ParseZalouserOutboundTarget;
|
||||
}>
|
||||
| undefined;
|
||||
let slackTestApiPromise:
|
||||
| Promise<{
|
||||
createSlackOutboundPayloadHarness: CreateSlackOutboundPayloadHarness;
|
||||
@@ -78,14 +67,6 @@ async function getWhatsAppOutboundAsync(): Promise<ChannelOutboundAdapter> {
|
||||
return whatsappOutbound;
|
||||
}
|
||||
|
||||
async function getParseZalouserOutboundTarget(): Promise<ParseZalouserOutboundTarget> {
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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<DiscordContractApiSurface>("discord");
|
||||
@@ -53,9 +53,9 @@ function getTelegramContractApi(): TelegramContractApiSurface {
|
||||
return telegramContractApi;
|
||||
}
|
||||
|
||||
function getWhatsAppApi(): WhatsAppApiSurface {
|
||||
whatsappApi ??= loadBundledPluginApiSync<WhatsAppApiSurface>("whatsapp");
|
||||
return whatsappApi;
|
||||
function getWhatsAppContractApi(): WhatsAppContractApiSurface {
|
||||
whatsappContractApi ??= loadBundledPluginContractApiSync<WhatsAppContractApiSurface>("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 = {
|
||||
|
||||
@@ -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<string, unknown> = {};
|
||||
const contractSearchConfig: Record<string, unknown> = {};
|
||||
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<string, unknown> | 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,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -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<TtsRuntimeModule> | null = null;
|
||||
let ttsRuntimeInitialized = false;
|
||||
let ttsPluginRegistryCacheKey: string | null = null;
|
||||
let ttsCorePromise: Promise<TtsCoreModule> | 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<TtsRuntimeModule> {
|
||||
return await ttsRuntimePromise;
|
||||
}
|
||||
|
||||
function getTtsPluginRegistryCacheKey(): string {
|
||||
ttsPluginRegistryCacheKey ??= pluginLoaderTesting.resolvePluginLoadCacheContext({
|
||||
config: {},
|
||||
}).cacheKey;
|
||||
return ttsPluginRegistryCacheKey;
|
||||
async function loadTtsCore(): Promise<TtsCoreModule> {
|
||||
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",
|
||||
|
||||
Reference in New Issue
Block a user