

























@@ -152,6 +152,28 @@ function createTtsConfig(prefsName: string): OpenClawConfig {
152152};
153153}
154154155+function requireRecord(value: unknown, label: string): Record<string, unknown> {
156+if (value === null || typeof value !== "object" || Array.isArray(value)) {
157+throw new Error(`expected ${label} to be a record`);
158+}
159+return value as Record<string, unknown>;
160+}
161+162+function requireFirstCallParam(calls: ReadonlyArray<readonly unknown[]>, label: string) {
163+const call = calls[0];
164+if (!call) {
165+throw new Error(`expected ${label} call`);
166+}
167+return call[0];
168+}
169+170+function requireAttempt(attempts: unknown[] | undefined, index: number) {
171+if (!attempts) {
172+throw new Error("expected synthesis attempts");
173+}
174+return requireRecord(attempts[index], `synthesis attempt ${index}`);
175+}
176+155177async function expectTtsPayloadResult(params: {
156178channel: string;
157179prefsName: string;
@@ -174,7 +196,12 @@ async function expectTtsPayloadResult(params: {
174196kind: "final",
175197});
176198177-expect(synthesizeMock).toHaveBeenCalledWith(expect.objectContaining({ target: params.target }));
199+expect(synthesizeMock).toHaveBeenCalled();
200+const request = requireRecord(
201+synthesizeMock.mock.calls.at(-1)?.[0],
202+"latest synthesis request",
203+);
204+expect(request.target).toBe(params.target);
178205expect(result.audioAsVoice).toBe(params.audioAsVoice);
179206expect(result.mediaUrl).toMatch(new RegExp(`voice-\\d+\\.${params.mediaExtension ?? "ogg"}$`));
180207expect(result.spokenText).toBe(params.text);
@@ -261,9 +288,13 @@ describe("speech-core native voice-note routing", () => {
261288voiceCompatible: false,
262289},
263290});
264-expect(transcodeAudioBufferMock).toHaveBeenCalledWith(
265-expect.objectContaining({ sourceExtension: "mp3", targetExtension: "caf" }),
291+expect(transcodeAudioBufferMock).toHaveBeenCalledOnce();
292+const transcodeRequest = requireRecord(
293+requireFirstCallParam(transcodeAudioBufferMock.mock.calls as unknown[][], "transcode"),
294+"transcode request",
266295);
296+expect(transcodeRequest.sourceExtension).toBe("mp3");
297+expect(transcodeRequest.targetExtension).toBe("caf");
267298});
268299269300it("falls back to the original mp3 buffer when the host transcoder fails", async () => {
@@ -339,14 +370,14 @@ describe("speech-core native voice-note routing", () => {
339370});
340371341372expect(result.success).toBe(true);
342-expect(synthesizeMock).toHaveBeenCalledWith(
343-expect.objectContaining({
344-cfg: runtimeConfig,
345-providerConfig: expect.objectContaining({
346-apiKey: "resolved-minimax-key",
347-}),
348-}),
373+expect(synthesizeMock).toHaveBeenCalled();
374+const request = requireRecord(
375+synthesizeMock.mock.calls[0]?.[0],
376+"runtime snapshot synthesis request",
349377);
378+expect(request.cfg).toBe(runtimeConfig);
379+const providerConfig = requireRecord(request.providerConfig, "provider config");
380+expect(providerConfig.apiKey).toBe("resolved-minimax-key");
350381});
351382352383it.each(["feishu", "whatsapp"] as const)(
@@ -394,7 +425,9 @@ describe("speech-core native voice-note routing", () => {
394425kind: "final",
395426});
396427397-expect(synthesizeMock).toHaveBeenCalledWith(expect.objectContaining({ text: "hello" }));
428+expect(synthesizeMock).toHaveBeenCalled();
429+const request = requireRecord(synthesizeMock.mock.calls[0]?.[0], "hidden TTS request");
430+expect(request.text).toBe("hello");
398431expect(result.mediaUrl).toMatch(/voice-\d+\.ogg$/);
399432expect(result.audioAsVoice).toBe(true);
400433expect(result.text).toBeUndefined();
@@ -513,15 +546,12 @@ describe("speech-core native voice-note routing", () => {
513546kind: "final",
514547});
515548516-expect(synthesizeMock).toHaveBeenCalledWith(
517-expect.objectContaining({
518-providerConfig: expect.objectContaining({
519-model: "base-model",
520-voice: "persona-voice",
521-style: "dry",
522-}),
523-}),
524-);
549+expect(synthesizeMock).toHaveBeenCalled();
550+const request = requireRecord(synthesizeMock.mock.calls[0]?.[0], "persona synthesis request");
551+const providerConfig = requireRecord(request.providerConfig, "persona provider config");
552+expect(providerConfig.model).toBe("base-model");
553+expect(providerConfig.voice).toBe("persona-voice");
554+expect(providerConfig.style).toBe("dry");
525555expect(result.mediaUrl).toMatch(/voice-\d+\.ogg$/);
526556527557mediaDir = result.mediaUrl ? path.dirname(result.mediaUrl) : undefined;
@@ -556,13 +586,12 @@ describe("speech-core native voice-note routing", () => {
556586});
557587558588expect(result.success).toBe(true);
559-expect(result.attempts?.[0]).toMatchObject({
560-provider: "missing",
561-outcome: "skipped",
562-reasonCode: "no_provider_registered",
563-persona: "alfred",
564-});
565-expect(result.attempts?.[0]).not.toHaveProperty("personaBinding");
589+const attempt = requireAttempt(result.attempts, 0);
590+expect(attempt.provider).toBe("missing");
591+expect(attempt.outcome).toBe("skipped");
592+expect(attempt.reasonCode).toBe("no_provider_registered");
593+expect(attempt.persona).toBe("alfred");
594+expect(attempt).not.toHaveProperty("personaBinding");
566595});
567596568597it("does not mark skipped telephony providers as missing persona bindings", async () => {
@@ -589,13 +618,12 @@ describe("speech-core native voice-note routing", () => {
589618});
590619591620expect(result.success).toBe(false);
592-expect(result.attempts?.[0]).toMatchObject({
593-provider: "mock",
594-outcome: "skipped",
595-reasonCode: "unsupported_for_telephony",
596-persona: "alfred",
597-});
598-expect(result.attempts?.[0]).not.toHaveProperty("personaBinding");
621+const attempt = requireAttempt(result.attempts, 0);
622+expect(attempt.provider).toBe("mock");
623+expect(attempt.outcome).toBe("skipped");
624+expect(attempt.reasonCode).toBe("unsupported_for_telephony");
625+expect(attempt.persona).toBe("alfred");
626+expect(attempt).not.toHaveProperty("personaBinding");
599627});
600628601629it("passes directive overrides to telephony synthesis providers", async () => {
@@ -638,13 +666,12 @@ describe("speech-core native voice-note routing", () => {
638666expect(result.success).toBe(true);
639667expect(result.providerModel).toBe("telephony-model");
640668expect(result.providerVoice).toBe("directed-voice");
641-expect(synthesizeTelephony).toHaveBeenCalledWith(
642-expect.objectContaining({
643-providerOverrides: {
644-voice: "directed-voice",
645-},
646-}),
669+expect(synthesizeTelephony).toHaveBeenCalledOnce();
670+const telephonyRequest = requireRecord(
671+requireFirstCallParam(synthesizeTelephony.mock.calls, "telephony synthesis"),
672+"telephony synthesis request",
647673);
674+expect(telephonyRequest.providerOverrides).toEqual({ voice: "directed-voice" });
648675});
649676650677it("uses provider defaults when fallback policy allows missing persona bindings", async () => {
@@ -669,12 +696,13 @@ describe("speech-core native voice-note routing", () => {
669696},
670697});
671698672-expect(prepareSynthesisMock).toHaveBeenCalledWith(
673-expect.objectContaining({
674-persona: undefined,
675-personaProviderConfig: undefined,
676-}),
699+expect(prepareSynthesisMock).toHaveBeenCalledOnce();
700+const prepareContext = requireRecord(
701+requireFirstCallParam(prepareSynthesisMock.mock.calls, "prepare synthesis"),
702+"prepare synthesis context",
677703);
704+expect(prepareContext.persona).toBeUndefined();
705+expect(prepareContext.personaProviderConfig).toBeUndefined();
678706});
679707680708it("preserves persona prompts by default when provider bindings are missing", async () => {
@@ -698,12 +726,14 @@ describe("speech-core native voice-note routing", () => {
698726},
699727});
700728701-expect(prepareSynthesisMock).toHaveBeenCalledWith(
702-expect.objectContaining({
703-persona: expect.objectContaining({ id: "alfred" }),
704-personaProviderConfig: undefined,
705-}),
729+expect(prepareSynthesisMock).toHaveBeenCalledOnce();
730+const prepareContext = requireRecord(
731+requireFirstCallParam(prepareSynthesisMock.mock.calls, "prepare synthesis"),
732+"prepare synthesis context",
706733);
734+const persona = requireRecord(prepareContext.persona, "prepare synthesis persona");
735+expect(persona.id).toBe("alfred");
736+expect(prepareContext.personaProviderConfig).toBeUndefined();
707737});
708738709739it("skips unbound providers under fail policy while allowing bound fallbacks", async () => {
@@ -738,20 +768,18 @@ describe("speech-core native voice-note routing", () => {
738768expect(result.success).toBe(true);
739769expect(result.provider).toBe("fallback");
740770expect(result.fallbackFrom).toBe("mock");
741-expect(result.attempts?.[0]).toMatchObject({
742-provider: "mock",
743-outcome: "skipped",
744-reasonCode: "not_configured",
745-persona: "alfred",
746-personaBinding: "missing",
747-error: "mock: persona alfred has no provider binding",
748-});
749-expect(result.attempts?.[1]).toMatchObject({
750-provider: "fallback",
751-outcome: "success",
752-persona: "alfred",
753-personaBinding: "applied",
754-});
771+const skippedAttempt = requireAttempt(result.attempts, 0);
772+expect(skippedAttempt.provider).toBe("mock");
773+expect(skippedAttempt.outcome).toBe("skipped");
774+expect(skippedAttempt.reasonCode).toBe("not_configured");
775+expect(skippedAttempt.persona).toBe("alfred");
776+expect(skippedAttempt.personaBinding).toBe("missing");
777+expect(skippedAttempt.error).toBe("mock: persona alfred has no provider binding");
778+const successAttempt = requireAttempt(result.attempts, 1);
779+expect(successAttempt.provider).toBe("fallback");
780+expect(successAttempt.outcome).toBe("success");
781+expect(successAttempt.persona).toBe("alfred");
782+expect(successAttempt.personaBinding).toBe("applied");
755783});
756784});
757785@@ -790,17 +818,14 @@ describe("speech-core per-agent TTS config", () => {
790818791819const resolved = resolveTtsConfig(cfg, "reader");
792820793-expect(resolved.rawConfig).toMatchObject({
794-enabled: true,
795-provider: "openai",
796-providers: {
797-openai: {
798-apiKey: "${OPENAI_API_KEY}",
799-voice: "nova",
800-speed: 1,
801-},
802-},
803-});
821+const rawConfig = requireRecord(resolved.rawConfig, "resolved raw TTS config");
822+expect(rawConfig.enabled).toBe(true);
823+expect(rawConfig.provider).toBe("openai");
824+const providers = requireRecord(rawConfig.providers, "resolved raw TTS providers");
825+const openai = requireRecord(providers.openai, "resolved OpenAI TTS provider config");
826+expect(openai.apiKey).toBe("${OPENAI_API_KEY}");
827+expect(openai.voice).toBe("nova");
828+expect(openai.speed).toBe(1);
804829});
805830806831it("composes per-agent TTS overrides with active persona bindings", async () => {
@@ -863,15 +888,15 @@ describe("speech-core per-agent TTS config", () => {
863888agentId: "reader",
864889});
865890866-expect(synthesizeMock).toHaveBeenCalledWith(
867-expect.objectContaining({
868-providerConfig: expect.objectContaining({
869-model: "base-model",
870-voice: "agent-voice",
871-style: "jarvis-style",
872-}),
873-}),
891+expect(synthesizeMock).toHaveBeenCalled();
892+const request = requireRecord(
893+synthesizeMock.mock.calls[0]?.[0],
894+"agent persona synthesis request",
874895);
896+const providerConfig = requireRecord(request.providerConfig, "agent persona provider config");
897+expect(providerConfig.model).toBe("base-model");
898+expect(providerConfig.voice).toBe("agent-voice");
899+expect(providerConfig.style).toBe("jarvis-style");
875900expect(result.mediaUrl).toMatch(/voice-\d+\.ogg$/);
876901mediaDir = result.mediaUrl ? path.dirname(result.mediaUrl) : undefined;
877902} finally {
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。