test: slim contract suite imports

This commit is contained in:
Peter Steinberger
2026-04-17 23:40:35 +01:00
parent 52b8e318bd
commit 7db9a53254
19 changed files with 141 additions and 422 deletions

View File

@@ -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,

View File

@@ -1,3 +0,0 @@
import { describeBundledWebSearchFastPathContract } from "../../../test/helpers/plugins/bundled-web-search-fast-path-contract.js";
describeBundledWebSearchFastPathContract("brave");

View File

@@ -1,3 +0,0 @@
import { describeBundledWebSearchFastPathContract } from "../../../test/helpers/plugins/bundled-web-search-fast-path-contract.js";
describeBundledWebSearchFastPathContract("duckduckgo");

View File

@@ -1,3 +0,0 @@
import { describeBundledWebSearchFastPathContract } from "../../../test/helpers/plugins/bundled-web-search-fast-path-contract.js";
describeBundledWebSearchFastPathContract("exa");

View File

@@ -1,3 +0,0 @@
import { describeBundledWebSearchFastPathContract } from "../../../test/helpers/plugins/bundled-web-search-fast-path-contract.js";
describeBundledWebSearchFastPathContract("firecrawl");

View File

@@ -1,3 +0,0 @@
import { describeBundledWebSearchFastPathContract } from "../../../test/helpers/plugins/bundled-web-search-fast-path-contract.js";
describeBundledWebSearchFastPathContract("google");

View File

@@ -1,3 +0,0 @@
import { describeBundledWebSearchFastPathContract } from "../../../test/helpers/plugins/bundled-web-search-fast-path-contract.js";
describeBundledWebSearchFastPathContract("minimax");

View File

@@ -1,3 +0,0 @@
import { describeBundledWebSearchFastPathContract } from "../../../test/helpers/plugins/bundled-web-search-fast-path-contract.js";
describeBundledWebSearchFastPathContract("moonshot");

View File

@@ -1,3 +0,0 @@
import { describeBundledWebSearchFastPathContract } from "../../../test/helpers/plugins/bundled-web-search-fast-path-contract.js";
describeBundledWebSearchFastPathContract("perplexity");

View File

@@ -1,3 +0,0 @@
import { describeBundledWebSearchFastPathContract } from "../../../test/helpers/plugins/bundled-web-search-fast-path-contract.js";
describeBundledWebSearchFastPathContract("searxng");

View File

@@ -1,3 +0,0 @@
import { describeBundledWebSearchFastPathContract } from "../../../test/helpers/plugins/bundled-web-search-fast-path-contract.js";
describeBundledWebSearchFastPathContract("tavily");

View File

@@ -1,3 +0,0 @@
import { describeBundledWebSearchFastPathContract } from "../../../test/helpers/plugins/bundled-web-search-fast-path-contract.js";
describeBundledWebSearchFastPathContract("xai");

View File

@@ -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);
}
},
);
});

View File

@@ -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",

View File

@@ -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}`;
}

View File

@@ -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,
};
}

View File

@@ -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 = {

View File

@@ -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,
}),
);
}
}
}
});
});
}

View File

@@ -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",