test(plugins): route tts contract helper changes narrowly

This commit is contained in:
Peter Steinberger
2026-04-25 06:04:53 +01:00
parent b79272baad
commit a2a49b430c
5 changed files with 80 additions and 15 deletions

View File

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

View File

@@ -16,6 +16,7 @@ export type BundledPluginContractSnapshot = {
pluginId: string;
cliBackendIds: string[];
providerIds: string[];
providerAuthEnvVars: Record<string, string[]>;
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<string, string[]> {
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<Record<string, string>>;
type BundledContractIdSnapshotKey = Exclude<
keyof Omit<BundledPluginContractSnapshot, "pluginId">,
"providerAuthEnvVars"
>;
export function resolveBundledContractSnapshotPluginIds(
key: keyof Omit<BundledPluginContractSnapshot, "pluginId">,
key: BundledContractIdSnapshotKey,
): string[] {
return BUNDLED_PLUGIN_CONTRACT_SNAPSHOTS.filter((entry) => entry[key].length > 0)
.map((entry) => entry.pluginId)

View File

@@ -75,12 +75,24 @@ type ManifestContractKey =
type ManifestRegistryContractKey = "webFetchProviders" | "webSearchProviders";
function normalizeProviderAuthEnvVars(
providerAuthEnvVars: Record<string, string[]> | undefined,
): Record<string, string[]> {
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 ?? [],

View File

@@ -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<string, string | undefined> = {},

View File

@@ -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(), () => [