From f8f719ee23c7462e533c79df02b41ff607c0c654 Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Fri, 1 May 2026 02:08:08 -0700 Subject: [PATCH] test(e2e): isolate plugin matrix runtime deps --- extensions/google/index.ts | 128 +++++++++++++++++- .../bundled-plugin-install-uninstall/sweep.sh | 6 +- 2 files changed, 130 insertions(+), 4 deletions(-) diff --git a/extensions/google/index.ts b/extensions/google/index.ts index 8474ef7bd61..0f53f977d9f 100644 --- a/extensions/google/index.ts +++ b/extensions/google/index.ts @@ -2,6 +2,14 @@ import type { ImageGenerationProvider } from "openclaw/plugin-sdk/image-generati import type { MediaUnderstandingProvider } from "openclaw/plugin-sdk/media-understanding"; import type { MusicGenerationProvider } from "openclaw/plugin-sdk/music-generation"; import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry"; +import type { + RealtimeVoiceBridge, + RealtimeVoiceBridgeCreateRequest, + RealtimeVoiceProviderConfig, + RealtimeVoiceProviderPlugin, +} from "openclaw/plugin-sdk/realtime-voice"; +import { normalizeResolvedSecretInputString } from "openclaw/plugin-sdk/secret-input"; +import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime"; import type { VideoGenerationProvider } from "openclaw/plugin-sdk/video-generation"; import { buildGoogleGeminiCliBackend } from "./cli-backend.js"; import { registerGoogleGeminiCliProvider } from "./gemini-cli-provider.js"; @@ -11,13 +19,13 @@ import { } from "./generation-provider-metadata.js"; import { geminiMemoryEmbeddingProviderAdapter } from "./memory-embedding-adapter.js"; import { registerGoogleProvider } from "./provider-registration.js"; -import { buildGoogleRealtimeVoiceProvider } from "./realtime-voice-provider.js"; import { buildGoogleSpeechProvider } from "./speech-provider.js"; import { createGeminiWebSearchProvider } from "./src/gemini-web-search-provider.js"; let googleImageGenerationProviderPromise: Promise | null = null; let googleMediaUnderstandingProviderPromise: Promise | null = null; let googleMusicGenerationProviderPromise: Promise | null = null; +let googleRealtimeVoiceProviderPromise: Promise | null = null; let googleVideoGenerationProviderPromise: Promise | null = null; type GoogleMediaUnderstandingProvider = Required< @@ -54,6 +62,15 @@ async function loadGoogleMusicGenerationProvider(): Promise { + if (!googleRealtimeVoiceProviderPromise) { + googleRealtimeVoiceProviderPromise = import("./realtime-voice-provider.js").then((mod) => + mod.buildGoogleRealtimeVoiceProvider(), + ); + } + return await googleRealtimeVoiceProviderPromise; +} + async function loadGoogleVideoGenerationProvider(): Promise { if (!googleVideoGenerationProviderPromise) { googleVideoGenerationProviderPromise = import("./video-generation-provider.js").then((mod) => @@ -137,6 +154,113 @@ function createLazyGoogleMusicGenerationProvider(): MusicGenerationProvider { }; } +function resolveGoogleRealtimeProviderConfig( + rawConfig: RealtimeVoiceProviderConfig, + cfg?: { models?: { providers?: { google?: { apiKey?: unknown } } } }, +): RealtimeVoiceProviderConfig { + const providers = + typeof rawConfig.providers === "object" && + rawConfig.providers !== null && + !Array.isArray(rawConfig.providers) + ? (rawConfig.providers as Record) + : undefined; + const nested = providers?.google; + const raw = + typeof nested === "object" && nested !== null && !Array.isArray(nested) + ? (nested as Record) + : typeof rawConfig.google === "object" && + rawConfig.google !== null && + !Array.isArray(rawConfig.google) + ? (rawConfig.google as Record) + : rawConfig; + return { + ...raw, + ...(raw.apiKey === undefined + ? cfg?.models?.providers?.google?.apiKey === undefined + ? {} + : { + apiKey: normalizeResolvedSecretInputString({ + value: cfg.models.providers.google.apiKey, + path: "models.providers.google.apiKey", + }), + } + : { + apiKey: normalizeResolvedSecretInputString({ + value: raw.apiKey, + path: "plugins.entries.voice-call.config.realtime.providers.google.apiKey", + }), + }), + }; +} + +function resolveGoogleRealtimeEnvApiKey(): string | undefined { + return ( + normalizeOptionalString(process.env.GEMINI_API_KEY) ?? + normalizeOptionalString(process.env.GOOGLE_API_KEY) + ); +} + +function createLazyGoogleRealtimeVoiceBridge( + req: RealtimeVoiceBridgeCreateRequest, +): RealtimeVoiceBridge { + let bridge: RealtimeVoiceBridge | undefined; + let bridgePromise: Promise | undefined; + const loadBridge = async () => { + if (!bridgePromise) { + bridgePromise = loadGoogleRealtimeVoiceProvider().then((provider) => + provider.createBridge(req), + ); + } + bridge = await bridgePromise; + return bridge; + }; + const requireBridge = () => { + if (!bridge) { + throw new Error("Google realtime voice bridge is not connected"); + } + return bridge; + }; + return { + supportsToolResultContinuation: true, + connect: async () => { + await (await loadBridge()).connect(); + }, + sendAudio: (audio) => requireBridge().sendAudio(audio), + setMediaTimestamp: (ts) => requireBridge().setMediaTimestamp(ts), + sendUserMessage: (text) => requireBridge().sendUserMessage?.(text), + triggerGreeting: (instructions) => requireBridge().triggerGreeting?.(instructions), + handleBargeIn: (options) => requireBridge().handleBargeIn?.(options), + submitToolResult: (callId, result, options) => + requireBridge().submitToolResult(callId, result, options), + acknowledgeMark: () => requireBridge().acknowledgeMark(), + close: () => bridge?.close(), + isConnected: () => bridge?.isConnected() ?? false, + }; +} + +function createLazyGoogleRealtimeVoiceProvider(): RealtimeVoiceProviderPlugin { + return { + id: "google", + label: "Google Live Voice", + autoSelectOrder: 20, + resolveConfig: ({ cfg, rawConfig }) => resolveGoogleRealtimeProviderConfig(rawConfig, cfg), + isConfigured: ({ cfg, providerConfig }) => + Boolean( + normalizeOptionalString(providerConfig.apiKey) ?? + normalizeOptionalString(cfg?.models?.providers?.google?.apiKey) ?? + resolveGoogleRealtimeEnvApiKey(), + ), + createBridge: createLazyGoogleRealtimeVoiceBridge, + createBrowserSession: async (req) => { + const provider = await loadGoogleRealtimeVoiceProvider(); + if (!provider.createBrowserSession) { + throw new Error("Google realtime voice browser sessions are unavailable"); + } + return await provider.createBrowserSession(req); + }, + }; +} + function createLazyGoogleVideoGenerationProvider(): VideoGenerationProvider { return { ...createGoogleVideoGenerationProviderMetadata(), @@ -157,7 +281,7 @@ export default definePluginEntry({ api.registerImageGenerationProvider(createLazyGoogleImageGenerationProvider()); api.registerMediaUnderstandingProvider(createLazyGoogleMediaUnderstandingProvider()); api.registerMusicGenerationProvider(createLazyGoogleMusicGenerationProvider()); - api.registerRealtimeVoiceProvider(buildGoogleRealtimeVoiceProvider()); + api.registerRealtimeVoiceProvider(createLazyGoogleRealtimeVoiceProvider()); api.registerSpeechProvider(buildGoogleSpeechProvider()); api.registerVideoGenerationProvider(createLazyGoogleVideoGenerationProvider()); api.registerWebSearchProvider(createGeminiWebSearchProvider()); diff --git a/scripts/e2e/lib/bundled-plugin-install-uninstall/sweep.sh b/scripts/e2e/lib/bundled-plugin-install-uninstall/sweep.sh index bbd0ca243e9..d440ae9b95f 100644 --- a/scripts/e2e/lib/bundled-plugin-install-uninstall/sweep.sh +++ b/scripts/e2e/lib/bundled-plugin-install-uninstall/sweep.sh @@ -15,8 +15,8 @@ fi export OPENCLAW_ENTRY openclaw_e2e_eval_test_state_from_b64 "${OPENCLAW_TEST_STATE_SCRIPT_B64:?missing OPENCLAW_TEST_STATE_SCRIPT_B64}" -export OPENCLAW_PLUGIN_STAGE_DIR="${OPENCLAW_PLUGIN_STAGE_DIR:-$HOME/.openclaw/plugin-runtime-deps}" -mkdir -p "$OPENCLAW_PLUGIN_STAGE_DIR" +OPENCLAW_PLUGIN_STAGE_BASE_DIR="${OPENCLAW_PLUGIN_STAGE_DIR:-$HOME/.openclaw/plugin-runtime-deps}" +mkdir -p "$OPENCLAW_PLUGIN_STAGE_BASE_DIR" probe="scripts/e2e/lib/bundled-plugin-install-uninstall/probe.mjs" runtime_smoke="scripts/e2e/lib/bundled-plugin-install-uninstall/runtime-smoke.mjs" @@ -33,6 +33,8 @@ echo "Selected ${#plugin_entries[@]} bundled plugins for shard ${OPENCLAW_BUNDLE plugin_index=0 for plugin_entry in "${plugin_entries[@]}"; do IFS=$'\t' read -r plugin_id plugin_dir requires_config <<<"$plugin_entry" + export OPENCLAW_PLUGIN_STAGE_DIR="$OPENCLAW_PLUGIN_STAGE_BASE_DIR/$plugin_index-$plugin_id" + mkdir -p "$OPENCLAW_PLUGIN_STAGE_DIR" install_log="/tmp/openclaw-install-${plugin_index}.log" uninstall_log="/tmp/openclaw-uninstall-${plugin_index}.log" plugin_started_at="$(date +%s)"