perf(test): route speech provider registry through unit-fast

This commit is contained in:
Peter Steinberger
2026-04-27 16:16:07 +01:00
parent 3120401f53
commit 8304635258
4 changed files with 115 additions and 93 deletions

View File

@@ -0,0 +1,58 @@
import type { OpenClawConfig } from "../config/types.js";
import {
buildCapabilityProviderMaps,
normalizeCapabilityProviderId,
} from "../plugins/provider-registry-shared.js";
import type { SpeechProviderPlugin } from "../plugins/types.js";
import type { SpeechProviderId } from "./provider-types.js";
export type SpeechProviderRegistryResolver = {
getProvider: (providerId: string, cfg?: OpenClawConfig) => SpeechProviderPlugin | undefined;
listProviders: (cfg?: OpenClawConfig) => SpeechProviderPlugin[];
};
export function normalizeSpeechProviderId(
providerId: string | undefined,
): SpeechProviderId | undefined {
return normalizeCapabilityProviderId(providerId);
}
export function createSpeechProviderRegistry(resolver: SpeechProviderRegistryResolver) {
const buildResolvedProviderMaps = (cfg?: OpenClawConfig) =>
buildCapabilityProviderMaps(resolver.listProviders(cfg));
const listProviders = (cfg?: OpenClawConfig): SpeechProviderPlugin[] => [
...buildResolvedProviderMaps(cfg).canonical.values(),
];
const getProvider = (
providerId: string | undefined,
cfg?: OpenClawConfig,
): SpeechProviderPlugin | undefined => {
const normalized = normalizeSpeechProviderId(providerId);
if (!normalized) {
return undefined;
}
return (
resolver.getProvider(normalized, cfg) ??
buildResolvedProviderMaps(cfg).aliases.get(normalized)
);
};
const canonicalizeProviderId = (
providerId: string | undefined,
cfg?: OpenClawConfig,
): SpeechProviderId | undefined => {
const normalized = normalizeSpeechProviderId(providerId);
if (!normalized) {
return undefined;
}
return getProvider(normalized, cfg)?.id ?? normalized;
};
return {
canonicalizeSpeechProviderId: canonicalizeProviderId,
getSpeechProvider: getProvider,
listSpeechProviders: listProviders,
};
}

View File

@@ -1,19 +1,10 @@
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import { beforeEach, describe, expect, it } from "vitest";
import type { OpenClawConfig } from "../config/types.js"; import type { OpenClawConfig } from "../config/types.js";
import type { SpeechProviderPlugin } from "../plugins/types.js"; import type { SpeechProviderPlugin } from "../plugins/types.js";
import {
const resolvePluginCapabilityProviderMock = vi.hoisted(() => vi.fn()); createSpeechProviderRegistry,
const resolvePluginCapabilityProvidersMock = vi.hoisted(() => vi.fn()); normalizeSpeechProviderId,
} from "./provider-registry-core.js";
vi.mock("../plugins/capability-provider-runtime.js", () => ({
resolvePluginCapabilityProvider: resolvePluginCapabilityProviderMock,
resolvePluginCapabilityProviders: resolvePluginCapabilityProvidersMock,
}));
let getSpeechProvider: typeof import("./provider-registry.js").getSpeechProvider;
let listSpeechProviders: typeof import("./provider-registry.js").listSpeechProviders;
let canonicalizeSpeechProviderId: typeof import("./provider-registry.js").canonicalizeSpeechProviderId;
let normalizeSpeechProviderId: typeof import("./provider-registry.js").normalizeSpeechProviderId;
function createSpeechProvider(id: string, aliases?: string[]): SpeechProviderPlugin { function createSpeechProvider(id: string, aliases?: string[]): SpeechProviderPlugin {
return { return {
@@ -31,59 +22,57 @@ function createSpeechProvider(id: string, aliases?: string[]): SpeechProviderPlu
} }
describe("speech provider registry", () => { describe("speech provider registry", () => {
beforeAll(async () => { const getProviderCalls: Array<{ providerId: string; cfg?: OpenClawConfig }> = [];
vi.resetModules(); const listProvidersCalls: Array<{ cfg?: OpenClawConfig }> = [];
({ let providers: SpeechProviderPlugin[] = [];
getSpeechProvider, let directProvider: SpeechProviderPlugin | undefined;
listSpeechProviders, let registry: ReturnType<typeof createSpeechProviderRegistry>;
canonicalizeSpeechProviderId,
normalizeSpeechProviderId,
} = await import("./provider-registry.js"));
});
beforeEach(() => { beforeEach(() => {
resolvePluginCapabilityProviderMock.mockReset(); providers = [];
resolvePluginCapabilityProviderMock.mockReturnValue(undefined); directProvider = undefined;
resolvePluginCapabilityProvidersMock.mockReset(); getProviderCalls.length = 0;
resolvePluginCapabilityProvidersMock.mockReturnValue([]); listProvidersCalls.length = 0;
registry = createSpeechProviderRegistry({
getProvider: (providerId, cfg) => {
getProviderCalls.push({ providerId, cfg });
return directProvider;
},
listProviders: (cfg) => {
listProvidersCalls.push({ cfg });
return providers;
},
});
}); });
it("lists providers from the speech capability runtime", () => { it("lists providers from the speech capability runtime", () => {
const cfg = {} as OpenClawConfig; const cfg = {} as OpenClawConfig;
resolvePluginCapabilityProvidersMock.mockReturnValue([createSpeechProvider("demo-speech")]); providers = [createSpeechProvider("demo-speech")];
expect(listSpeechProviders(cfg).map((provider) => provider.id)).toEqual(["demo-speech"]); expect(registry.listSpeechProviders(cfg).map((provider) => provider.id)).toEqual([
expect(resolvePluginCapabilityProvidersMock).toHaveBeenCalledWith({ "demo-speech",
key: "speechProviders", ]);
cfg, expect(listProvidersCalls).toEqual([{ cfg }]);
});
}); });
it("gets providers by normalized id through the capability runtime", () => { it("gets providers by normalized id through the capability runtime", () => {
const cfg = {} as OpenClawConfig; const cfg = {} as OpenClawConfig;
const provider = createSpeechProvider("microsoft", ["edge"]); directProvider = createSpeechProvider("microsoft", ["edge"]);
resolvePluginCapabilityProviderMock.mockReturnValue(provider);
expect(getSpeechProvider(" MICROSOFT ", cfg)).toBe(provider); expect(registry.getSpeechProvider(" MICROSOFT ", cfg)).toBe(directProvider);
expect(resolvePluginCapabilityProviderMock).toHaveBeenCalledWith({ expect(getProviderCalls).toEqual([{ providerId: "microsoft", cfg }]);
key: "speechProviders",
providerId: "microsoft",
cfg,
});
}); });
it("canonicalizes aliases from listed providers when direct lookup misses", () => { it("canonicalizes aliases from listed providers when direct lookup misses", () => {
resolvePluginCapabilityProvidersMock.mockReturnValue([ providers = [createSpeechProvider("microsoft", ["edge"])];
createSpeechProvider("microsoft", ["edge"]),
]);
expect(normalizeSpeechProviderId("edge")).toBe("edge"); expect(normalizeSpeechProviderId("edge")).toBe("edge");
expect(canonicalizeSpeechProviderId("edge")).toBe("microsoft"); expect(registry.canonicalizeSpeechProviderId("edge")).toBe("microsoft");
}); });
it("returns empty results when the capability runtime has no speech providers", () => { it("returns empty results when the capability runtime has no speech providers", () => {
expect(listSpeechProviders()).toEqual([]); expect(registry.listSpeechProviders()).toEqual([]);
expect(getSpeechProvider("demo-speech")).toBeUndefined(); expect(registry.getSpeechProvider("demo-speech")).toBeUndefined();
expect(canonicalizeSpeechProviderId("demo-speech")).toBe("demo-speech"); expect(registry.canonicalizeSpeechProviderId("demo-speech")).toBe("demo-speech");
}); });
}); });

View File

@@ -3,18 +3,12 @@ import {
resolvePluginCapabilityProvider, resolvePluginCapabilityProvider,
resolvePluginCapabilityProviders, resolvePluginCapabilityProviders,
} from "../plugins/capability-provider-runtime.js"; } from "../plugins/capability-provider-runtime.js";
import {
buildCapabilityProviderMaps,
normalizeCapabilityProviderId,
} from "../plugins/provider-registry-shared.js";
import type { SpeechProviderPlugin } from "../plugins/types.js"; import type { SpeechProviderPlugin } from "../plugins/types.js";
import type { SpeechProviderId } from "./provider-types.js"; export { normalizeSpeechProviderId } from "./provider-registry-core.js";
import {
export function normalizeSpeechProviderId( createSpeechProviderRegistry,
providerId: string | undefined, type SpeechProviderRegistryResolver,
): SpeechProviderId | undefined { } from "./provider-registry-core.js";
return normalizeCapabilityProviderId(providerId);
}
function resolveSpeechProviderPluginEntries(cfg?: OpenClawConfig): SpeechProviderPlugin[] { function resolveSpeechProviderPluginEntries(cfg?: OpenClawConfig): SpeechProviderPlugin[] {
return resolvePluginCapabilityProviders({ return resolvePluginCapabilityProviders({
@@ -23,41 +17,21 @@ function resolveSpeechProviderPluginEntries(cfg?: OpenClawConfig): SpeechProvide
}); });
} }
function buildProviderMaps(cfg?: OpenClawConfig): { const defaultSpeechProviderRegistryResolver: SpeechProviderRegistryResolver = {
canonical: Map<string, SpeechProviderPlugin>; getProvider: (providerId, cfg) =>
aliases: Map<string, SpeechProviderPlugin>;
} {
return buildCapabilityProviderMaps(resolveSpeechProviderPluginEntries(cfg));
}
export function listSpeechProviders(cfg?: OpenClawConfig): SpeechProviderPlugin[] {
return [...buildProviderMaps(cfg).canonical.values()];
}
export function getSpeechProvider(
providerId: string | undefined,
cfg?: OpenClawConfig,
): SpeechProviderPlugin | undefined {
const normalized = normalizeSpeechProviderId(providerId);
if (!normalized) {
return undefined;
}
return (
resolvePluginCapabilityProvider({ resolvePluginCapabilityProvider({
key: "speechProviders", key: "speechProviders",
providerId: normalized, providerId,
cfg, cfg,
}) ?? buildProviderMaps(cfg).aliases.get(normalized) }),
); listProviders: resolveSpeechProviderPluginEntries,
} };
export function canonicalizeSpeechProviderId( const defaultSpeechProviderRegistry = createSpeechProviderRegistry(
providerId: string | undefined, defaultSpeechProviderRegistryResolver,
cfg?: OpenClawConfig, );
): SpeechProviderId | undefined {
const normalized = normalizeSpeechProviderId(providerId); export const listSpeechProviders = defaultSpeechProviderRegistry.listSpeechProviders;
if (!normalized) { export const getSpeechProvider = defaultSpeechProviderRegistry.getSpeechProvider;
return undefined; export const canonicalizeSpeechProviderId =
} defaultSpeechProviderRegistry.canonicalizeSpeechProviderId;
return getSpeechProvider(normalized, cfg)?.id ?? normalized;
}

View File

@@ -93,6 +93,7 @@ export const forcedUnitFastTestFiles = [
"src/security/windows-acl.test.ts", "src/security/windows-acl.test.ts",
"src/realtime-transcription/websocket-session.test.ts", "src/realtime-transcription/websocket-session.test.ts",
"src/trajectory/export.test.ts", "src/trajectory/export.test.ts",
"src/tts/provider-registry.test.ts",
"src/tts/status-config.test.ts", "src/tts/status-config.test.ts",
"src/version.test.ts", "src/version.test.ts",
]; ];