@@ -24,19 +24,91 @@ jest.mock('@/rapida/clients/protos/talk-api_pb', () => ({
2424 ACTION : 5 ,
2525 } ,
2626 } ,
27- CreateConversationMetricRequest : jest . fn ( ) ,
28- CreateMessageMetricRequest : jest . fn ( ) ,
27+ CreateConversationMetricRequest : jest . fn ( ) . mockImplementation ( ( ) => ( {
28+ setAssistantid : jest . fn ( ) ,
29+ setAssistantconversationid : jest . fn ( ) ,
30+ addMetrics : jest . fn ( ) ,
31+ } ) ) ,
32+ CreateMessageMetricRequest : jest . fn ( ) . mockImplementation ( ( ) => ( {
33+ setAssistantid : jest . fn ( ) ,
34+ setAssistantconversationid : jest . fn ( ) ,
35+ setMessageid : jest . fn ( ) ,
36+ addMetrics : jest . fn ( ) ,
37+ } ) ) ,
38+ StreamMode : {
39+ STREAM_MODE_AUDIO : 1 ,
40+ STREAM_MODE_TEXT : 2 ,
41+ STREAM_MODE_BOTH : 3 ,
42+ } ,
43+ ConversationInitialization : jest . fn ( ) . mockImplementation ( ( ) => {
44+ const options = new Map ( ) ;
45+ const metadata = new Map ( ) ;
46+ const args = new Map ( ) ;
47+ return {
48+ setAssistantconversationid : jest . fn ( ) ,
49+ setAssistant : jest . fn ( ) ,
50+ getOptionsMap : jest . fn ( ( ) => options ) ,
51+ getMetadataMap : jest . fn ( ( ) => metadata ) ,
52+ getArgsMap : jest . fn ( ( ) => args ) ,
53+ setStreammode : jest . fn ( ) ,
54+ setWeb : jest . fn ( ) ,
55+ } ;
56+ } ) ,
57+ ConversationConfiguration : jest . fn ( ) . mockImplementation ( ( ) => ( {
58+ setStreammode : jest . fn ( ) ,
59+ } ) ) ,
60+ WebIdentity : jest . fn ( ) . mockImplementation ( ( ) => ( {
61+ setUserid : jest . fn ( ) ,
62+ } ) ) ,
63+ } ) ) ;
64+
65+ jest . mock ( '@/rapida/clients/protos/webrtc_pb' , ( ) => ( {
66+ WebTalkRequest : jest . fn ( ) . mockImplementation ( ( ) => ( {
67+ hasInitialization : jest . fn ( ) . mockReturnValue ( false ) ,
68+ hasConfiguration : jest . fn ( ) . mockReturnValue ( false ) ,
69+ hasMessage : jest . fn ( ) . mockReturnValue ( false ) ,
70+ hasSignaling : jest . fn ( ) . mockReturnValue ( false ) ,
71+ getMessage : jest . fn ( ) ,
72+ setInitialization : jest . fn ( ) ,
73+ setConfiguration : jest . fn ( ) ,
74+ } ) ) ,
75+ WebTalkResponse : {
76+ DataCase : {
77+ USER : 2 ,
78+ ASSISTANT : 3 ,
79+ } ,
80+ } ,
81+ } ) ) ;
82+
83+ jest . mock ( '@/rapida/clients/protos/assistant-api_pb' , ( ) => ( {
84+ GetAssistantRequest : jest . fn ( ) . mockImplementation ( ( ) => ( {
85+ setAssistantdefinition : jest . fn ( ) ,
86+ } ) ) ,
87+ GetAssistantResponse : jest . fn ( ) ,
2988} ) ) ;
3089
3190jest . mock ( '@/rapida/clients/protos/common_pb' , ( ) => ( {
32- AssistantDefinition : jest . fn ( ) ,
91+ AssistantDefinition : jest . fn ( ) . mockImplementation ( ( ) => {
92+ let assistantId = '' ;
93+ let version = '' ;
94+ return {
95+ setAssistantid : jest . fn ( ( v : string ) => { assistantId = v ; } ) ,
96+ getAssistantid : jest . fn ( ( ) => assistantId ) ,
97+ setVersion : jest . fn ( ( v : string ) => { version = v ; } ) ,
98+ getVersion : jest . fn ( ( ) => version ) ,
99+ } ;
100+ } ) ,
33101 AudioConfig : {
34102 AudioFormat : { LINEAR16 : 1 } ,
35103 } ,
36104 StreamConfig : jest . fn ( ) . mockImplementation ( ( ) => ( {
37105 setAudio : jest . fn ( ) ,
38106 } ) ) ,
39- Metric : jest . fn ( ) ,
107+ Metric : jest . fn ( ) . mockImplementation ( ( ) => ( {
108+ setName : jest . fn ( ) ,
109+ setValue : jest . fn ( ) ,
110+ setDescription : jest . fn ( ) ,
111+ } ) ) ,
40112 AssistantConversationConfiguration : jest . fn ( ) ,
41113 AssistantConversationMessageTextContent : jest . fn ( ) . mockImplementation ( ( ) => ( {
42114 setContent : jest . fn ( ) ,
@@ -69,10 +141,21 @@ jest.mock('@/rapida/types/feedback', () => ({
69141 getFeedback : jest . fn ( ) ,
70142} ) ) ;
71143
72- import { Agent } from '@/rapida/agents/' ;
144+ import {
145+ Agent ,
146+ buildConfigurationRequest ,
147+ buildInitializationRequest ,
148+ describeRequest ,
149+ describeResponse ,
150+ resolveStreamMode ,
151+ } from '@/rapida/agents/' ;
73152import { ConnectionState } from '@/rapida/types/connection-state' ;
74153import { AgentEvent } from '@/rapida/types/agent-event' ;
75154import { MessageRole , MessageStatus } from '@/rapida/types/message' ;
155+ import { CreateConversationMetric , CreateMessageMetric } from '@/rapida/clients/talk' ;
156+ import { GetAssistant } from '@/rapida/clients/assistant' ;
157+ import { getFeedback } from '@/rapida/types/feedback' ;
158+ import { Channel } from '@/rapida/types/channel' ;
76159
77160// Create a concrete implementation for testing since Agent may be abstract
78161class TestAgent extends Agent {
@@ -88,13 +171,22 @@ class TestAgent extends Agent {
88171 public getAgentConfig ( ) {
89172 return this . agentConfig ;
90173 }
174+
175+ public setConversation ( id : string ) {
176+ this . changeConversation ( id ) ;
177+ }
178+
179+ public disconnectBase ( ) {
180+ return this . disconnectAgent ( ) ;
181+ }
91182}
92183
93184describe ( 'Agent' , ( ) => {
94185 let agent : TestAgent ;
95186
96187 const mockConnectionConfig = {
97188 endpoint : 'https://api.test.com' ,
189+ onConnectionChange : jest . fn ( ) ,
98190 } ;
99191
100192 const mockAgentConfig = {
@@ -107,6 +199,10 @@ describe('Agent', () => {
107199 channel : 'audio' ,
108200 playerOption : { sampleRate : 24000 , format : 'pcm' } ,
109201 } ,
202+ definition : {
203+ getAssistantid : ( ) => 'test-agent' ,
204+ } ,
205+ version : 'v1' ,
110206 } ;
111207
112208 beforeEach ( ( ) => {
@@ -259,4 +355,168 @@ describe('Agent', () => {
259355 expect ( agent . agentMessages [ 0 ] . status ) . toBe ( MessageStatus . Pending ) ;
260356 } ) ;
261357 } ) ;
358+
359+ describe ( 'advanced agent behavior' , ( ) => {
360+ it ( 'throws on base switchAgent()' , async ( ) => {
361+ await expect ( agent . switchAgent ( mockAgentConfig as any ) ) . rejects . toThrow (
362+ 'switchAgent must be implemented by subclass' ,
363+ ) ;
364+ } ) ;
365+
366+ it ( 'registers callback' , ( ) => {
367+ const cb = { onUserMessage : jest . fn ( ) } ;
368+ agent . registerCallback ( cb as any ) ;
369+ expect ( ( agent as any ) . agentCallbacks ) . toContain ( cb ) ;
370+ } ) ;
371+
372+ it ( 'disconnectAgent() is idempotent when already disconnected' , async ( ) => {
373+ await agent . disconnectBase ( ) ;
374+ expect ( mockConnectionConfig . onConnectionChange ) . not . toHaveBeenCalled ( ) ;
375+ } ) ;
376+
377+ it ( 'createMessageMetric throws without active conversation' , ( ) => {
378+ expect ( ( ) =>
379+ agent . createMessageMetric ( 'm-1' , [ { name : 'n' , description : 'd' , value : 'good' } ] ) ,
380+ ) . toThrow ( 'Cannot create message metric: no active conversation' ) ;
381+ } ) ;
382+
383+ it ( 'createConversationMetric throws without active conversation' , ( ) => {
384+ expect ( ( ) =>
385+ agent . createConversationMetric ( [ { name : 'n' , description : 'd' , value : 'good' } ] ) ,
386+ ) . toThrow ( 'Cannot create conversation metric: no active conversation' ) ;
387+ } ) ;
388+
389+ it ( 'creates message metric and emits feedback' , async ( ) => {
390+ ( getFeedback as jest . Mock ) . mockReturnValue ( 'POSITIVE' ) ;
391+ const feedbackListener = jest . fn ( ) ;
392+ agent . on ( AgentEvent . FeedbackEvent , feedbackListener ) ;
393+ agent . setConversation ( 'conv-1' ) ;
394+ agent . agentMessages = [
395+ {
396+ id : 'msg-1' ,
397+ role : MessageRole . System ,
398+ messages : [ 'hello' ] ,
399+ time : new Date ( ) ,
400+ status : MessageStatus . Complete ,
401+ } as any ,
402+ ] ;
403+
404+ agent . createMessageMetric ( 'msg-1' , [
405+ { name : 'quality' , description : 'score' , value : 'good' } ,
406+ ] ) ;
407+
408+ await Promise . resolve ( ) ;
409+
410+ expect ( CreateMessageMetric ) . toHaveBeenCalled ( ) ;
411+ expect ( feedbackListener ) . toHaveBeenCalledWith ( 'message' , 'POSITIVE' ) ;
412+ expect ( ( agent . agentMessages [ 0 ] as any ) . feedback ) . toBe ( 'POSITIVE' ) ;
413+ } ) ;
414+
415+ it ( 'handles empty message metrics safely' , async ( ) => {
416+ agent . setConversation ( 'conv-1' ) ;
417+ agent . createMessageMetric ( 'msg-1' , [ ] ) ;
418+ await Promise . resolve ( ) ;
419+ expect ( CreateMessageMetric ) . toHaveBeenCalled ( ) ;
420+ } ) ;
421+
422+ it ( 'creates conversation metric when conversation exists' , ( ) => {
423+ agent . setConversation ( 'conv-2' ) ;
424+ agent . createConversationMetric ( [
425+ { name : 'latency' , description : 'ms' , value : '123' } ,
426+ ] ) ;
427+ expect ( CreateConversationMetric ) . toHaveBeenCalled ( ) ;
428+ } ) ;
429+
430+ it ( 'returns assistant from getAssistant()' , async ( ) => {
431+ const result = await agent . getAssistant ( ) ;
432+ expect ( GetAssistant ) . toHaveBeenCalled ( ) ;
433+ expect ( result ) . toBeDefined ( ) ;
434+ } ) ;
435+
436+ it ( 'changeConversation sets conversation once' , ( ) => {
437+ agent . setConversation ( 'conv-1' ) ;
438+ agent . setConversation ( 'conv-2' ) ;
439+ expect ( agent . conversationId ) . toBe ( 'conv-1' ) ;
440+ } ) ;
441+ } ) ;
442+
443+ describe ( 'request/response helpers' , ( ) => {
444+ it ( 'describeRequest formats known request types' , ( ) => {
445+ expect (
446+ describeRequest ( {
447+ hasInitialization : ( ) => true ,
448+ hasConfiguration : ( ) => false ,
449+ hasMessage : ( ) => false ,
450+ hasSignaling : ( ) => false ,
451+ } as any ) ,
452+ ) . toBe ( 'Initialization' ) ;
453+
454+ expect (
455+ describeRequest ( {
456+ hasInitialization : ( ) => false ,
457+ hasConfiguration : ( ) => false ,
458+ hasMessage : ( ) => true ,
459+ getMessage : ( ) => ( { getText : ( ) => 'hello world' } ) ,
460+ hasSignaling : ( ) => false ,
461+ } as any ) ,
462+ ) . toContain ( 'Text(' ) ;
463+ } ) ;
464+
465+ it ( 'describeResponse formats empty and mixed responses' , ( ) => {
466+ expect (
467+ describeResponse ( {
468+ hasInitialization : ( ) => false ,
469+ hasConfiguration : ( ) => false ,
470+ hasAssistant : ( ) => false ,
471+ hasUser : ( ) => false ,
472+ hasInterruption : ( ) => false ,
473+ hasDirective : ( ) => false ,
474+ hasSignaling : ( ) => false ,
475+ } as any ) ,
476+ ) . toBe ( 'Empty' ) ;
477+
478+ const mixed = describeResponse ( {
479+ hasInitialization : ( ) => true ,
480+ hasConfiguration : ( ) => true ,
481+ hasAssistant : ( ) => true ,
482+ getAssistant : ( ) => ( { getText : ( ) => 'assistant text' } ) ,
483+ hasUser : ( ) => true ,
484+ getUser : ( ) => ( { getText : ( ) => 'user text' } ) ,
485+ hasInterruption : ( ) => true ,
486+ hasDirective : ( ) => true ,
487+ hasSignaling : ( ) => true ,
488+ } as any ) ;
489+
490+ expect ( mixed ) . toContain ( 'Initialization' ) ;
491+ expect ( mixed ) . toContain ( 'Configuration' ) ;
492+ expect ( mixed ) . toContain ( 'Assistant(' ) ;
493+ expect ( mixed ) . toContain ( 'User(' ) ;
494+ expect ( mixed ) . toContain ( 'Interruption' ) ;
495+ expect ( mixed ) . toContain ( 'Directive' ) ;
496+ expect ( mixed ) . toContain ( 'Signaling' ) ;
497+ } ) ;
498+
499+ it ( 'resolveStreamMode maps channels' , ( ) => {
500+ expect ( resolveStreamMode ( Channel . Audio ) ) . toBeDefined ( ) ;
501+ expect ( resolveStreamMode ( Channel . Text ) ) . toBeDefined ( ) ;
502+ expect ( resolveStreamMode ( 'unknown' as any ) ) . toBeDefined ( ) ;
503+ } ) ;
504+
505+ it ( 'builds initialization/configuration requests' , ( ) => {
506+ const config = {
507+ definition : { } ,
508+ options : new Map ( [ [ 'k1' , { x : 1 } ] ] ) ,
509+ metadata : new Map ( [ [ 'k2' , { y : 2 } ] ] ) ,
510+ arguments : new Map ( [ [ 'k3' , { z : 3 } ] ] ) ,
511+ inputOptions : { channel : Channel . Text } ,
512+ userIdentifier : { id : 'user-1' } ,
513+ } as any ;
514+
515+ const initReq = buildInitializationRequest ( config , 'conv-10' ) ;
516+ expect ( initReq ) . toBeDefined ( ) ;
517+
518+ const confReq = buildConfigurationRequest ( Channel . Audio ) ;
519+ expect ( confReq ) . toBeDefined ( ) ;
520+ } ) ;
521+ } ) ;
262522} ) ;
0 commit comments