fix(frontend): track playback completion and meaningful inactivity

This commit is contained in:
2026-10-07 09:27:54 -03:00
parent ff32c6a605
commit 09bf6ece00
8 changed files with 173 additions and 10 deletions
@@ -9,6 +9,7 @@ const audio = vi.hoisted(() => ({
close: vi.fn<() => Promise<void>>(),
play: vi.fn(),
clear: vi.fn(),
finishPlayback: vi.fn(),
setMuted: vi.fn(),
setGate: vi.fn(),
}));
@@ -19,6 +20,7 @@ vi.mock("../audio/engine", () => ({
close = audio.close;
play = audio.play;
clear = audio.clear;
finishPlayback = audio.finishPlayback;
setMuted = audio.setMuted;
setGate = audio.setGate;
},
@@ -89,6 +91,33 @@ afterEach(() => {
});
describe("realtime session", () => {
it("waits for buffered audio to drain after response.done", async () => {
const client = new RealtimeClient(options());
const socket = await connect(client);
socket.receive({ type: "response.created", response: { id: "r1" } });
socket.receive({ type: "response.audio.delta", response_id: "r1", delta: "AAAA" });
socket.receive({ type: "response.done", response: { id: "r1", status: "completed" } });
await vi.waitFor(() => {
expect(audio.finishPlayback).toHaveBeenCalledOnce();
});
expect(client.status).toBe("ai-speaking");
audio.start.mock.calls[0]?.[3]();
expect(client.status).toBe("listening");
await client.close();
});
it("does not let a playback drain override a user interruption", async () => {
const client = new RealtimeClient(options());
const socket = await connect(client);
socket.receive({ type: "response.created", response: { id: "r1" } });
socket.receive({ type: "response.audio.delta", response_id: "r1", delta: "AAAA" });
socket.receive({ type: "input_audio_buffer.speech_started" });
await vi.waitFor(() => {
expect(client.status).toBe("user-speaking");
});
audio.start.mock.calls[0]?.[3]();
expect(client.status).toBe("user-speaking");
await client.close();
});
it("starts microphone streaming without a session.updated acknowledgment", async () => {
const client = new RealtimeClient(options());
const pending = client.connect();