fix(frontend): start conversations without a welcome modal
This commit is contained in:
@@ -3,8 +3,9 @@ import { afterEach, beforeEach, describe, expect, it, vi, type Mock } from "vite
|
||||
import type { ClientOptions } from "./client";
|
||||
import { VoiceController } from "./controller";
|
||||
|
||||
const { MockClient, closeContext } = vi.hoisted(() => {
|
||||
const { MockClient, closeContext, createContext } = vi.hoisted(() => {
|
||||
const closeContext = vi.fn().mockResolvedValue(undefined);
|
||||
const createContext = vi.fn(() => ({ state: "running", close: closeContext }));
|
||||
class MockClient {
|
||||
static instances: MockClient[] = [];
|
||||
readonly options: ClientOptions;
|
||||
@@ -41,16 +42,17 @@ const { MockClient, closeContext } = vi.hoisted(() => {
|
||||
sendToolOutput = vi.fn();
|
||||
join = vi.fn();
|
||||
}
|
||||
return { MockClient, closeContext };
|
||||
return { MockClient, closeContext, createContext };
|
||||
});
|
||||
vi.mock("./client", async (importOriginal) => ({ ...(await importOriginal()), RealtimeClient: MockClient }));
|
||||
vi.mock("../audio/engine", () => ({ createAudioContext: () => ({ state: "running", close: closeContext }) }));
|
||||
vi.mock("../audio/engine", () => ({ createAudioContext: createContext }));
|
||||
let pendingReasoning: ((value: Response) => void) | undefined;
|
||||
let controller: VoiceController;
|
||||
let fetchMock: Mock<(path: string, init?: RequestInit) => Promise<Response>>;
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers();
|
||||
vi.clearAllMocks();
|
||||
createContext.mockReturnValue({ state: "running", close: closeContext });
|
||||
MockClient.instances = [];
|
||||
pendingReasoning = undefined;
|
||||
vi.stubGlobal("AudioContext", vi.fn());
|
||||
@@ -87,6 +89,83 @@ async function start() {
|
||||
return client;
|
||||
}
|
||||
describe("voice application lifecycle", () => {
|
||||
it.each(["TALK", "TALKFIRST"])(
|
||||
"automatically starts %s once with the greeting after initialization",
|
||||
async (startSessionMode) => {
|
||||
fetchMock.mockResolvedValueOnce(
|
||||
new Response(JSON.stringify({ startSessionMode, s2sUrl: "ws://localhost/v1/realtime" })),
|
||||
);
|
||||
controller.offerAutoStart();
|
||||
expect(MockClient.instances).toHaveLength(0);
|
||||
await controller.initialize();
|
||||
controller.offerAutoStart();
|
||||
controller.offerAutoStart();
|
||||
expect(MockClient.instances).toHaveLength(1);
|
||||
expect(MockClient.instances[0]?.options.greeting).toBe("hola vocero");
|
||||
expect(controller.getSnapshot().status).toBe("listening");
|
||||
await controller.stop();
|
||||
controller.offerAutoStart();
|
||||
expect(MockClient.instances).toHaveLength(1);
|
||||
expect(controller.getSnapshot().status).toBe("closed");
|
||||
},
|
||||
);
|
||||
it.each(["", "UNSUPPORTED"])("leaves startup manual for mode %s", async (startSessionMode) => {
|
||||
fetchMock.mockResolvedValueOnce(
|
||||
new Response(JSON.stringify({ startSessionMode, s2sUrl: "ws://localhost/v1/realtime" })),
|
||||
);
|
||||
await controller.initialize();
|
||||
controller.offerAutoStart();
|
||||
expect(createContext).not.toHaveBeenCalled();
|
||||
expect(MockClient.instances).toHaveLength(0);
|
||||
await controller.start();
|
||||
expect(MockClient.instances).toHaveLength(1);
|
||||
expect(MockClient.instances[0]?.options.greeting).toBeUndefined();
|
||||
});
|
||||
it("waits for caller context before the automatic attempt", async () => {
|
||||
fetchMock.mockResolvedValueOnce(
|
||||
new Response(JSON.stringify({ startSessionMode: "TALK", s2sUrl: "ws://localhost/v1/realtime" })),
|
||||
);
|
||||
let finishProfile: ((value: Response) => void) | undefined;
|
||||
fetchMock.mockImplementationOnce(
|
||||
() =>
|
||||
new Promise<Response>((resolve) => {
|
||||
finishProfile = resolve;
|
||||
}),
|
||||
);
|
||||
const initializing = controller.initialize();
|
||||
await vi.advanceTimersByTimeAsync(1);
|
||||
expect(controller.getSnapshot()).toMatchObject({ configReady: true, contextLoading: true });
|
||||
controller.offerAutoStart();
|
||||
expect(MockClient.instances).toHaveLength(0);
|
||||
finishProfile?.(new Response(JSON.stringify({ result: { name: "Ana" } })));
|
||||
await initializing;
|
||||
controller.offerAutoStart();
|
||||
expect(MockClient.instances).toHaveLength(1);
|
||||
});
|
||||
it.each(["TALK", "TALKFIRST"])(
|
||||
"leaves a manual fallback when the browser blocks %s audio",
|
||||
async (startSessionMode) => {
|
||||
fetchMock.mockResolvedValueOnce(
|
||||
new Response(JSON.stringify({ startSessionMode, s2sUrl: "ws://localhost/v1/realtime" })),
|
||||
);
|
||||
createContext.mockReturnValueOnce({ state: "suspended", close: closeContext });
|
||||
await controller.initialize();
|
||||
controller.offerAutoStart();
|
||||
expect(closeContext).toHaveBeenCalledOnce();
|
||||
expect(MockClient.instances).toHaveLength(0);
|
||||
expect(controller.getSnapshot()).toMatchObject({
|
||||
status: "idle",
|
||||
error: "",
|
||||
notice: "Toca el micrófono para empezar a conversar.",
|
||||
});
|
||||
controller.offerAutoStart();
|
||||
expect(createContext).toHaveBeenCalledOnce();
|
||||
await controller.start();
|
||||
expect(MockClient.instances).toHaveLength(1);
|
||||
expect(MockClient.instances[0]?.options.greeting).toBe("hola vocero");
|
||||
expect(controller.getSnapshot()).toMatchObject({ status: "listening", notice: "" });
|
||||
},
|
||||
);
|
||||
it("notifies only meter subscribers when microphone level changes", async () => {
|
||||
const client = await start();
|
||||
const app = vi.fn();
|
||||
|
||||
Reference in New Issue
Block a user