223 lines
6.0 KiB
TypeScript
223 lines
6.0 KiB
TypeScript
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<string, unknown>) {
|
|
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",
|
|
});
|
|
});
|
|
});
|