@@ -28,18 +28,19 @@ import {ActivatedRoute, NavigationEnd, Router} from '@angular/router';
2828import { BehaviorSubject , NEVER , of , Subject , throwError } from 'rxjs' ;
2929
3030import { EvalCase } from '../../core/models/Eval' ;
31- import { AGENT_SERVICE , AgentService } from '../../core/services/agent.service ' ;
32- import { ARTIFACT_SERVICE , ArtifactService , } from '../../core/services/artifact.service ' ;
33- import { DOWNLOAD_SERVICE , DownloadService , } from '../../core/services/download.service ' ;
34- import { EVAL_SERVICE , EvalService } from '../../core/services/eval.service ' ;
35- import { EVENT_SERVICE , EventService } from '../../core/services/event.service ' ;
36- import { FEATURE_FLAG_SERVICE , FeatureFlagService , } from '../../core/services/feature-flag.service ' ;
37- import { GRAPH_SERVICE , GraphService } from '../../core/services/graph.service ' ;
31+ import { AGENT_SERVICE , AgentService } from '../../core/services/interfaces/ agent' ;
32+ import { ARTIFACT_SERVICE , ArtifactService , } from '../../core/services/interfaces/ artifact' ;
33+ import { DOWNLOAD_SERVICE , DownloadService , } from '../../core/services/interfaces/ download' ;
34+ import { EVAL_SERVICE , EvalService } from '../../core/services/interfaces/ eval' ;
35+ import { EVENT_SERVICE , EventService } from '../../core/services/interfaces/ event' ;
36+ import { FEATURE_FLAG_SERVICE , FeatureFlagService , } from '../../core/services/interfaces/ feature-flag' ;
37+ import { GRAPH_SERVICE , GraphService } from '../../core/services/interfaces/ graph' ;
3838import { LOCAL_FILE_SERVICE } from '../../core/services/interfaces/localfile' ;
3939import { SAFE_VALUES_SERVICE } from '../../core/services/interfaces/safevalues' ;
4040import { STRING_TO_COLOR_SERVICE } from '../../core/services/interfaces/string-to-color' ;
41- import { SESSION_SERVICE , SessionService , } from '../../core/services/session.service' ;
42- import { STREAM_CHAT_SERVICE } from '../../core/services/stream-chat.service' ;
41+ import { LOCATION_SERVICE } from '../../core/services/location.service' ;
42+ import { SESSION_SERVICE , SessionService , } from '../../core/services/interfaces/session' ;
43+ import { STREAM_CHAT_SERVICE } from '../../core/services/interfaces/stream-chat' ;
4344import { MockAgentService } from '../../core/services/testing/mock-agent.service' ;
4445import { MockArtifactService } from '../../core/services/testing/mock-artifact.service' ;
4546import { MockDownloadService } from '../../core/services/testing/mock-download.service' ;
@@ -55,9 +56,9 @@ import {MockStringToColorService} from '../../core/services/testing/mock-string-
5556import { MockTraceService } from '../../core/services/testing/mock-trace.service' ;
5657import { MockVideoService } from '../../core/services/testing/mock-video.service' ;
5758import { MockWebSocketService } from '../../core/services/testing/mock-websocket.service' ;
58- import { TRACE_SERVICE , TraceService } from '../../core/services/trace.service ' ;
59- import { VIDEO_SERVICE , VideoService } from '../../core/services/video.service ' ;
60- import { WEBSOCKET_SERVICE , WebSocketService , } from '../../core/services/websocket.service ' ;
59+ import { TRACE_SERVICE , TraceService } from '../../core/services/interfaces/ trace' ;
60+ import { VIDEO_SERVICE , VideoService } from '../../core/services/interfaces/ video' ;
61+ import { WEBSOCKET_SERVICE , WebSocketService , } from '../../core/services/interfaces/ websocket' ;
6162import { fakeAsync ,
6263 tick } from '../../testing/utils' ;
6364import { ChatPanelComponent } from '../chat-panel/chat-panel.component' ;
@@ -145,7 +146,7 @@ describe('ChatComponent', () => {
145146 mockSessionService . createSessionResponse . next (
146147 { id : SESSION_1_ID , state : { } } ) ;
147148 mockTraceService . selectedTraceRow$ . next ( undefined ) ;
148- mockTraceService . hoveredMessageIndicies $. next ( [ ] ) ;
149+ mockTraceService . hoveredMessageIndices $. next ( [ ] ) ;
149150 mockFeatureFlagService . isImportSessionEnabledResponse . next ( true ) ;
150151 mockFeatureFlagService . isEditFunctionArgsEnabledResponse . next ( true ) ;
151152 mockFeatureFlagService . isSessionUrlEnabledResponse . next ( true ) ;
@@ -215,7 +216,7 @@ describe('ChatComponent', () => {
215216 { provide : MatSnackBar , useValue : mockSnackBar } ,
216217 { provide : Router , useValue : mockRouter } ,
217218 { provide : ActivatedRoute , useValue : mockActivatedRoute } ,
218- { provide : Location , useValue : mockLocation } ,
219+ { provide : LOCATION_SERVICE , useValue : mockLocation } ,
219220 { provide : MARKDOWN_COMPONENT , useValue : MockMarkdownComponent } ,
220221 ] ,
221222 } )
@@ -291,29 +292,35 @@ describe('ChatComponent', () => {
291292 } ) ;
292293
293294 describe ( 'when session ID is provided in URL' , ( ) => {
294- beforeEach ( async ( ) => {
295+ beforeEach ( ( ) => {
296+ mockAgentService . listAppsResponse . next ( [ TEST_APP_1_NAME ] ) ;
295297 mockFeatureFlagService . isSessionUrlEnabledResponse . next ( true ) ;
296298 mockActivatedRoute . snapshot ! . queryParams = {
297299 [ APP_QUERY_PARAM ] : TEST_APP_1_NAME ,
298300 [ SESSION_QUERY_PARAM ] : SESSION_2_ID ,
299301 } ;
300302 mockSessionService . getSessionResponse . next (
301303 { id : SESSION_2_ID , state : { } , events : [ ] } ) ;
302- fixture = TestBed . createComponent ( ChatComponent ) ;
303- component = fixture . componentInstance ;
304- component . ngOnInit ( ) ;
305- fixture . detectChanges ( ) ;
306- component . selectApp ( TEST_APP_2_NAME ) ;
307- await fixture . whenStable ( ) ;
308304 } ) ;
309- it ( 'should load session from URL' , ( ) => {
310- expect ( mockSessionService . getSession )
311- . toHaveBeenCalledWith (
312- USER_ID ,
313- TEST_APP_2_NAME ,
314- SESSION_2_ID ,
315- ) ;
316- expect ( component . sessionId ) . toBe ( SESSION_2_ID ) ;
305+
306+ describe ( 'on app change' , ( ) => {
307+ beforeEach ( async ( ) => {
308+ fixture = TestBed . createComponent ( ChatComponent ) ;
309+ component = fixture . componentInstance ;
310+ component . ngOnInit ( ) ;
311+ fixture . detectChanges ( ) ;
312+ component . selectApp ( TEST_APP_2_NAME ) ;
313+ await fixture . whenStable ( ) ;
314+ } ) ;
315+ it ( 'should load session from URL' , ( ) => {
316+ expect ( mockSessionService . getSession )
317+ . toHaveBeenCalledWith (
318+ USER_ID ,
319+ TEST_APP_2_NAME ,
320+ SESSION_2_ID ,
321+ ) ;
322+ expect ( component . sessionId ) . toBe ( SESSION_2_ID ) ;
323+ } ) ;
317324 } ) ;
318325 } ) ;
319326
@@ -389,7 +396,8 @@ describe('ChatComponent', () => {
389396 'sessionTab' , [ 'refreshSession' , 'getSession' ] ) ;
390397 sessionTabSpy . refreshSession . and . returnValue (
391398 { id : SESSION_2_ID } as any ) ;
392- spyOn ( component , 'sessionTab' ) . and . returnValue ( sessionTabSpy ) ;
399+ spyOnProperty ( component , 'sessionTab' , 'get' )
400+ . and . returnValue ( sessionTabSpy ) ;
393401 component . deleteSession ( SESSION_1_ID ) ;
394402 } ) ;
395403 it ( 'should delete session' , ( ) => {
@@ -509,11 +517,12 @@ describe('ChatComponent', () => {
509517 component . sessionId = SESSION_1_ID ;
510518 component . messages . set (
511519 [ { role : 'bot' , text : 'response' , eventId : EVENT_1_ID } ] ) ;
512- component . eventData = new Map ( [ [ EVENT_1_ID , { id : EVENT_1_ID } ] ] ) ;
513520 spyOn ( component . sideDrawer ( ) ! , 'open' ) ;
514- component . clickEvent ( 0 ) ;
515521 } ) ;
522+
516523 it ( 'should open side panel with event details' , ( ) => {
524+ component . eventData = new Map ( [ [ EVENT_1_ID , { id : EVENT_1_ID } ] ] ) ;
525+ component . clickEvent ( 0 ) ;
517526 expect ( component . sideDrawer ( ) ! . open ) . toHaveBeenCalled ( ) ;
518527 expect ( component . selectedEvent . id ) . toBe ( EVENT_1_ID ) ;
519528 expect ( mockEventService . getEventTrace ) . toHaveBeenCalledWith ( EVENT_1_ID ) ;
@@ -525,6 +534,31 @@ describe('ChatComponent', () => {
525534 EVENT_1_ID ,
526535 ) ;
527536 } ) ;
537+
538+ it ( 'should call getEventTrace with filter and parse llm request/response' ,
539+ ( ) => {
540+ const invocationId = 'inv-1' ;
541+ const timestamp = 123456789 ;
542+ component . eventData = new Map ( [ [
543+ EVENT_1_ID , {
544+ id : EVENT_1_ID ,
545+ invocationId,
546+ timestampInMillis : timestamp ,
547+ }
548+ ] ] ) ;
549+ const llmRequest = { prompt : 'test prompt' } ;
550+ const llmResponse = { response : 'test response' } ;
551+ mockEventService . getEventTraceResponse . next ( {
552+ 'gcp.vertex.agent.llm_request' : JSON . stringify ( llmRequest ) ,
553+ 'gcp.vertex.agent.llm_response' : JSON . stringify ( llmResponse ) ,
554+ } ) ;
555+
556+ component . clickEvent ( 0 ) ;
557+
558+ expect ( mockEventService . getEventTrace ) . toHaveBeenCalledWith ( EVENT_1_ID ) ;
559+ expect ( component . llmRequest ) . toEqual ( llmRequest ) ;
560+ expect ( component . llmResponse ) . toEqual ( llmResponse ) ;
561+ } ) ;
528562 } ) ;
529563
530564 describe ( 'when updateState() is called' , ( ) => {
@@ -607,13 +641,65 @@ describe('ChatComponent', () => {
607641 expect ( messageCards [ 0 ] . nativeElement . textContent )
608642 . toContain ( TEST_MESSAGE ) ;
609643 } ) ;
644+
645+ describe ( 'when event contains multiple text parts' , ( ) => {
646+ it ( 'should combine consecutive text parts into a single message' ,
647+ async ( ) => {
648+ const sseEvent = {
649+ id : 'event-1' ,
650+ author : 'bot' ,
651+ content :
652+ { role : 'bot' , parts : [ { text : 'Hello ' } , { text : 'World!' } ] } ,
653+ } ;
654+ component . messages . set ( [ ] ) ;
655+ component . userInput = 'test message' ;
656+ await component . sendMessage (
657+ new KeyboardEvent ( 'keydown' , { key : 'Enter' } ) ) ;
658+ mockAgentService . runSseResponse . next ( sseEvent ) ;
659+ fixture . detectChanges ( ) ;
660+
661+ const botMessages =
662+ component . messages ( ) . filter ( m => m . role === 'bot' ) ;
663+ expect ( botMessages . length ) . toBe ( 1 ) ;
664+ expect ( botMessages [ 0 ] . text ) . toBe ( 'Hello World!' ) ;
665+ } ) ;
666+
667+ it ( 'should not combine non-consecutive text parts' , async ( ) => {
668+ const sseEvent = {
669+ id : 'event-1' ,
670+ author : 'bot' ,
671+ content : {
672+ role : 'bot' ,
673+ parts : [
674+ { text : 'Hello ' } ,
675+ { functionCall : { name : 'foo' , args : { } } } ,
676+ { text : 'World!' } ,
677+ ]
678+ } ,
679+ } ;
680+ component . messages . set ( [ ] ) ;
681+ component . userInput = 'test message' ;
682+ await component . sendMessage (
683+ new KeyboardEvent ( 'keydown' , { key : 'Enter' } ) ) ;
684+ mockAgentService . runSseResponse . next ( sseEvent ) ;
685+ fixture . detectChanges ( ) ;
686+
687+ const botMessages =
688+ component . messages ( ) . filter ( m => m . role === 'bot' ) ;
689+ expect ( botMessages . length ) . toBe ( 3 ) ;
690+ expect ( botMessages [ 0 ] . text ) . toBe ( 'Hello ' ) ;
691+ expect ( botMessages [ 1 ] . functionCall ) . toEqual ( { name : 'foo' , args : { } } ) ;
692+ expect ( botMessages [ 2 ] . text ) . toBe ( 'World!' ) ;
693+ } ) ;
694+ } ) ;
610695 } ) ;
611696
612697 describe ( 'when chat-panel emits sendMessage' , ( ) => {
613698 const mockEvent = new KeyboardEvent ( 'keydown' , { key : 'Enter' } ) ;
614699 beforeEach ( ( ) => {
615700 spyOn ( component , 'sendMessage' ) . and . callThrough ( ) ;
616- mockAgentService . runSseResponse . next ( '' ) ;
701+ mockAgentService . runSseResponse . next (
702+ { content : { role : 'bot' , parts : [ ] } } ) ;
617703 const chatPanelDebugEl =
618704 fixture . debugElement . query ( By . directive ( ChatPanelComponent ) ) ;
619705 chatPanelDebugEl . triggerEventHandler ( 'sendMessage' , mockEvent ) ;
0 commit comments