/** * @license * Copyright 2026 Google LLC * SPDX-License-Identifier: Apache-2.0 */ import { describe, expect, it } from 'vitest'; import { MockAgentProtocol } from './mock.js'; import type { AgentEvent, AgentProtocol } from './types.js'; const waitForStreamEnd = (session: AgentProtocol): Promise => new Promise((resolve) => { const events: AgentEvent[] = []; const unsubscribe = session.subscribe((e) => { events.push(e); if (e.type === 'agent_end') { unsubscribe(); resolve(events); } }); }); describe('MockAgentProtocol', () => { it('should emit queued events on send and subscribe', async () => { const session = new MockAgentProtocol(); const event1 = { type: 'message', role: 'agent', content: [{ type: 'text', text: 'hello' }], } as AgentEvent; session.pushResponse([event1]); const streamPromise = waitForStreamEnd(session); const { streamId } = await session.send({ message: { content: [{ type: 'text', text: 'hi' }] }, }); expect(streamId).toBeDefined(); const streamedEvents = await streamPromise; // Ordered: user message, agent_start, agent message, agent_end = 4 events expect(streamedEvents).toHaveLength(4); expect(streamedEvents[0].type).toBe('message'); expect((streamedEvents[0] as AgentEvent<'message'>).role).toBe('user'); expect(streamedEvents[1].type).toBe('agent_start'); expect(streamedEvents[2].type).toBe('message'); expect((streamedEvents[2] as AgentEvent<'message'>).role).toBe('agent'); expect(streamedEvents[3].type).toBe('agent_end'); expect(session.events).toHaveLength(4); expect(session.events).toEqual(streamedEvents); }); it('should handle multiple responses', async () => { const session = new MockAgentProtocol(); // Test with empty payload (no message injected) session.pushResponse([]); session.pushResponse([ { type: 'error', message: 'fail', fatal: true, status: 'RESOURCE_EXHAUSTED', }, ]); // First send const stream1Promise = waitForStreamEnd(session); const { streamId: s1 } = await session.send({ update: { title: 't1' }, }); const events1 = await stream1Promise; expect(events1).toHaveLength(3); // session_update, agent_start, agent_end expect(events1[0].type).toBe('session_update'); expect(events1[1].type).toBe('agent_start'); expect(events1[2].type).toBe('agent_end'); // Second send const stream2Promise = waitForStreamEnd(session); const { streamId: s2 } = await session.send({ update: { title: 't2' }, }); expect(s1).not.toBe(s2); const events2 = await stream2Promise; expect(events2).toHaveLength(4); // session_update, agent_start, error, agent_end expect(events2[0].type).toBe('session_update'); expect(events2[1].type).toBe('agent_start'); expect(events2[2].type).toBe('error'); expect(events2[3].type).toBe('agent_end'); expect(session.events).toHaveLength(7); }); it('should handle abort on a waiting stream', async () => { const session = new MockAgentProtocol(); // Use keepOpen to prevent auto agent_end session.pushResponse([{ type: 'message' }], { keepOpen: true }); const events: AgentEvent[] = []; let resolveStream: (evs: AgentEvent[]) => void; const streamPromise = new Promise((res) => { resolveStream = res; }); session.subscribe((e) => { events.push(e); if (e.type === 'agent_end') { resolveStream(events); } }); const { streamId: _streamId } = await session.send({ update: { title: 't' }, }); // Initial events should have been emitted expect(events.map((e) => e.type)).toEqual([ 'session_update', 'agent_start', 'message', ]); // At this point, the stream should be "waiting" for more events because it's still active // and hasn't seen an agent_end. await session.abort(); const finalEvents = await streamPromise; expect(finalEvents[3].type).toBe('agent_end'); expect((finalEvents[3] as AgentEvent<'agent_end'>).reason).toBe('aborted'); }); it('should handle pushToStream on a waiting stream', async () => { const session = new MockAgentProtocol(); session.pushResponse([], { keepOpen: true }); const events: AgentEvent[] = []; session.subscribe((e) => events.push(e)); const { streamId } = await session.send({ update: { title: 't' } }); expect(events.map((e) => e.type)).toEqual([ 'session_update', 'agent_start', ]); // Push new event to active stream session.pushToStream(streamId!, [{ type: 'message' }]); expect(events).toHaveLength(3); expect(events[2].type).toBe('message'); await session.abort(); expect(events).toHaveLength(4); expect(events[3].type).toBe('agent_end'); }); it('should handle pushToStream with close option', async () => { const session = new MockAgentProtocol(); session.pushResponse([], { keepOpen: true }); const streamPromise = waitForStreamEnd(session); const { streamId } = await session.send({ update: { title: 't' } }); // Push new event and close session.pushToStream(streamId!, [{ type: 'message' }], { close: true }); const events = await streamPromise; expect(events.map((e) => e.type)).toEqual([ 'session_update', 'agent_start', 'message', 'agent_end', ]); expect((events[3] as AgentEvent<'agent_end'>).reason).toBe('completed'); }); it('should not double up on agent_end if provided manually', async () => { const session = new MockAgentProtocol(); session.pushResponse([ { type: 'message' }, { type: 'agent_end', reason: 'completed' }, ]); const streamPromise = waitForStreamEnd(session); await session.send({ update: { title: 't' } }); const events = await streamPromise; const endEvents = events.filter((e) => e.type === 'agent_end'); expect(endEvents).toHaveLength(1); }); it('should handle elicitations', async () => { const session = new MockAgentProtocol(); session.pushResponse([]); const streamPromise = waitForStreamEnd(session); await session.send({ elicitations: [ { requestId: 'r1', action: 'accept', content: { foo: 'bar' } }, ], }); const events = await streamPromise; expect(events[0].type).toBe('elicitation_response'); expect((events[0] as AgentEvent<'elicitation_response'>).requestId).toBe( 'r1', ); expect(events[1].type).toBe('agent_start'); }); it('should handle updates and track state', async () => { const session = new MockAgentProtocol(); session.pushResponse([]); const streamPromise = waitForStreamEnd(session); await session.send({ update: { title: 'New Title', model: 'gpt-4', config: { x: 1 } }, }); expect(session.title).toBe('New Title'); expect(session.model).toBe('gpt-4'); expect(session.config).toEqual({ x: 1 }); const events = await streamPromise; expect(events[0].type).toBe('session_update'); expect(events[1].type).toBe('agent_start'); }); it('should return streamId: null if no response queued', async () => { const session = new MockAgentProtocol(); const { streamId } = await session.send({ update: { title: 'foo' } }); expect(streamId).toBeNull(); expect(session.events).toHaveLength(1); expect(session.events[0].type).toBe('session_update'); expect(session.events[0].streamId).toEqual(expect.any(String)); }); it('should throw on action', async () => { const session = new MockAgentProtocol(); await expect( session.send({ action: { type: 'foo', data: {} } }), ).rejects.toThrow('Actions not supported in MockAgentProtocol: foo'); }); });