11import React from "react" ;
22import { fireEvent , render , screen , waitFor } from "@testing-library/react" ;
33import { z } from "zod" ;
4+ import { EventType } from "@ag-ui/client" ;
45import {
56 MockReconnectableAgent ,
67 MockStepwiseAgent ,
@@ -10,34 +11,149 @@ import {
1011 runStartedEvent ,
1112 testId ,
1213} from "../../../__tests__/utils/test-helpers" ;
13- import { ReactActivityMessageRenderer } from "../../../types" ;
14+ import type { ReactActivityMessageRenderer } from "../../../types" ;
1415import {
1516 CopilotChatConfigurationProvider ,
1617 CopilotKitProvider ,
1718 useCopilotKit ,
1819} from "../../../providers" ;
19- import { AbstractAgent } from "@ag-ui/client" ;
20+ import type { AbstractAgent } from "@ag-ui/client" ;
2021import { IntelligenceAgent } from "@copilotkit/core" ;
2122import { getThreadClone } from "../../../hooks/use-agent" ;
2223import { createA2UIMessageRenderer } from "../../../a2ui/A2UIMessageRenderer" ;
2324import type { Theme } from "@copilotkit/a2ui-renderer" ;
2425import { CopilotChat } from ".." ;
2526
26- const { mockWebsandboxCreate, mockWebsandboxDestroy } = vi . hoisted ( ( ) => {
27+ const {
28+ mockWebsandboxCreate,
29+ mockWebsandboxDestroy,
30+ mockPhoenixSockets,
31+ MockPhoenixSocket,
32+ } = vi . hoisted ( ( ) => {
2733 const mockDestroy = vi . fn ( ) ;
28- const mockCreate = vi . fn ( ( ) => ( {
34+ const mockCreate = vi . fn ( ( ... _args : unknown [ ] ) => ( {
2935 iframe : document . createElement ( "iframe" ) ,
3036 promise : Promise . resolve ( ) ,
3137 run : vi . fn ( ) . mockResolvedValue ( undefined ) ,
3238 destroy : mockDestroy ,
3339 } ) ) ;
40+ const mockSockets : MockPhoenixSocket [ ] = [ ] ;
41+
42+ class MockPhoenixPush {
43+ private callbacks = new Map < string , ( response ?: unknown ) => void > ( ) ;
44+
45+ receive (
46+ status : string ,
47+ callback : ( response ?: unknown ) => void ,
48+ ) : MockPhoenixPush {
49+ this . callbacks . set ( status , callback ) ;
50+ return this ;
51+ }
52+
53+ trigger ( status : string , response ?: unknown ) : void {
54+ this . callbacks . get ( status ) ?.( response ) ;
55+ }
56+ }
57+
58+ class MockPhoenixChannel {
59+ public topic : string ;
60+ public params : Record < string , unknown > ;
61+ public left = false ;
62+
63+ private handlers = new Map <
64+ string ,
65+ Array < { ref : number ; callback : ( payload : unknown ) => void } >
66+ > ( ) ;
67+ private joinPush = new MockPhoenixPush ( ) ;
68+ private nextRef = 1 ;
69+
70+ constructor ( topic : string , params : Record < string , unknown > ) {
71+ this . topic = topic ;
72+ this . params = params ;
73+ }
74+
75+ on ( event : string , callback : ( payload : unknown ) => void ) : number {
76+ if ( ! this . handlers . has ( event ) ) {
77+ this . handlers . set ( event , [ ] ) ;
78+ }
79+ const ref = this . nextRef ;
80+ this . nextRef += 1 ;
81+ this . handlers . get ( event ) ?. push ( { ref, callback } ) ;
82+ return ref ;
83+ }
84+
85+ off ( event : string , ref ?: number ) : void {
86+ if ( ref === undefined ) {
87+ this . handlers . delete ( event ) ;
88+ return ;
89+ }
90+ this . handlers . set (
91+ event ,
92+ ( this . handlers . get ( event ) ?? [ ] ) . filter (
93+ ( handler ) => handler . ref !== ref ,
94+ ) ,
95+ ) ;
96+ }
97+
98+ join ( ) : MockPhoenixPush {
99+ return this . joinPush ;
100+ }
101+
102+ leave ( ) : void {
103+ this . left = true ;
104+ }
105+
106+ triggerJoin ( status : string , response ?: unknown ) : void {
107+ this . joinPush . trigger ( status , response ) ;
108+ }
109+
110+ serverPush ( event : string , payload : unknown ) : void {
111+ for ( const { callback } of this . handlers . get ( event ) ?? [ ] ) {
112+ callback ( payload ) ;
113+ }
114+ }
115+ }
116+
117+ class MockPhoenixSocket {
118+ public channels : MockPhoenixChannel [ ] = [ ] ;
119+
120+ constructor (
121+ public url : string ,
122+ public opts : Record < string , unknown > ,
123+ ) {
124+ mockSockets . push ( this ) ;
125+ }
126+
127+ connect ( ) : void { }
128+
129+ disconnect ( ) : void { }
130+
131+ onOpen ( ) : void { }
132+
133+ onError ( ) : void { }
134+
135+ channel (
136+ topic : string ,
137+ params : Record < string , unknown > ,
138+ ) : MockPhoenixChannel {
139+ const channel = new MockPhoenixChannel ( topic , params ) ;
140+ this . channels . push ( channel ) ;
141+ return channel ;
142+ }
143+ }
34144
35145 return {
36146 mockWebsandboxCreate : mockCreate ,
37147 mockWebsandboxDestroy : mockDestroy ,
148+ mockPhoenixSockets : mockSockets ,
149+ MockPhoenixSocket,
38150 } ;
39151} ) ;
40152
153+ vi . mock ( "phoenix" , ( ) => ( {
154+ Socket : MockPhoenixSocket ,
155+ } ) ) ;
156+
41157vi . mock ( "@jetbrains/websandbox" , ( ) => ( {
42158 default : {
43159 create : ( ...args : unknown [ ] ) => mockWebsandboxCreate ( ...args ) ,
@@ -329,66 +445,22 @@ describe("CopilotChat activity message rendering", () => {
329445 } ) ;
330446 } ) ;
331447
332- it ( "restores a completed A2UI surface from an IntelligenceAgent /connect bootstrap plan " , async ( ) => {
448+ it ( "restores a completed A2UI surface from IntelligenceAgent /connect gateway replay " , async ( ) => {
333449 const threadId = testId ( "intelligence-connect-thread" ) ;
334450 const surfaceId = testId ( "intelligence-connect-surface" ) ;
335451 const fetchMock = vi . fn ( ) . mockResolvedValueOnce (
336452 jsonResponse ( {
337- mode : "bootstrap" ,
338- latestEventId : "event-3" ,
339- events : [
340- {
341- type : "RUN_STARTED" ,
342- threadId,
343- run_id : "backend-run-1" ,
344- input : {
345- messages : [
346- {
347- id : testId ( "connect-user-message" ) ,
348- role : "user" ,
349- content : "show me the restored ui" ,
350- } ,
351- ] ,
352- } ,
353- } ,
354- {
355- type : "ACTIVITY_SNAPSHOT" ,
356- messageId : testId ( "connect-a2ui-activity" ) ,
357- activityType : "a2ui-surface" ,
358- content : {
359- a2ui_operations : [
360- {
361- version : "v0.9" ,
362- createSurface : {
363- surfaceId,
364- catalogId :
365- "https://a2ui.org/specification/v0_9/basic_catalog.json" ,
366- } ,
367- } ,
368- {
369- version : "v0.9" ,
370- updateComponents : {
371- surfaceId,
372- components : [
373- {
374- id : "root" ,
375- component : "Text" ,
376- text : "Restored dashboard" ,
377- variant : "body" ,
378- } ,
379- ] ,
380- } ,
381- } ,
382- ] ,
383- } ,
384- } ,
385- {
386- type : "RUN_FINISHED" ,
387- } ,
388- ] ,
453+ threadId,
454+ runId : null ,
455+ joinToken : "join-token-1" ,
456+ realtime : {
457+ clientUrl : "ws://localhost:4000/client" ,
458+ topic : `thread:${ threadId } ` ,
459+ } ,
389460 } ) ,
390461 ) ;
391462 vi . stubGlobal ( "fetch" , fetchMock ) ;
463+ mockPhoenixSockets . length = 0 ;
392464
393465 const agent = new IntelligenceAgent ( {
394466 url : "ws://localhost:4000/client" ,
@@ -409,6 +481,70 @@ describe("CopilotChat activity message rendering", () => {
409481 await waitFor ( ( ) => {
410482 expect ( fetchMock ) . toHaveBeenCalledTimes ( 1 ) ;
411483 } ) ;
484+
485+ await waitFor ( ( ) => {
486+ expect ( mockPhoenixSockets ) . toHaveLength ( 1 ) ;
487+ expect ( mockPhoenixSockets [ 0 ] ?. channels ) . toHaveLength ( 1 ) ;
488+ } ) ;
489+
490+ const channel = mockPhoenixSockets [ 0 ] ! . channels [ 0 ] ! ;
491+ expect ( channel . topic ) . toBe ( `thread:${ threadId } ` ) ;
492+ expect ( channel . params ) . toEqual ( {
493+ stream_mode : "connect" ,
494+ last_seen_event_id : null ,
495+ } ) ;
496+
497+ channel . triggerJoin ( "ok" ) ;
498+ channel . serverPush ( "ag_ui_event" , {
499+ type : EventType . RUN_STARTED ,
500+ threadId,
501+ run_id : "backend-run-1" ,
502+ input : {
503+ messages : [
504+ {
505+ id : testId ( "connect-user-message" ) ,
506+ role : "user" ,
507+ content : "show me the restored ui" ,
508+ } ,
509+ ] ,
510+ } ,
511+ } ) ;
512+ channel . serverPush ( "ag_ui_event" , {
513+ type : EventType . ACTIVITY_SNAPSHOT ,
514+ messageId : testId ( "connect-a2ui-activity" ) ,
515+ activityType : "a2ui-surface" ,
516+ content : {
517+ a2ui_operations : [
518+ {
519+ version : "v0.9" ,
520+ createSurface : {
521+ surfaceId,
522+ catalogId :
523+ "https://a2ui.org/specification/v0_9/basic_catalog.json" ,
524+ } ,
525+ } ,
526+ {
527+ version : "v0.9" ,
528+ updateComponents : {
529+ surfaceId,
530+ components : [
531+ {
532+ id : "root" ,
533+ component : "Text" ,
534+ text : "Restored dashboard" ,
535+ variant : "body" ,
536+ } ,
537+ ] ,
538+ } ,
539+ } ,
540+ ] ,
541+ } ,
542+ } ) ;
543+ channel . serverPush ( "ag_ui_event" , {
544+ type : EventType . RUN_FINISHED ,
545+ } ) ;
546+ channel . serverPush ( "stream_idle" , { latestEventId : "event-3" } ) ;
547+
412548 await waitFor ( ( ) => {
413549 expect ( screen . getByText ( "show me the restored ui" ) ) . toBeDefined ( ) ;
414550 } ) ;
0 commit comments