import { describe, expect, it, vi } from "vitest"; import type { OpenClawPluginCommandDefinition } from "../../test/helpers/extensions/plugin-command.js"; import { createPluginRuntimeMock } from "../../test/helpers/extensions/plugin-runtime-mock.js"; import register from "./index.js"; function createHarness(config: Record) { let command: OpenClawPluginCommandDefinition | undefined; const runtime = createPluginRuntimeMock({ config: { loadConfig: vi.fn(() => config), writeConfigFile: vi.fn().mockResolvedValue(undefined), }, tts: { listVoices: vi.fn(), }, }); const api = { runtime, registerCommand: vi.fn((definition: OpenClawPluginCommandDefinition) => { command = definition; }), }; register.register(api as never); if (!command) { throw new Error("talk-voice command not registered"); } return { command, runtime }; } function createCommandContext(args: string, channel: string = "discord") { return { args, channel, channelId: channel, isAuthorizedSender: true, commandBody: args ? `/voice ${args}` : "/voice", config: {}, requestConversationBinding: vi.fn(), detachConversationBinding: vi.fn(), getCurrentConversationBinding: vi.fn(), }; } describe("talk-voice plugin", () => { it("reports active provider status", async () => { const { command } = createHarness({ talk: { provider: "microsoft", providers: { microsoft: { voiceId: "en-US-AvaNeural", apiKey: "secret-token", }, }, }, }); const result = await command.handler(createCommandContext("")); expect(result).toEqual({ text: "Talk voice status:\n" + "- provider: microsoft\n" + "- talk.voiceId: en-US-AvaNeural\n" + "- microsoft.apiKey: secret…", }); }); it("lists voices from the active provider", async () => { const { command, runtime } = createHarness({ talk: { provider: "elevenlabs", providers: { elevenlabs: { apiKey: "sk-eleven", baseUrl: "https://voices.example.test", }, }, }, }); vi.mocked(runtime.tts.listVoices).mockResolvedValue([ { id: "voice-a", name: "Claudia", category: "general" }, { id: "voice-b", name: "Bert" }, ]); const result = await command.handler(createCommandContext("list 1")); expect(runtime.tts.listVoices).toHaveBeenCalledWith({ provider: "elevenlabs", cfg: { talk: { provider: "elevenlabs", providers: { elevenlabs: { apiKey: "sk-eleven", baseUrl: "https://voices.example.test", }, }, }, }, apiKey: "sk-eleven", baseUrl: "https://voices.example.test", }); expect(result).toEqual({ text: "ElevenLabs voices: 2\n\n" + "- Claudia · general\n" + " id: voice-a\n\n" + "(showing first 1)", }); }); it("surfaces richer provider voice metadata when available", async () => { const { command, runtime } = createHarness({ talk: { provider: "microsoft", providers: { microsoft: {}, }, }, }); vi.mocked(runtime.tts.listVoices).mockResolvedValue([ { id: "en-US-AvaNeural", name: "Ava", category: "General", locale: "en-US", gender: "Female", personalities: ["Friendly", "Positive"], description: "Friendly, Positive", }, ]); const result = await command.handler(createCommandContext("list")); expect(result).toEqual({ text: "Microsoft voices: 1\n\n" + "- Ava · General\n" + " id: en-US-AvaNeural\n" + " meta: en-US · Female · Friendly, Positive\n" + " note: Friendly, Positive", }); }); it("writes canonical talk provider config and legacy elevenlabs voice id", async () => { const { command, runtime } = createHarness({ talk: { provider: "elevenlabs", providers: { elevenlabs: { apiKey: "sk-eleven", }, }, }, }); vi.mocked(runtime.tts.listVoices).mockResolvedValue([{ id: "voice-a", name: "Claudia" }]); const result = await command.handler(createCommandContext("set Claudia")); expect(runtime.config.writeConfigFile).toHaveBeenCalledWith({ talk: { provider: "elevenlabs", providers: { elevenlabs: { apiKey: "sk-eleven", voiceId: "voice-a", }, }, voiceId: "voice-a", }, }); expect(result).toEqual({ text: "✅ ElevenLabs Talk voice set to Claudia\nvoice-a", }); }); it("writes provider voice id without legacy top-level field for microsoft", async () => { const { command, runtime } = createHarness({ talk: { provider: "microsoft", providers: { microsoft: {}, }, }, }); vi.mocked(runtime.tts.listVoices).mockResolvedValue([{ id: "en-US-AvaNeural", name: "Ava" }]); await command.handler(createCommandContext("set Ava")); expect(runtime.config.writeConfigFile).toHaveBeenCalledWith({ talk: { provider: "microsoft", providers: { microsoft: { voiceId: "en-US-AvaNeural", }, }, }, }); }); it("returns provider lookup errors cleanly", async () => { const { command, runtime } = createHarness({ talk: { provider: "microsoft", providers: { microsoft: {}, }, }, }); vi.mocked(runtime.tts.listVoices).mockRejectedValue( new Error("speech provider microsoft does not support voice listing"), ); const result = await command.handler(createCommandContext("list")); expect(result).toEqual({ text: "Microsoft voice list failed: speech provider microsoft does not support voice listing", }); }); });