import { act, renderHook, waitFor } from "@testing-library/react"; import { afterEach, describe, expect, it, vi } from "vitest"; import { useGeminiLive } from "@/hooks/use-gemini-live"; const serverCall = vi.hoisted(() => vi.fn()); vi.mock("@tanstack/react-start", () => ({ useServerFn: () => serverCall, createServerFn: () => ({}) })); vi.mock("@/lib/gemini.functions", () => ({ buildEchoSite: {}, deployEchoBuild: {}, listEchoProjects: {}, openEchoProject: {}, lookupEchoHistory: {}, saveEchoCall: {}, startEchoLive: {} })); vi.mock("@/lib/codehub.functions", () => ({ connectCustomDomain: {}, deployCodeProject: {} })); class Socket { static OPEN = 1; static instances: Socket[] = []; readyState = 1; send = vi.fn(); close = vi.fn(); onopen?: () => void; onmessage?: (event: { data: string }) => Promise; constructor() { Socket.instances.push(this); } } class Context { currentTime = 0; sampleRate = 48000; destination = {}; resume = vi.fn().mockResolvedValue(undefined); close = vi.fn().mockResolvedValue(undefined); audioWorklet = { addModule: vi.fn().mockResolvedValue(undefined) }; createMediaStreamSource = () => ({ connect: vi.fn() }); } afterEach(() => { vi.unstubAllGlobals(); vi.restoreAllMocks(); serverCall.mockReset(); Socket.instances = []; }); function environment() { const track = { stop: vi.fn() }; const mic = vi.fn().mockResolvedValue({ getTracks: () => [track] }); vi.stubGlobal("navigator", { mediaDevices: { getUserMedia: mic } }); vi.stubGlobal("AudioContext", Context); vi.stubGlobal("AudioWorkletNode", class { port = { onmessage: null }; }); vi.stubGlobal("WebSocket", Socket); vi.stubGlobal("URL", class extends URL { static override createObjectURL = () => "blob:test"; static override revokeObjectURL = vi.fn(); }); return { track, mic }; } describe("security-only voice transport", () => { it("dispatches Google's top-level security tools without invoking Echo functions or saving Echo history", async () => { const { track } = environment(); const onToolCall = vi.fn().mockResolvedValue({ awaitingApproval: true }); const startSession = vi.fn().mockResolvedValue({ token: "test-token" }); const { result, unmount } = renderHook(() => useGeminiLive({ onEvent: vi.fn(), startSession, onToolCall, saveCall: false })); await act(async () => { await result.current.start("Aman"); }); const ws = Socket.instances[0]; expect(ws).toBeDefined(); if (!ws) throw new Error("Missing socket"); await act(async () => { ws.onopen?.(); await ws.onmessage?.({ data: JSON.stringify({ setupComplete: {} }) }); }); expect(result.current.status).toBe("connected"); await act(async () => { await ws.onmessage?.({ data: JSON.stringify({ toolCall: { functionCalls: [{ id: "scan-1", name: "request_security_scan", args: { hostname: "example.com" } }] } }) }); }); expect(onToolCall).toHaveBeenCalledWith({ id: "scan-1", name: "request_security_scan", args: { hostname: "example.com" } }); expect(JSON.parse(ws.send.mock.calls.at(-1)?.[0])).toEqual({ toolResponse: { functionResponses: [{ id: "scan-1", name: "request_security_scan", response: { awaitingApproval: true } }] } }); act(() => result.current.setMuted(true)); expect(result.current.muted).toBe(true); await act(async () => { result.current.stop(); }); expect(track.stop).toHaveBeenCalledTimes(1); expect(result.current.muted).toBe(false); expect(serverCall).not.toHaveBeenCalled(); unmount(); }); it("never revives a stopped call when microphone permission arrives late", async () => { const { mic, track } = environment(); let resolveStream: ((stream: { getTracks: () => typeof track[] }) => void) | undefined; mic.mockImplementation(() => new Promise(resolve => { resolveStream = resolve; })); const startSession = vi.fn().mockResolvedValue({ token: "test-token" }); const { result, unmount } = renderHook(() => useGeminiLive({ onEvent: vi.fn(), startSession, saveCall: false })); let starting: Promise | undefined; act(() => { starting = result.current.start("Aman"); }); await waitFor(() => expect(result.current.status).toBe("connecting")); await act(async () => { result.current.stop(); }); await act(async () => { resolveStream?.({ getTracks: () => [track] }); await starting; }); expect(track.stop).toHaveBeenCalledTimes(1); expect(startSession).not.toHaveBeenCalled(); expect(Socket.instances).toHaveLength(0); expect(result.current.status).toBe("closed"); unmount(); }); });