




























@@ -10,9 +10,11 @@ const {
1010 joinVoiceChannelMock,
1111 entersStateMock,
1212 createAudioPlayerMock,
13+ createAudioResourceMock,
1314 resolveAgentRouteMock,
1415 agentCommandMock,
1516 transcribeAudioFileMock,
17+ textToSpeechStreamMock,
1618 textToSpeechMock,
1719} = vi.hoisted(() => {
1820type EventHandler = (...args: unknown[]) => unknown;
@@ -94,6 +96,7 @@ const {
9496entersStateMock: vi.fn(async (_target?: unknown, _state?: string, _timeoutMs?: number) => {
9597return undefined;
9698}),
99+createAudioResourceMock: vi.fn(),
97100createAudioPlayerMock: vi.fn(() => ({
98101on: vi.fn(),
99102off: vi.fn(),
@@ -104,6 +107,9 @@ const {
104107resolveAgentRouteMock: vi.fn(() => ({ agentId: "agent-1", sessionKey: "discord:g1:c1" })),
105108agentCommandMock: vi.fn(async (_opts?: unknown, _runtime?: unknown) => ({ payloads: [] })),
106109transcribeAudioFileMock: vi.fn(async () => ({ text: "hello from voice" })),
110+textToSpeechStreamMock: vi.fn(
111+async (): Promise<unknown> => ({ success: false, error: "stream unavailable" }),
112+),
107113textToSpeechMock: vi.fn(async () => ({ success: true, audioPath: "/tmp/voice.mp3" })),
108114};
109115});
@@ -121,7 +127,7 @@ vi.mock("./sdk-runtime.js", () => ({
121127Connecting: "connecting",
122128},
123129createAudioPlayer: createAudioPlayerMock,
124-createAudioResource: vi.fn(),
130+createAudioResource: createAudioResourceMock,
125131entersState: entersStateMock,
126132getVoiceConnection: getVoiceConnectionMock,
127133joinVoiceChannel: joinVoiceChannelMock,
@@ -154,6 +160,7 @@ vi.mock("../runtime.js", () => ({
154160transcribeAudioFile: transcribeAudioFileMock,
155161},
156162tts: {
163+textToSpeechStream: textToSpeechStreamMock,
157164textToSpeech: textToSpeechMock,
158165},
159166}),
@@ -207,8 +214,11 @@ describe("DiscordVoiceManager", () => {
207214agentCommandMock.mockResolvedValue({ payloads: [] });
208215transcribeAudioFileMock.mockReset();
209216transcribeAudioFileMock.mockResolvedValue({ text: "hello from voice" });
217+textToSpeechStreamMock.mockReset();
218+textToSpeechStreamMock.mockResolvedValue({ success: false, error: "stream unavailable" });
210219textToSpeechMock.mockReset();
211220textToSpeechMock.mockResolvedValue({ success: true, audioPath: "/tmp/voice.mp3" });
221+createAudioResourceMock.mockClear();
212222});
213223214224const createManager = (
@@ -750,6 +760,49 @@ describe("DiscordVoiceManager", () => {
750760);
751761});
752762763+it("plays streaming TTS audio before falling back to a synthesized file", async () => {
764+const release = vi.fn(async () => undefined);
765+textToSpeechStreamMock.mockResolvedValue({
766+success: true,
767+audioStream: new ReadableStream<Uint8Array>({
768+start(controller) {
769+controller.enqueue(new Uint8Array([1, 2, 3]));
770+controller.close();
771+},
772+}),
773+ release,
774+});
775+agentCommandMock.mockResolvedValueOnce({
776+payloads: [{ text: "hello back" }],
777+} as never);
778+779+const client = createClient();
780+client.fetchMember.mockResolvedValue({
781+nickname: "Guest Nick",
782+user: {
783+id: "u-guest",
784+username: "guest",
785+globalName: "Guest",
786+discriminator: "4321",
787+},
788+});
789+const manager = createManager({ groupPolicy: "open" }, client, {
790+commands: { useAccessGroups: false },
791+});
792+await processVoiceSegment(manager, "u-guest");
793+794+expect(textToSpeechStreamMock).toHaveBeenCalledWith(
795+expect.objectContaining({
796+channel: "discord",
797+disableFallback: true,
798+text: "hello back",
799+}),
800+);
801+expect(textToSpeechMock).not.toHaveBeenCalled();
802+expect(createAudioResourceMock).toHaveBeenCalledWith(expect.anything());
803+await vi.waitFor(() => expect(release).toHaveBeenCalledTimes(1));
804+});
805+753806it("passes per-channel system prompt overrides to voice agent runs", async () => {
754807const client = createClient();
755808client.fetchMember.mockResolvedValue({
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。