From 48cfb77e9b7d409257afee1aa03dc53aa8359f8a Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 10 May 2026 07:48:14 +0100 Subject: [PATCH] test: clear talk realtime relay broad matchers --- src/gateway/talk-realtime-relay.test.ts | 379 +++++++++++++----------- 1 file changed, 199 insertions(+), 180 deletions(-) diff --git a/src/gateway/talk-realtime-relay.test.ts b/src/gateway/talk-realtime-relay.test.ts index a5628386681..b3a0c6aa624 100644 --- a/src/gateway/talk-realtime-relay.test.ts +++ b/src/gateway/talk-realtime-relay.test.ts @@ -86,6 +86,57 @@ describe("talk realtime gateway relay", () => { }; } + function expectRecordFields(record: unknown, expected: Record) { + expect(record).toBeDefined(); + const actual = record as Record; + for (const [key, value] of Object.entries(expected)) { + expect(actual[key]).toEqual(value); + } + return actual; + } + + function mockCallArg(mock: ReturnType, callIndex = 0, argIndex = 0) { + const call = mock.mock.calls[callIndex]; + if (!call) { + throw new Error(`Expected mock call ${callIndex}`); + } + return call[argIndex]; + } + + function findEventPayload( + events: Array<{ payload: unknown }>, + predicate: (payload: Record) => boolean, + ) { + const event = events.find((entry) => { + const payload = entry.payload; + return ( + typeof payload === "object" && + payload !== null && + predicate(payload as Record) + ); + }); + if (!event) { + throw new Error("Expected matching relay event"); + } + return event.payload as Record; + } + + function expectChatAbortPayload(mock: ReturnType, stopReason: string) { + expect(mockCallArg(mock)).toBe("chat"); + expectRecordFields(mockCallArg(mock, 0, 1), { + runId: "run-1", + sessionKey: "main", + state: "aborted", + stopReason, + }); + } + + function expectNodeAbortPayload(mock: ReturnType) { + expect(mockCallArg(mock)).toBe("main"); + expect(mockCallArg(mock, 0, 1)).toBe("chat"); + expectRecordFields(mockCallArg(mock, 0, 2), { runId: "run-1", state: "aborted" }); + } + it("bridges browser audio, transcripts, and tool results through a backend provider", async () => { let bridgeRequest: RealtimeVoiceBridgeCreateRequest | undefined; const bridge = { @@ -140,91 +191,93 @@ describe("talk realtime gateway relay", () => { }); await Promise.resolve(); - expect(session).toMatchObject({ + const sessionFields = expectRecordFields(session, { provider: "relay-test", transport: "gateway-relay", model: "browser-model", voice: "voice-a", - audio: { - inputEncoding: "pcm16", - inputSampleRateHz: 24000, - outputEncoding: "pcm16", - outputSampleRateHz: 24000, - }, }); - expect(bridgeRequest).toMatchObject({ + expectRecordFields(sessionFields.audio, { + inputEncoding: "pcm16", + inputSampleRateHz: 24000, + outputEncoding: "pcm16", + outputSampleRateHz: 24000, + }); + expectRecordFields(bridgeRequest, { providerConfig: { model: "provider-model" }, audioFormat: { encoding: "pcm16", sampleRateHz: 24000, channels: 1 }, instructions: "be brief", }); - expect(events).toEqual( - expect.arrayContaining([ - expect.objectContaining({ - event: "talk.event", - connIds: ["conn-1"], - payload: expect.objectContaining({ - relaySessionId: session.relaySessionId, - type: "ready", - talkEvent: expect.objectContaining({ - sessionId: session.relaySessionId, - type: "session.ready", - seq: 1, - mode: "realtime", - transport: "gateway-relay", - brain: "agent-consult", - provider: "relay-test", - }), - }), - }), - expect.objectContaining({ - payload: expect.objectContaining({ - relaySessionId: session.relaySessionId, - type: "audio", - audioBase64: Buffer.from("audio-out").toString("base64"), - talkEvent: expect.objectContaining({ type: "output.audio.delta" }), - }), - }), - expect.objectContaining({ - payload: expect.objectContaining({ - relaySessionId: session.relaySessionId, - type: "transcript", - role: "user", - text: "hello", - final: true, - talkEvent: expect.objectContaining({ type: "transcript.done", final: true }), - }), - }), - expect.objectContaining({ - payload: expect.objectContaining({ - relaySessionId: session.relaySessionId, - type: "transcript", - role: "assistant", - text: "hi there", - final: true, - talkEvent: expect.objectContaining({ - type: "output.text.done", - final: true, - payload: { text: "hi there" }, - }), - }), - }), - expect.objectContaining({ - payload: expect.objectContaining({ - relaySessionId: session.relaySessionId, - type: "toolCall", - itemId: "item-1", - callId: "call-1", - name: "openclaw_agent_consult", - args: { question: "what now" }, - talkEvent: expect.objectContaining({ - type: "tool.call", - itemId: "item-1", - callId: "call-1", - }), - }), - }), - ]), + + const readyPayload = findEventPayload(events, (payload) => payload.type === "ready"); + expectRecordFields(readyPayload, { + relaySessionId: session.relaySessionId, + type: "ready", + }); + expectRecordFields(readyPayload.talkEvent, { + sessionId: session.relaySessionId, + type: "session.ready", + seq: 1, + mode: "realtime", + transport: "gateway-relay", + brain: "agent-consult", + provider: "relay-test", + }); + const readyEvent = events.find((entry) => entry.payload === readyPayload); + expectRecordFields(readyEvent, { event: "talk.event", connIds: ["conn-1"] }); + + const audioPayload = findEventPayload(events, (payload) => payload.type === "audio"); + expectRecordFields(audioPayload, { + relaySessionId: session.relaySessionId, + type: "audio", + audioBase64: Buffer.from("audio-out").toString("base64"), + }); + expectRecordFields(audioPayload.talkEvent, { type: "output.audio.delta" }); + + const userTranscript = findEventPayload( + events, + (payload) => payload.type === "transcript" && payload.role === "user", ); + expectRecordFields(userTranscript, { + relaySessionId: session.relaySessionId, + type: "transcript", + role: "user", + text: "hello", + final: true, + }); + expectRecordFields(userTranscript.talkEvent, { type: "transcript.done", final: true }); + + const assistantTranscript = findEventPayload( + events, + (payload) => payload.type === "transcript" && payload.role === "assistant", + ); + expectRecordFields(assistantTranscript, { + relaySessionId: session.relaySessionId, + type: "transcript", + role: "assistant", + text: "hi there", + final: true, + }); + expectRecordFields(assistantTranscript.talkEvent, { + type: "output.text.done", + final: true, + payload: { text: "hi there" }, + }); + + const toolCallPayload = findEventPayload(events, (payload) => payload.type === "toolCall"); + expectRecordFields(toolCallPayload, { + relaySessionId: session.relaySessionId, + type: "toolCall", + itemId: "item-1", + callId: "call-1", + name: "openclaw_agent_consult", + args: { question: "what now" }, + }); + expectRecordFields(toolCallPayload.talkEvent, { + type: "tool.call", + itemId: "item-1", + callId: "call-1", + }); sendTalkRealtimeRelayAudio({ relaySessionId: session.relaySessionId, @@ -276,65 +329,67 @@ describe("talk realtime gateway relay", () => { ); expect(bridge.handleBargeIn).toHaveBeenCalledWith({ audioPlaybackActive: true }); expect(bridge.close).toHaveBeenCalled(); - expect(events).toEqual( - expect.arrayContaining([ - expect.objectContaining({ - payload: expect.objectContaining({ - relaySessionId: session.relaySessionId, - type: "inputAudio", - byteLength: Buffer.from("audio-in").byteLength, - talkEvent: expect.objectContaining({ type: "input.audio.delta" }), - }), - }), - expect.objectContaining({ - payload: expect.objectContaining({ - relaySessionId: session.relaySessionId, - type: "clear", - talkEvent: expect.objectContaining({ - type: "turn.cancelled", - payload: { reason: "barge-in" }, - final: true, - }), - }), - }), - expect.objectContaining({ - payload: expect.objectContaining({ - relaySessionId: session.relaySessionId, - type: "toolResult", - callId: "call-1", - talkEvent: expect.objectContaining({ - type: "tool.result", - callId: "call-1", - final: false, - }), - }), - }), - expect.objectContaining({ - payload: expect.objectContaining({ - relaySessionId: session.relaySessionId, - type: "toolResult", - callId: "call-1", - talkEvent: expect.objectContaining({ - type: "tool.result", - callId: "call-1", - final: true, - }), - }), - }), - ]), - ); - expect(events).toEqual( - expect.arrayContaining([ - expect.objectContaining({ - payload: expect.objectContaining({ - relaySessionId: session.relaySessionId, - type: "close", - reason: "completed", - talkEvent: expect.objectContaining({ type: "session.closed", final: true }), - }), - }), - ]), + const inputAudioPayload = findEventPayload( + events, + (payload) => + payload.type === "inputAudio" && payload.byteLength === Buffer.from("audio-in").byteLength, ); + expectRecordFields(inputAudioPayload, { + relaySessionId: session.relaySessionId, + type: "inputAudio", + byteLength: Buffer.from("audio-in").byteLength, + }); + expectRecordFields(inputAudioPayload.talkEvent, { type: "input.audio.delta" }); + + const clearPayload = findEventPayload(events, (payload) => payload.type === "clear"); + expectRecordFields(clearPayload, { + relaySessionId: session.relaySessionId, + type: "clear", + }); + expectRecordFields(clearPayload.talkEvent, { + type: "turn.cancelled", + payload: { reason: "barge-in" }, + final: true, + }); + + const toolResultPayloads = events + .map((entry) => entry.payload) + .filter( + (payload): payload is Record => + typeof payload === "object" && + payload !== null && + (payload as Record).type === "toolResult" && + (payload as Record).callId === "call-1", + ); + expect(toolResultPayloads).toHaveLength(2); + expectRecordFields(toolResultPayloads[0], { + relaySessionId: session.relaySessionId, + type: "toolResult", + callId: "call-1", + }); + expectRecordFields(toolResultPayloads[0]?.talkEvent, { + type: "tool.result", + callId: "call-1", + final: false, + }); + expectRecordFields(toolResultPayloads[1], { + relaySessionId: session.relaySessionId, + type: "toolResult", + callId: "call-1", + }); + expectRecordFields(toolResultPayloads[1]?.talkEvent, { + type: "tool.result", + callId: "call-1", + final: true, + }); + + const closePayload = findEventPayload(events, (payload) => payload.type === "close"); + expectRecordFields(closePayload, { + relaySessionId: session.relaySessionId, + type: "close", + reason: "completed", + }); + expectRecordFields(closePayload.talkEvent, { type: "session.closed", final: true }); }); it("rejects relay control from a different connection", () => { @@ -439,20 +494,8 @@ describe("talk realtime gateway relay", () => { expect(abortController.signal.aborted).toBe(true); expect(removeChatRun).toHaveBeenCalledWith("run-1", "run-1", "main"); - expect(broadcast).toHaveBeenCalledWith( - "chat", - expect.objectContaining({ - runId: "run-1", - sessionKey: "main", - state: "aborted", - stopReason: "barge-in", - }), - ); - expect(nodeSendToSession).toHaveBeenCalledWith( - "main", - "chat", - expect.objectContaining({ runId: "run-1", state: "aborted" }), - ); + expectChatAbortPayload(broadcast, "barge-in"); + expectNodeAbortPayload(nodeSendToSession); }); it("aborts linked agent consult runs when the relay session closes", () => { @@ -461,20 +504,8 @@ describe("talk realtime gateway relay", () => { stopTalkRealtimeRelaySession({ relaySessionId: session.relaySessionId, connId: "conn-1" }); expect(abortController.signal.aborted).toBe(true); - expect(broadcast).toHaveBeenCalledWith( - "chat", - expect.objectContaining({ - runId: "run-1", - sessionKey: "main", - state: "aborted", - stopReason: "relay-closed", - }), - ); - expect(nodeSendToSession).toHaveBeenCalledWith( - "main", - "chat", - expect.objectContaining({ runId: "run-1", state: "aborted" }), - ); + expectChatAbortPayload(broadcast, "relay-closed"); + expectNodeAbortPayload(nodeSendToSession); }); it("aborts linked agent consult runs when the provider closes the relay", () => { @@ -542,20 +573,8 @@ describe("talk realtime gateway relay", () => { bridgeRequest?.onClose?.("error"); expect(abortController.signal.aborted).toBe(true); - expect(broadcast).toHaveBeenCalledWith( - "chat", - expect.objectContaining({ - runId: "run-1", - sessionKey: "main", - state: "aborted", - stopReason: "relay-closed", - }), - ); - expect(nodeSendToSession).toHaveBeenCalledWith( - "main", - "chat", - expect.objectContaining({ runId: "run-1", state: "aborted" }), - ); + expectChatAbortPayload(broadcast, "relay-closed"); + expectNodeAbortPayload(nodeSendToSession); }); it("caps active relay sessions per browser connection", () => { @@ -590,13 +609,13 @@ describe("talk realtime gateway relay", () => { expect(() => createSession("conn-1")).toThrow( "Too many active realtime relay sessions for this connection", ); - expect(createSession("conn-2")).toMatchObject({ + const session = expectRecordFields(createSession("conn-2"), { provider: "relay-test", transport: "gateway-relay", - audio: { - inputEncoding: "pcm16", - outputEncoding: "pcm16", - }, + }); + expectRecordFields(session.audio, { + inputEncoding: "pcm16", + outputEncoding: "pcm16", }); }); });