





















@@ -1,4 +1,4 @@
1-import type { Readable } from "node:stream";
1+import { PassThrough, type Readable } from "node:stream";
22import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
33import { ChannelType } from "../internal/discord.js";
44import { createVoiceCaptureState } from "./capture-state.js";
@@ -1223,9 +1223,70 @@ describe("DiscordVoiceManager", () => {
12231223);
12241224});
122512251226+it("skips incomplete and non-actionable forced agent-proxy transcripts", async () => {
1227+agentCommandMock.mockResolvedValueOnce({ payloads: [{ text: "valid answer" }] });
1228+const manager = createManager({
1229+groupPolicy: "open",
1230+voice: {
1231+enabled: true,
1232+mode: "agent-proxy",
1233+realtime: { provider: "openai" },
1234+},
1235+});
1236+1237+await manager.join({ guildId: "g1", channelId: "1001" });
1238+const entry = getSessionEntry(manager) as {
1239+realtime?: {
1240+beginSpeakerTurn: (
1241+context: { extraSystemPrompt?: string; senderIsOwner: boolean; speakerLabel: string },
1242+userId: string,
1243+) => { close: () => void; sendInputAudio: (audio: Buffer) => void };
1244+};
1245+};
1246+const bridgeParams = createRealtimeVoiceBridgeSessionMock.mock.calls.at(-1)?.[0] as
1247+| {
1248+onTranscript?: (role: "user" | "assistant", text: string, isFinal: boolean) => void;
1249+}
1250+| undefined;
1251+1252+const incompleteTurn = entry.realtime?.beginSpeakerTurn(
1253+{ extraSystemPrompt: undefined, senderIsOwner: true, speakerLabel: "Owner" },
1254+"u-owner",
1255+);
1256+incompleteTurn?.sendInputAudio(Buffer.alloc(8));
1257+bridgeParams?.onTranscript?.("user", "Get this working and...", true);
1258+1259+const closingTurn = entry.realtime?.beginSpeakerTurn(
1260+{ extraSystemPrompt: undefined, senderIsOwner: true, speakerLabel: "Owner" },
1261+"u-owner",
1262+);
1263+closingTurn?.sendInputAudio(Buffer.alloc(8));
1264+bridgeParams?.onTranscript?.("user", "I'll be right back. See you guys. Bye-bye.", true);
1265+1266+await new Promise((resolve) => setTimeout(resolve, 260));
1267+expect(agentCommandMock).not.toHaveBeenCalled();
1268+1269+const validTurn = entry.realtime?.beginSpeakerTurn(
1270+{ extraSystemPrompt: undefined, senderIsOwner: true, speakerLabel: "Owner" },
1271+"u-owner",
1272+);
1273+validTurn?.sendInputAudio(Buffer.alloc(8));
1274+bridgeParams?.onTranscript?.("user", "ship it.", true);
1275+1276+await new Promise((resolve) => setTimeout(resolve, 260));
1277+expect(agentCommandMock).toHaveBeenCalledWith(
1278+expect.objectContaining({ message: expect.stringContaining("ship it.") }),
1279+expect.anything(),
1280+);
1281+expect(realtimeSessionMock.sendUserMessage).toHaveBeenCalledWith(
1282+expect.stringContaining("valid answer"),
1283+);
1284+});
1285+12261286it("queues forced agent-proxy answers until current realtime playback idles", async () => {
12271287let resolveFirst: ((value: { payloads: Array<{ text: string }> }) => void) | undefined;
12281288let resolveSecond: ((value: { payloads: Array<{ text: string }> }) => void) | undefined;
1289+let resolveThird: ((value: { payloads: Array<{ text: string }> }) => void) | undefined;
12291290agentCommandMock
12301291.mockImplementationOnce(
12311292() =>
@@ -1238,6 +1299,12 @@ describe("DiscordVoiceManager", () => {
12381299new Promise<{ payloads: Array<{ text: string }> }>((resolve) => {
12391300resolveSecond = resolve;
12401301}),
1302+)
1303+.mockImplementationOnce(
1304+() =>
1305+new Promise<{ payloads: Array<{ text: string }> }>((resolve) => {
1306+resolveThird = resolve;
1307+}),
12411308);
12421309const manager = createManager({
12431310groupPolicy: "open",
@@ -1263,6 +1330,7 @@ describe("DiscordVoiceManager", () => {
12631330const bridgeParams = createRealtimeVoiceBridgeSessionMock.mock.calls.at(-1)?.[0] as
12641331| {
12651332audioSink?: { sendAudio: (audio: Buffer) => void };
1333+onEvent?: (event: { direction: "server"; type: string }) => void;
12661334onTranscript?: (role: "user" | "assistant", text: string, isFinal: boolean) => void;
12671335}
12681336| undefined;
@@ -1279,6 +1347,12 @@ describe("DiscordVoiceManager", () => {
12791347);
12801348secondTurn?.sendInputAudio(Buffer.alloc(8));
12811349bridgeParams?.onTranscript?.("user", "second question", true);
1350+const thirdTurn = entry.realtime?.beginSpeakerTurn(
1351+{ extraSystemPrompt: undefined, senderIsOwner: true, speakerLabel: "Owner" },
1352+"u-owner",
1353+);
1354+thirdTurn?.sendInputAudio(Buffer.alloc(8));
1355+bridgeParams?.onTranscript?.("user", "third question", true);
12821356await new Promise((resolve) => setTimeout(resolve, 260));
1283135712841358resolveFirst?.({ payloads: [{ text: "first answer" }] });
@@ -1290,6 +1364,18 @@ describe("DiscordVoiceManager", () => {
12901364bridgeParams?.audioSink?.sendAudio(Buffer.alloc(480));
1291136512921366resolveSecond?.({ payloads: [{ text: "second answer" }] });
1367+resolveThird?.({ payloads: [{ text: "third answer" }] });
1368+await new Promise((resolve) => setTimeout(resolve, 0));
1369+expect(realtimeSessionMock.sendUserMessage).not.toHaveBeenCalledWith(
1370+expect.stringContaining("second answer"),
1371+);
1372+expect(realtimeSessionMock.sendUserMessage).not.toHaveBeenCalledWith(
1373+expect.stringContaining("third answer"),
1374+);
1375+1376+bridgeParams?.onEvent?.({ direction: "server", type: "response.done" });
1377+const firstStream = createAudioResourceMock.mock.calls.at(-1)?.[0] as PassThrough | undefined;
1378+await vi.waitFor(() => expect(firstStream?.writableEnded).toBe(true));
12931379await new Promise((resolve) => setTimeout(resolve, 0));
12941380expect(realtimeSessionMock.sendUserMessage).not.toHaveBeenCalledWith(
12951381expect.stringContaining("second answer"),
@@ -1302,6 +1388,23 @@ describe("DiscordVoiceManager", () => {
13021388expect(realtimeSessionMock.sendUserMessage).toHaveBeenCalledWith(
13031389expect.stringContaining("second answer"),
13041390);
1391+expect(realtimeSessionMock.sendUserMessage).not.toHaveBeenCalledWith(
1392+expect.stringContaining("third answer"),
1393+);
1394+1395+bridgeParams?.audioSink?.sendAudio(Buffer.alloc(480));
1396+bridgeParams?.onEvent?.({ direction: "server", type: "response.done" });
1397+const secondStream = createAudioResourceMock.mock.calls.at(-1)?.[0] as PassThrough | undefined;
1398+await vi.waitFor(() => expect(secondStream?.writableEnded).toBe(true));
1399+await new Promise((resolve) => setTimeout(resolve, 0));
1400+expect(realtimeSessionMock.sendUserMessage).not.toHaveBeenCalledWith(
1401+expect.stringContaining("third answer"),
1402+);
1403+1404+idleHandler?.();
1405+expect(realtimeSessionMock.sendUserMessage).toHaveBeenCalledWith(
1406+expect.stringContaining("third answer"),
1407+);
13051408});
1306140913071410it("matches agent-proxy consult tool calls to the pending transcript", async () => {
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。