test: clear talk realtime relay broad matchers

This commit is contained in:
Peter Steinberger
2026-05-10 07:48:14 +01:00
parent 47c4d73580
commit 48cfb77e9b

View File

@@ -86,6 +86,57 @@ describe("talk realtime gateway relay", () => {
};
}
function expectRecordFields(record: unknown, expected: Record<string, unknown>) {
expect(record).toBeDefined();
const actual = record as Record<string, unknown>;
for (const [key, value] of Object.entries(expected)) {
expect(actual[key]).toEqual(value);
}
return actual;
}
function mockCallArg(mock: ReturnType<typeof vi.fn>, 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<string, unknown>) => boolean,
) {
const event = events.find((entry) => {
const payload = entry.payload;
return (
typeof payload === "object" &&
payload !== null &&
predicate(payload as Record<string, unknown>)
);
});
if (!event) {
throw new Error("Expected matching relay event");
}
return event.payload as Record<string, unknown>;
}
function expectChatAbortPayload(mock: ReturnType<typeof vi.fn>, stopReason: string) {
expect(mockCallArg(mock)).toBe("chat");
expectRecordFields(mockCallArg(mock, 0, 1), {
runId: "run-1",
sessionKey: "main",
state: "aborted",
stopReason,
});
}
function expectNodeAbortPayload(mock: ReturnType<typeof vi.fn>) {
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<string, unknown> =>
typeof payload === "object" &&
payload !== null &&
(payload as Record<string, unknown>).type === "toolResult" &&
(payload as Record<string, unknown>).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",
});
});
});