diff --git a/scripts/test-projects.test-support.mjs b/scripts/test-projects.test-support.mjs index f915a5d1610..c309e2b1e6e 100644 --- a/scripts/test-projects.test-support.mjs +++ b/scripts/test-projects.test-support.mjs @@ -215,6 +215,15 @@ const BROAD_CHANGED_RERUN_PATTERNS = [ /^test\/vitest\/vitest\.(?:config|shared\.config|scoped-config|performance-config)\.ts$/u, /^test\/helpers\//u, ]; +const PRECISE_SOURCE_TEST_TARGETS = new Map([ + [ + "test/helpers/plugins/tts-contract-suites.ts", + [ + "src/plugins/contracts/core-extension-facade-boundary.test.ts", + "src/plugins/contracts/tts.contract.test.ts", + ], + ], +]); const TOOLING_SOURCE_TEST_TARGETS = new Map([ ["scripts/changed-lanes.mjs", ["test/scripts/changed-lanes.test.ts"]], ["scripts/check-changed.mjs", ["test/scripts/changed-lanes.test.ts"]], @@ -239,6 +248,7 @@ const TOOLING_TEST_TARGETS = new Map([ ], ]); const SOURCE_TEST_TARGETS = new Map([ + ...PRECISE_SOURCE_TEST_TARGETS, ["src/agents/live-model-turn-probes.ts", ["src/agents/live-model-turn-probes.test.ts"]], [ "src/auto-reply/reply/dispatch-from-config.ts", @@ -487,15 +497,16 @@ function stripChangedArgs(args) { function shouldKeepBroadChangedRun(changedPaths) { return changedPaths.some((changedPath) => - BROAD_CHANGED_RERUN_PATTERNS.some((pattern) => pattern.test(changedPath)), + PRECISE_SOURCE_TEST_TARGETS.has(changedPath) + ? false + : BROAD_CHANGED_RERUN_PATTERNS.some((pattern) => pattern.test(changedPath)), ); } function resolveToolingChangedTestTargets(changedPaths) { const targets = []; for (const changedPath of changedPaths) { - const testTargets = - TOOLING_SOURCE_TEST_TARGETS.get(changedPath) ?? TOOLING_TEST_TARGETS.get(changedPath); + const testTargets = resolveToolingTestTargets(changedPath); if (!testTargets) { return null; } @@ -504,6 +515,10 @@ function resolveToolingChangedTestTargets(changedPaths) { return [...new Set(targets)]; } +function resolveToolingTestTargets(changedPath) { + return TOOLING_SOURCE_TEST_TARGETS.get(changedPath) ?? TOOLING_TEST_TARGETS.get(changedPath); +} + function isRoutableChangedTarget(changedPath) { if (GENERATED_CHANGED_TEST_TARGETS.has(changedPath)) { return false; @@ -530,7 +545,8 @@ export function resolveChangedTestTargetPlan(changedPaths) { return { mode: "broad", targets: [] }; } const targets = changedPaths.flatMap((changedPath) => { - const mappedTargets = SOURCE_TEST_TARGETS.get(changedPath); + const mappedTargets = + resolveToolingTestTargets(changedPath) ?? SOURCE_TEST_TARGETS.get(changedPath); if (mappedTargets) { return mappedTargets; } diff --git a/src/plugins/contracts/inventory/bundled-capability-metadata.ts b/src/plugins/contracts/inventory/bundled-capability-metadata.ts index 3bced92a5ac..403908f5a9b 100644 --- a/src/plugins/contracts/inventory/bundled-capability-metadata.ts +++ b/src/plugins/contracts/inventory/bundled-capability-metadata.ts @@ -16,6 +16,7 @@ export type BundledPluginContractSnapshot = { pluginId: string; cliBackendIds: string[]; providerIds: string[]; + providerAuthEnvVars: Record; speechProviderIds: string[]; realtimeTranscriptionProviderIds: string[]; realtimeVoiceProviderIds: string[]; @@ -47,6 +48,7 @@ export type BundledCapabilityManifest = Pick< | "cliBackends" | "contracts" | "legacyPluginIds" + | "providerAuthEnvVars" | "providers" >; @@ -98,6 +100,26 @@ function listBundledCapabilityManifests(): readonly BundledCapabilityManifest[] const BUNDLED_CAPABILITY_MANIFESTS = listBundledCapabilityManifests(); +function normalizeStringListRecord(record: unknown): Record { + if (!record || typeof record !== "object" || Array.isArray(record)) { + return {}; + } + return Object.fromEntries( + Object.entries(record) + .map( + ([key, values]) => + [ + key.trim(), + uniqueStrings(Array.isArray(values) ? values : [], (value) => + typeof value === "string" ? value.trim() : "", + ), + ] as const, + ) + .filter(([key, values]) => key && values.length > 0) + .toSorted(([left], [right]) => left.localeCompare(right)), + ); +} + export function buildBundledPluginContractSnapshot( manifest: BundledCapabilityManifest, ): BundledPluginContractSnapshot { @@ -105,6 +127,7 @@ export function buildBundledPluginContractSnapshot( pluginId: manifest.id, cliBackendIds: uniqueStrings(manifest.cliBackends, (value) => value.trim()), providerIds: uniqueStrings(manifest.providers, (value) => value.trim()), + providerAuthEnvVars: normalizeStringListRecord(manifest.providerAuthEnvVars), speechProviderIds: uniqueStrings(manifest.contracts?.speechProviders, (value) => value.trim()), realtimeTranscriptionProviderIds: uniqueStrings( manifest.contracts?.realtimeTranscriptionProviders, @@ -188,8 +211,13 @@ export const BUNDLED_AUTO_ENABLE_PROVIDER_PLUGIN_IDS = Object.fromEntries( ).toSorted(([left], [right]) => left.localeCompare(right)), ) as Readonly>; +type BundledContractIdSnapshotKey = Exclude< + keyof Omit, + "providerAuthEnvVars" +>; + export function resolveBundledContractSnapshotPluginIds( - key: keyof Omit, + key: BundledContractIdSnapshotKey, ): string[] { return BUNDLED_PLUGIN_CONTRACT_SNAPSHOTS.filter((entry) => entry[key].length > 0) .map((entry) => entry.pluginId) diff --git a/src/plugins/contracts/registry.ts b/src/plugins/contracts/registry.ts index 961e127a419..6d13d204c29 100644 --- a/src/plugins/contracts/registry.ts +++ b/src/plugins/contracts/registry.ts @@ -75,12 +75,24 @@ type ManifestContractKey = type ManifestRegistryContractKey = "webFetchProviders" | "webSearchProviders"; +function normalizeProviderAuthEnvVars( + providerAuthEnvVars: Record | undefined, +): Record { + return Object.fromEntries( + Object.entries(providerAuthEnvVars ?? {}).map(([providerId, envVars]) => [ + providerId, + uniqueStrings(envVars), + ]), + ); +} + function resolveBundledManifestContracts(): PluginRegistrationContractEntry[] { if (process.env.VITEST) { return BUNDLED_PLUGIN_CONTRACT_SNAPSHOTS.map((entry) => ({ pluginId: entry.pluginId, cliBackendIds: [...entry.cliBackendIds], providerIds: [...entry.providerIds], + providerAuthEnvVars: normalizeProviderAuthEnvVars(entry.providerAuthEnvVars), speechProviderIds: [...entry.speechProviderIds], realtimeTranscriptionProviderIds: [...entry.realtimeTranscriptionProviderIds], realtimeVoiceProviderIds: [...entry.realtimeVoiceProviderIds], @@ -118,6 +130,7 @@ function resolveBundledManifestContracts(): PluginRegistrationContractEntry[] { pluginId: plugin.id, cliBackendIds: uniqueStrings(plugin.cliBackends), providerIds: uniqueStrings(plugin.providers), + providerAuthEnvVars: normalizeProviderAuthEnvVars(plugin.providerAuthEnvVars), speechProviderIds: uniqueStrings(plugin.contracts?.speechProviders ?? []), realtimeTranscriptionProviderIds: uniqueStrings( plugin.contracts?.realtimeTranscriptionProviders ?? [], diff --git a/test/helpers/plugins/tts-contract-suites.ts b/test/helpers/plugins/tts-contract-suites.ts index 342791fb29e..e0d6ddcbbcb 100644 --- a/test/helpers/plugins/tts-contract-suites.ts +++ b/test/helpers/plugins/tts-contract-suites.ts @@ -1,6 +1,7 @@ import type { AssistantMessage } from "@mariozechner/pi-ai"; import { beforeEach, describe, expect, it, vi } from "vitest"; import type { OpenClawConfig } from "../../../src/config/config.js"; +import { BUNDLED_PLUGIN_CONTRACT_SNAPSHOTS } from "../../../src/plugins/contracts/inventory/bundled-capability-metadata.js"; import { createEmptyPluginRegistry } from "../../../src/plugins/registry-empty.js"; import { setActivePluginRegistry } from "../../../src/plugins/runtime.js"; import type { SpeechProviderPlugin } from "../../../src/plugins/types.js"; @@ -37,16 +38,12 @@ let formatTtsProviderError: TtsRuntimeModule["_test"]["formatTtsProviderError"]; let sanitizeTtsErrorForLog: TtsRuntimeModule["_test"]["sanitizeTtsErrorForLog"]; const SPEECH_PROVIDER_ENV_KEYS = [ - "ELEVENLABS_API_KEY", - "GEMINI_API_KEY", - "GOOGLE_API_KEY", - "GRADIUM_API_KEY", - "MINIMAX_API_KEY", - "OPENAI_API_KEY", - "VYDRA_API_KEY", - "XAI_API_KEY", - "XI_API_KEY", -] as const; + ...new Set( + BUNDLED_PLUGIN_CONTRACT_SNAPSHOTS.flatMap((entry) => + entry.speechProviderIds.flatMap((providerId) => entry.providerAuthEnvVars[providerId] ?? []), + ), + ), +].toSorted((left, right) => left.localeCompare(right)); function isolatedSpeechProviderEnv( overrides: Record = {}, diff --git a/test/scripts/test-projects.test.ts b/test/scripts/test-projects.test.ts index 476bbc333be..7f00db5ff7a 100644 --- a/test/scripts/test-projects.test.ts +++ b/test/scripts/test-projects.test.ts @@ -137,6 +137,17 @@ describe("scripts/test-projects changed-target routing", () => { ).toBeNull(); }); + it("routes precise plugin contract helpers without broad-running every shard", () => { + expect( + resolveChangedTargetArgs(["--changed", "origin/main"], process.cwd(), () => [ + "test/helpers/plugins/tts-contract-suites.ts", + ]), + ).toEqual([ + "src/plugins/contracts/core-extension-facade-boundary.test.ts", + "src/plugins/contracts/tts.contract.test.ts", + ]); + }); + it("keeps the broad changed run for unknown root surfaces", () => { expect( resolveChangedTargetArgs(["--changed", "origin/main"], process.cwd(), () => [