Skip to content

Commit e202dfd

Browse files
committed
test: update react-core connect replay expectation
1 parent f28ef5f commit e202dfd

1 file changed

Lines changed: 193 additions & 57 deletions

File tree

packages/react-core/src/v2/components/chat/__tests__/CopilotChatActivityRendering.e2e.test.tsx

Lines changed: 193 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import React from "react";
22
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
33
import { z } from "zod";
4+
import { EventType } from "@ag-ui/client";
45
import {
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";
1415
import {
1516
CopilotChatConfigurationProvider,
1617
CopilotKitProvider,
1718
useCopilotKit,
1819
} from "../../../providers";
19-
import { AbstractAgent } from "@ag-ui/client";
20+
import type { AbstractAgent } from "@ag-ui/client";
2021
import { IntelligenceAgent } from "@copilotkit/core";
2122
import { getThreadClone } from "../../../hooks/use-agent";
2223
import { createA2UIMessageRenderer } from "../../../a2ui/A2UIMessageRenderer";
2324
import type { Theme } from "@copilotkit/a2ui-renderer";
2425
import { 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+
41157
vi.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

Comments
 (0)