import { beforeEach, describe, expect, it, vi } from "vitest" ;
import type { OpenClawConfig } from "../../config/config.js" ;
import { talkHandlers } from "./talk.js" ;
const mocks = vi.hoisted(() => ({
loadConfig: vi.fn<() => OpenClawConfig>(),
readConfigFileSnapshot: vi.fn(),
canonicalizeSpeechProviderId: vi.fn((providerId: string | undefined) => providerId),
getSpeechProvider: vi.fn(),
synthesizeSpeech: vi.fn(),
}));
vi.mock("../../config/config.js" , () => ({
loadConfig: mocks.loadConfig,
readConfigFileSnapshot: mocks.readConfigFileSnapshot,
}));
vi.mock("../../tts/provider-registry.js" , () => ({
canonicalizeSpeechProviderId: mocks.canonicalizeSpeechProviderId,
getSpeechProvider: mocks.getSpeechProvider,
}));
vi.mock("../../tts/tts.js" , () => ({
synthesizeSpeech: mocks.synthesizeSpeech,
}));
function createTalkConfig(apiKey: unknown): OpenClawConfig {
return {
talk: {
provider: "acme" ,
providers: {
acme: {
apiKey,
voiceId: "stub-default-voice" ,
},
},
},
} as OpenClawConfig;
}
describe("talk.speak handler" , () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("uses the active runtime config snapshot instead of the raw config snapshot" , async () => {
const runtimeConfig = createTalkConfig("env-acme-key" );
const diskConfig = createTalkConfig({
source: "env" ,
provider: "default" ,
id: "ACME_SPEECH_API_KEY" ,
});
mocks.loadConfig.mockReturnValue(runtimeConfig);
mocks.readConfigFileSnapshot.mockResolvedValue({
path: "/tmp/openclaw.json" ,
hash: "test-hash" ,
valid: true ,
config: diskConfig,
});
mocks.getSpeechProvider.mockReturnValue({
id: "acme" ,
label: "Acme Speech" ,
resolveTalkConfig: ({
talkProviderConfig,
}: {
talkProviderConfig: Record<string, unknown>;
}) => talkProviderConfig,
});
mocks.synthesizeSpeech.mockImplementation(
async ({ cfg }: { cfg: OpenClawConfig; text: string; disableFallback: boolean }) => {
expect(cfg.messages?.tts?.provider).toBe("acme" );
expect(cfg.messages?.tts?.providers?.acme?.apiKey).toBe("env-acme-key" );
return {
success: true ,
provider: "acme" ,
audioBuffer: Buffer.from([1 , 2 , 3 ]),
outputFormat: "mp3" ,
voiceCompatible: false ,
fileExtension: ".mp3" ,
};
},
);
const respond = vi.fn();
await talkHandlers["talk.speak" ]({
req: { type: "req" , id: "1" , method: "talk.speak" },
params: { text: "Hello from talk mode." },
client: null ,
isWebchatConnect: () => false ,
respond: respond as never,
context: {} as never,
});
expect(mocks.loadConfig).toHaveBeenCalledTimes(1 );
expect(mocks.readConfigFileSnapshot).not.toHaveBeenCalled();
expect(mocks.synthesizeSpeech).toHaveBeenCalledWith(
expect.objectContaining({
text: "Hello from talk mode." ,
disableFallback: true ,
}),
);
expect(respond).toHaveBeenCalledWith(
true ,
expect.objectContaining({
provider: "acme" ,
audioBase64: Buffer.from([1 , 2 , 3 ]).toString("base64" ),
outputFormat: "mp3" ,
mimeType: "audio/mpeg" ,
fileExtension: ".mp3" ,
}),
undefined,
);
});
});
Messung V0.5 in Prozent C=98 H=98 G=97
¤ Dauer der Verarbeitung: 0.12 Sekunden
(vorverarbeitet am 2026-06-10)
¤
*© Formatika GbR, Deutschland