























@@ -12,13 +12,26 @@ function createEmptyMockManifestRegistry(): MockManifestRegistry {
1212}
13131414const mocks = vi.hoisted(() => ({
15+createMockRegistry: () => ({
16+plugins: [],
17+diagnostics: [],
18+memoryEmbeddingProviders: [],
19+speechProviders: [],
20+realtimeTranscriptionProviders: [],
21+realtimeVoiceProviders: [],
22+mediaUnderstandingProviders: [],
23+imageGenerationProviders: [],
24+videoGenerationProviders: [],
25+musicGenerationProviders: [],
26+}),
1527resolveRuntimePluginRegistry: vi.fn<
1628(params?: unknown) => ReturnType<typeof createEmptyPluginRegistry> | undefined
1729>(() => undefined),
1830resolvePluginRegistryLoadCacheKey: vi.fn((options: unknown) => JSON.stringify(options)),
1931loadPluginManifestRegistry: vi.fn<(params?: Record<string, unknown>) => MockManifestRegistry>(
2032() => createEmptyMockManifestRegistry(),
2133),
34+loadBundledCapabilityRuntimeRegistry: vi.fn(),
2235loadPluginRegistrySnapshot: vi.fn(() => ({ plugins: [] })),
2336withBundledPluginAllowlistCompat: vi.fn(
2437({ config, pluginIds }: { config?: OpenClawConfig; pluginIds: string[] }) =>
@@ -39,6 +52,10 @@ vi.mock("./loader.js", () => ({
3952resolvePluginRegistryLoadCacheKey: mocks.resolvePluginRegistryLoadCacheKey,
4053}));
415455+vi.mock("./bundled-capability-runtime.js", () => ({
56+loadBundledCapabilityRuntimeRegistry: mocks.loadBundledCapabilityRuntimeRegistry,
57+}));
58+4259vi.mock("./manifest-registry-installed.js", () => ({
4360loadPluginManifestRegistryForInstalledIndex: mocks.loadPluginManifestRegistry,
4461}));
@@ -191,6 +208,8 @@ describe("resolvePluginCapabilityProviders", () => {
191208mocks.loadPluginRegistrySnapshot.mockReturnValue({ plugins: [] });
192209mocks.loadPluginManifestRegistry.mockReset();
193210mocks.loadPluginManifestRegistry.mockReturnValue(createEmptyMockManifestRegistry());
211+mocks.loadBundledCapabilityRuntimeRegistry.mockReset();
212+mocks.loadBundledCapabilityRuntimeRegistry.mockImplementation(() => mocks.createMockRegistry());
194213mocks.withBundledPluginAllowlistCompat.mockClear();
195214mocks.withBundledPluginAllowlistCompat.mockImplementation(
196215({ config, pluginIds }: { config?: OpenClawConfig; pluginIds: string[] }) =>
@@ -490,6 +509,169 @@ describe("resolvePluginCapabilityProviders", () => {
490509});
491510});
492511512+it("uses bundled capability capture when runtime snapshot is empty for a requested speech provider", () => {
513+const active = createEmptyPluginRegistry();
514+active.speechProviders.push({
515+pluginId: "openai",
516+pluginName: "openai",
517+source: "test",
518+provider: {
519+id: "openai",
520+label: "openai",
521+isConfigured: () => true,
522+synthesize: async () => ({
523+audioBuffer: Buffer.from("x"),
524+outputFormat: "mp3",
525+voiceCompatible: false,
526+fileExtension: ".mp3",
527+}),
528+},
529+} as never);
530+const captured = createEmptyPluginRegistry();
531+captured.speechProviders.push({
532+pluginId: "google",
533+pluginName: "google",
534+source: "test",
535+provider: {
536+id: "google",
537+label: "google",
538+isConfigured: () => true,
539+synthesize: async () => ({
540+audioBuffer: Buffer.from("x"),
541+outputFormat: "mp3",
542+voiceCompatible: false,
543+fileExtension: ".mp3",
544+}),
545+},
546+} as never);
547+mocks.loadPluginManifestRegistry.mockReturnValue({
548+plugins: [
549+{
550+id: "google",
551+origin: "bundled",
552+contracts: { speechProviders: ["google"] },
553+},
554+{
555+id: "microsoft",
556+origin: "bundled",
557+contracts: { speechProviders: ["microsoft"] },
558+},
559+] as never,
560+diagnostics: [],
561+});
562+mocks.resolveRuntimePluginRegistry.mockImplementation((params?: unknown) =>
563+params === undefined ? active : createEmptyPluginRegistry(),
564+);
565+mocks.loadBundledCapabilityRuntimeRegistry.mockReturnValue(captured);
566+567+const providers = resolvePluginCapabilityProviders({
568+key: "speechProviders",
569+cfg: {
570+messages: { tts: { provider: "google" } },
571+} as OpenClawConfig,
572+});
573+574+expectResolvedCapabilityProviderIds(providers, ["openai", "google"]);
575+expect(mocks.resolveRuntimePluginRegistry).toHaveBeenCalledWith({
576+config: expect.anything(),
577+onlyPluginIds: ["google"],
578+activate: false,
579+installBundledRuntimeDeps: false,
580+});
581+expect(mocks.loadBundledCapabilityRuntimeRegistry).toHaveBeenCalledWith({
582+pluginIds: ["google"],
583+env: process.env,
584+pluginSdkResolution: undefined,
585+});
586+});
587+588+it("uses bundled capability capture when runtime snapshot misses a requested speech provider", () => {
589+const active = createEmptyPluginRegistry();
590+active.speechProviders.push({
591+pluginId: "openai",
592+pluginName: "openai",
593+source: "test",
594+provider: {
595+id: "openai",
596+label: "openai",
597+isConfigured: () => true,
598+synthesize: async () => ({
599+audioBuffer: Buffer.from("x"),
600+outputFormat: "mp3",
601+voiceCompatible: false,
602+fileExtension: ".mp3",
603+}),
604+},
605+} as never);
606+const loaded = createEmptyPluginRegistry();
607+loaded.speechProviders.push({
608+pluginId: "azure-speech",
609+pluginName: "azure-speech",
610+source: "test",
611+provider: {
612+id: "azure-speech",
613+label: "Azure Speech",
614+isConfigured: () => true,
615+synthesize: async () => ({
616+audioBuffer: Buffer.from("x"),
617+outputFormat: "mp3",
618+voiceCompatible: false,
619+fileExtension: ".mp3",
620+}),
621+},
622+} as never);
623+const captured = createEmptyPluginRegistry();
624+captured.speechProviders.push({
625+pluginId: "google",
626+pluginName: "google",
627+source: "test",
628+provider: {
629+id: "google",
630+label: "google",
631+isConfigured: () => true,
632+synthesize: async () => ({
633+audioBuffer: Buffer.from("x"),
634+outputFormat: "mp3",
635+voiceCompatible: false,
636+fileExtension: ".mp3",
637+}),
638+},
639+} as never);
640+mocks.loadPluginManifestRegistry.mockReturnValue({
641+plugins: [
642+{
643+id: "azure-speech",
644+origin: "bundled",
645+contracts: { speechProviders: ["azure-speech"] },
646+},
647+{
648+id: "google",
649+origin: "bundled",
650+contracts: { speechProviders: ["google"] },
651+},
652+] as never,
653+diagnostics: [],
654+});
655+mocks.resolveRuntimePluginRegistry.mockImplementation((params?: unknown) =>
656+params === undefined ? active : loaded,
657+);
658+mocks.loadBundledCapabilityRuntimeRegistry.mockReturnValue(captured);
659+660+const providers = resolvePluginCapabilityProviders({
661+key: "speechProviders",
662+cfg: {
663+messages: { tts: { provider: "google" } },
664+} as OpenClawConfig,
665+});
666+667+expectResolvedCapabilityProviderIds(providers, ["openai", "google"]);
668+expect(mocks.loadBundledCapabilityRuntimeRegistry).toHaveBeenCalledWith({
669+pluginIds: ["google"],
670+env: process.env,
671+pluginSdkResolution: undefined,
672+});
673+});
674+493675it("does not merge unrelated bundled capability providers when cfg requests one provider", () => {
494676const active = createEmptyPluginRegistry();
495677active.speechProviders.push({
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。