@copilotkit/runtime-client-gql
Version:
<img src="https://github.com/user-attachments/assets/0a6b64d9-e193-4940-a3f6-60334ac34084" alt="banner" style="border-radius: 12px; border: 2px solid #d6d4fa;" />
845 lines (736 loc) • 24.4 kB
text/typescript
import { describe, test, expect, vi } from "vitest";
import * as gql from "../client";
import { MessageStatusCode } from "../graphql/@generated/graphql";
import {
gqlToAGUI,
gqlTextMessageToAGUIMessage,
gqlResultMessageToAGUIMessage,
gqlImageMessageToAGUIMessage,
} from "./gql-to-agui";
describe("message-conversion", () => {
describe("gqlTextMessageToAGUIMessage", () => {
test("should convert developer message", () => {
const gqlMessage = new gql.TextMessage({
id: "dev-message-id",
content: "Hello from developer",
role: gql.Role.Developer,
});
const result = gqlTextMessageToAGUIMessage(gqlMessage);
expect(result).toEqual({
id: "dev-message-id",
role: "developer",
content: "Hello from developer",
});
});
test("should convert system message", () => {
const gqlMessage = new gql.TextMessage({
id: "system-message-id",
content: "System instruction",
role: gql.Role.System,
});
const result = gqlTextMessageToAGUIMessage(gqlMessage);
expect(result).toEqual({
id: "system-message-id",
role: "system",
content: "System instruction",
});
});
test("should convert assistant message", () => {
const gqlMessage = new gql.TextMessage({
id: "assistant-message-id",
content: "Assistant response",
role: gql.Role.Assistant,
});
const result = gqlTextMessageToAGUIMessage(gqlMessage);
expect(result).toEqual({
id: "assistant-message-id",
role: "assistant",
content: "Assistant response",
});
});
test("should throw error for unknown role", () => {
const gqlMessage = new gql.TextMessage({
id: "unknown-message-id",
content: "Unknown message",
role: "unknown" as any,
});
expect(() => gqlTextMessageToAGUIMessage(gqlMessage)).toThrow("Unknown message role");
});
});
describe("gqlResultMessageToAGUIMessage", () => {
test("should convert result message to tool message", () => {
const gqlMessage = new gql.ResultMessage({
id: "result-id",
result: "Function result data",
actionExecutionId: "action-exec-123",
actionName: "testAction",
});
const result = gqlResultMessageToAGUIMessage(gqlMessage);
expect(result).toEqual({
id: "result-id",
role: "tool",
content: "Function result data",
toolCallId: "action-exec-123",
toolName: "testAction",
});
});
});
describe("gqlToAGUI", () => {
test("should convert an array of text messages", () => {
const gqlMessages = [
new gql.TextMessage({
id: "dev-1",
content: "Hello",
role: gql.Role.Developer,
}),
new gql.TextMessage({
id: "assistant-1",
content: "Hi there",
role: gql.Role.Assistant,
}),
];
const result = gqlToAGUI(gqlMessages);
expect(result).toHaveLength(2);
expect(result[0]).toEqual({
id: "dev-1",
role: "developer",
content: "Hello",
});
expect(result[1]).toEqual({
id: "assistant-1",
role: "assistant",
content: "Hi there",
});
});
test("should handle agent state messages", () => {
const gqlMessages = [new gql.AgentStateMessage({ id: "agent-state-1" })];
const result = gqlToAGUI(gqlMessages);
expect(result).toHaveLength(1);
expect(result[0]).toEqual({
id: "agent-state-1",
role: "assistant",
});
});
// test("should throw error for unknown message type", () => {
// // Create a message with unknown type
// const unknownMessage = new gql.Message({ id: "unknown-1" });
// // Override the type checking methods to simulate unknown type
// unknownMessage.isTextMessage = () => false as any;
// unknownMessage.isResultMessage = () => false as any;
// unknownMessage.isActionExecutionMessage = () => false as any;
// unknownMessage.isAgentStateMessage = () => false as any;
// unknownMessage.isImageMessage = () => false as any;
// expect(() => gqlToAGUI([unknownMessage])).toThrow("Unknown message type");
// });
test("should handle a mix of message types", () => {
const gqlMessages = [
new gql.TextMessage({
id: "dev-1",
content: "Run action",
role: gql.Role.Developer,
}),
new gql.TextMessage({
id: "assistant-1",
content: "I'll run the action",
role: gql.Role.Assistant,
}),
new gql.ResultMessage({
id: "result-1",
result: "Action result",
actionExecutionId: "action-exec-1",
actionName: "testAction",
}),
];
const result = gqlToAGUI(gqlMessages);
expect(result).toHaveLength(3);
expect(result[0]).toEqual({
id: "dev-1",
role: "developer",
content: "Run action",
});
expect(result[1]).toEqual({
id: "assistant-1",
role: "assistant",
content: "I'll run the action",
});
expect(result[2]).toEqual({
id: "result-1",
role: "tool",
content: "Action result",
toolCallId: "action-exec-1",
toolName: "testAction",
});
});
test("should handle action execution messages with parent messages", () => {
const assistantMsg = new gql.TextMessage({
id: "assistant-1",
content: "I'll execute an action",
role: gql.Role.Assistant,
});
const actionExecMsg = new gql.ActionExecutionMessage({
id: "action-1",
name: "testAction",
arguments: { param: "value" },
parentMessageId: "assistant-1",
});
const result = gqlToAGUI([assistantMsg, actionExecMsg]);
// Now we expect 2 messages: the original assistant message and the action execution message
expect(result).toHaveLength(2);
expect(result[0]).toEqual({
id: "assistant-1",
role: "assistant",
content: "I'll execute an action",
});
expect(result[1]).toEqual({
id: "action-1",
role: "assistant",
name: "testAction",
toolCalls: [
{
id: "action-1",
function: {
name: "testAction",
arguments: JSON.stringify({ param: "value" }),
},
type: "function",
},
],
});
});
test("should handle multiple action execution messages for the same parent", () => {
const assistantMsg = new gql.TextMessage({
id: "assistant-1",
content: "I'll execute multiple actions",
role: gql.Role.Assistant,
});
const action1 = new gql.ActionExecutionMessage({
id: "action-1",
name: "firstAction",
arguments: { param: "value1" },
parentMessageId: "assistant-1",
});
const action2 = new gql.ActionExecutionMessage({
id: "action-2",
name: "secondAction",
arguments: { param: "value2" },
parentMessageId: "assistant-1",
});
const result = gqlToAGUI([assistantMsg, action1, action2]);
// Now we expect 3 messages: the original assistant message and 2 separate action execution messages
expect(result).toHaveLength(3);
expect(result[0]).toEqual({
id: "assistant-1",
role: "assistant",
content: "I'll execute multiple actions",
});
expect(result[1]).toEqual({
id: "action-1",
role: "assistant",
name: "firstAction",
toolCalls: [
{
id: "action-1",
function: {
name: "firstAction",
arguments: JSON.stringify({ param: "value1" }),
},
type: "function",
},
],
});
expect(result[2]).toEqual({
id: "action-2",
role: "assistant",
name: "secondAction",
toolCalls: [
{
id: "action-2",
function: {
name: "secondAction",
arguments: JSON.stringify({ param: "value2" }),
},
type: "function",
},
],
});
});
test("should not add toolCalls to non-assistant messages", () => {
const developerMsg = new gql.TextMessage({
id: "dev-1",
content: "Developer message",
role: gql.Role.Developer,
});
const actionExecMsg = new gql.ActionExecutionMessage({
id: "action-1",
name: "testAction",
arguments: { param: "value" },
parentMessageId: "dev-1", // This should be ignored since parent is not assistant
});
const result = gqlToAGUI([developerMsg, actionExecMsg]);
// Now we expect 2 messages: the developer message and the action execution as assistant message
expect(result).toHaveLength(2);
expect(result[0]).toEqual({
id: "dev-1",
role: "developer",
content: "Developer message",
});
// The action execution becomes its own assistant message regardless of parent
expect(result[1]).toEqual({
id: "action-1",
role: "assistant",
name: "testAction",
toolCalls: [
{
id: "action-1",
function: {
name: "testAction",
arguments: JSON.stringify({ param: "value" }),
},
type: "function",
},
],
});
});
test("should handle action execution messages without actions context", () => {
const actionExecMsg = new gql.ActionExecutionMessage({
id: "action-1",
name: "testAction",
arguments: { param: "value" },
});
const result = gqlToAGUI([actionExecMsg]);
expect(result).toHaveLength(1);
expect(result[0]).toEqual({
id: "action-1",
role: "assistant",
name: "testAction",
toolCalls: [
{
id: "action-1",
function: {
name: "testAction",
arguments: JSON.stringify({ param: "value" }),
},
type: "function",
},
],
});
// Should not have render functions without actions context
expect(result[0]).not.toHaveProperty("render");
expect(result[0]).not.toHaveProperty("renderAndWaitForResponse");
});
test("should handle action execution messages with actions context and render functions", () => {
const actionExecMsg = new gql.ActionExecutionMessage({
id: "action-1",
name: "testAction",
arguments: { param: "value" },
status: { code: MessageStatusCode.Pending },
});
const mockRender = vi.fn();
const mockRenderAndWaitForResponse = (props: any) => "Test Render With Response";
const actions = {
testAction: {
name: "testAction",
render: mockRender,
renderAndWaitForResponse: mockRenderAndWaitForResponse,
},
};
const result = gqlToAGUI([actionExecMsg], actions);
expect(result).toHaveLength(1);
expect(result[0]).toMatchObject({
id: "action-1",
role: "assistant",
name: "testAction",
content: "",
toolCalls: [
{
id: "action-1",
function: {
name: "testAction",
arguments: JSON.stringify({ param: "value" }),
},
type: "function",
},
],
});
// Should have generativeUI function
expect(result[0]).toHaveProperty("generativeUI");
expect(typeof (result[0] as any).generativeUI).toBe("function");
});
test("should provide correct status in generativeUI function props", () => {
const actionExecMsg = new gql.ActionExecutionMessage({
id: "action-1",
name: "testAction",
arguments: { param: "value" },
status: { code: MessageStatusCode.Pending },
});
const mockRender = vi.fn();
const actions = {
testAction: {
name: "testAction",
render: mockRender,
},
};
const result = gqlToAGUI([actionExecMsg], actions);
// Call the generativeUI function
(result[0] as any).generativeUI?.();
expect(mockRender).toHaveBeenCalledWith({
status: "inProgress",
args: { param: "value" },
result: undefined,
respond: expect.any(Function),
messageId: "action-1",
});
});
test("should provide executing status when not pending", () => {
const actionExecMsg = new gql.ActionExecutionMessage({
id: "action-1",
name: "testAction",
arguments: { param: "value" },
status: { code: MessageStatusCode.Success },
});
const mockRender = vi.fn();
const actions = {
testAction: {
name: "testAction",
render: mockRender,
},
};
const result = gqlToAGUI([actionExecMsg], actions);
// Call the generativeUI function
(result[0] as any).generativeUI?.();
expect(mockRender).toHaveBeenCalledWith({
status: "executing",
args: { param: "value" },
result: undefined,
respond: expect.any(Function),
messageId: "action-1",
});
});
test("should provide complete status when result is available", () => {
const actionExecMsg = new gql.ActionExecutionMessage({
id: "action-1",
name: "testAction",
arguments: { param: "value" },
status: { code: MessageStatusCode.Success },
});
const resultMsg = new gql.ResultMessage({
id: "result-1",
result: "Action completed successfully",
actionExecutionId: "action-1",
actionName: "testAction",
});
const mockRender = vi.fn();
const actions = {
testAction: {
name: "testAction",
render: mockRender,
},
};
const result = gqlToAGUI([actionExecMsg, resultMsg], actions);
// Find the action execution message result (not the tool result)
const actionMessage = result.find((msg) => msg.role === "assistant" && "toolCalls" in msg);
// Call the generativeUI function
(actionMessage as any)?.generativeUI?.();
expect(mockRender).toHaveBeenCalledWith({
status: "complete",
args: { param: "value" },
result: "Action completed successfully",
respond: expect.any(Function),
messageId: "action-1",
});
});
test("should handle generativeUI function props override", () => {
const actionExecMsg = new gql.ActionExecutionMessage({
id: "action-1",
name: "testAction",
arguments: { param: "value" },
status: { code: MessageStatusCode.Pending },
});
const mockRender = vi.fn();
const actions = {
testAction: {
name: "testAction",
render: mockRender,
},
};
const result = gqlToAGUI([actionExecMsg], actions);
// Call with custom props
(result[0] as any).generativeUI?.({
status: "custom",
customProp: "test",
respond: () => "custom respond",
});
expect(mockRender).toHaveBeenCalledWith({
status: "custom",
args: { param: "value" },
result: undefined,
respond: expect.any(Function),
customProp: "test",
messageId: "action-1",
});
});
test("should handle missing render functions gracefully", () => {
const actionExecMsg = new gql.ActionExecutionMessage({
id: "action-1",
name: "testAction",
arguments: { param: "value" },
});
const actions = {
testAction: {
name: "testAction",
// No render functions provided
},
};
const result = gqlToAGUI([actionExecMsg], actions);
expect(result[0]).toMatchObject({
id: "action-1",
role: "assistant",
name: "testAction",
content: "",
toolCalls: [
{
id: "action-1",
function: {
name: "testAction",
arguments: JSON.stringify({ param: "value" }),
},
type: "function",
},
],
});
// Should have undefined generativeUI functions
expect((result[0] as any).generativeUI).toBeUndefined();
});
test("should handle action not found in actions context", () => {
const actionExecMsg = new gql.ActionExecutionMessage({
id: "action-1",
name: "unknownAction",
arguments: { param: "value" },
});
const actions = {
testAction: {
name: "testAction",
render: () => "Test",
},
};
const result = gqlToAGUI([actionExecMsg], actions);
expect(result).toHaveLength(1);
expect(result[0]).toEqual({
id: "action-1",
role: "assistant",
name: "unknownAction",
toolCalls: [
{
id: "action-1",
function: {
name: "unknownAction",
arguments: JSON.stringify({ param: "value" }),
},
type: "function",
},
],
});
// Should not have generativeUI functions when action not found
expect(result[0]).not.toHaveProperty("generativeUI");
});
test("should handle agent state messages with coAgentStateRenders", () => {
const agentStateMsg = new gql.AgentStateMessage({
id: "agent-state-1",
agentName: "testAgent",
state: { status: "running", data: "test data" },
role: gql.Role.Assistant,
});
const mockRender = vi.fn();
const coAgentStateRenders = {
testAgent: {
name: "testAgent",
render: mockRender,
},
};
const result = gqlToAGUI([agentStateMsg], undefined, coAgentStateRenders);
expect(result).toHaveLength(1);
expect(result[0]).toEqual({
id: "agent-state-1",
role: "assistant",
agentName: "testAgent",
state: { status: "running", data: "test data" },
generativeUI: expect.any(Function),
});
// Should have generativeUI function
expect(result[0]).toHaveProperty("generativeUI");
expect(typeof (result[0] as any).generativeUI).toBe("function");
// Call the generativeUI function
(result[0] as any).generativeUI?.();
expect(mockRender).toHaveBeenCalledWith({
state: { status: "running", data: "test data" },
});
});
test("should handle agent state messages without coAgentStateRenders", () => {
const agentStateMsg = new gql.AgentStateMessage({
id: "agent-state-1",
agentName: "testAgent",
state: { status: "running", data: "test data" },
role: gql.Role.Assistant,
});
const result = gqlToAGUI([agentStateMsg]);
expect(result).toHaveLength(1);
expect(result[0]).toEqual({
id: "agent-state-1",
role: "assistant",
agentName: "testAgent",
state: { status: "running", data: "test data" },
});
// Should not have generativeUI functions without coAgentStateRenders
expect(result[0]).not.toHaveProperty("generativeUI");
});
test("should handle agent state messages with agent not found in coAgentStateRenders", () => {
const agentStateMsg = new gql.AgentStateMessage({
id: "agent-state-1",
agentName: "unknownAgent",
state: { status: "running", data: "test data" },
role: gql.Role.Assistant,
});
const coAgentStateRenders = {
testAgent: {
name: "testAgent",
render: () => "Test",
},
};
const result = gqlToAGUI([agentStateMsg], undefined, coAgentStateRenders);
expect(result).toHaveLength(1);
expect(result[0]).toEqual({
id: "agent-state-1",
role: "assistant",
agentName: "unknownAgent",
state: { status: "running", data: "test data" },
});
// Should not have generativeUI functions when agent not found
expect(result[0]).not.toHaveProperty("generativeUI");
});
test("should handle user role messages", () => {
const userMsg = new gql.TextMessage({
id: "user-1",
content: "Hello from user",
role: gql.Role.User,
});
const result = gqlToAGUI([userMsg]);
expect(result).toHaveLength(1);
expect(result[0]).toEqual({
id: "user-1",
role: "user",
content: "Hello from user",
});
});
test("should handle mixed message types including agent state messages", () => {
const textMsg = new gql.TextMessage({
id: "text-1",
content: "Hello",
role: gql.Role.Assistant,
});
const agentStateMsg = new gql.AgentStateMessage({
id: "agent-state-1",
agentName: "testAgent",
state: { status: "running" },
role: gql.Role.Assistant,
});
const mockRender = vi.fn();
const coAgentStateRenders = {
testAgent: {
name: "testAgent",
render: mockRender,
},
};
const result = gqlToAGUI([textMsg, agentStateMsg], undefined, coAgentStateRenders);
expect(result).toHaveLength(2);
expect(result[0]).toEqual({
id: "text-1",
role: "assistant",
content: "Hello",
});
expect(result[1]).toMatchObject({
id: "agent-state-1",
role: "assistant",
agentName: "testAgent",
state: { status: "running" },
generativeUI: expect.any(Function),
});
expect(result[1]).toHaveProperty("generativeUI");
});
});
describe("gqlImageMessageToAGUIMessage", () => {
test("should throw error for invalid image format", () => {
const invalidImageMsg = new gql.ImageMessage({
id: "img-1",
format: "bmp", // not in VALID_IMAGE_FORMATS
bytes: "somebase64string",
role: gql.Role.User,
});
expect(() => gqlImageMessageToAGUIMessage(invalidImageMsg)).toThrow("Invalid image format");
});
test("should throw error for empty image bytes", () => {
const invalidImageMsg = new gql.ImageMessage({
id: "img-2",
format: "jpeg",
bytes: "",
role: gql.Role.User,
});
expect(() => gqlImageMessageToAGUIMessage(invalidImageMsg)).toThrow(
"Image bytes must be a non-empty string",
);
});
test("should convert valid image message", () => {
const validImageMsg = new gql.ImageMessage({
id: "img-3",
format: "jpeg",
bytes: "somebase64string",
role: gql.Role.User,
});
const result = gqlImageMessageToAGUIMessage(validImageMsg);
expect(result).toMatchObject({
id: "img-3",
role: "user",
content: "",
image: {
format: "jpeg",
bytes: "somebase64string",
},
});
});
test("should convert valid user image message", () => {
const validImageMsg = new gql.ImageMessage({
id: "img-user-1",
format: "jpeg",
bytes: "userbase64string",
role: gql.Role.User,
});
const result = gqlImageMessageToAGUIMessage(validImageMsg);
expect(result).toMatchObject({
id: "img-user-1",
role: "user",
content: "",
image: {
format: "jpeg",
bytes: "userbase64string",
},
});
});
test("should convert valid assistant image message", () => {
const validImageMsg = new gql.ImageMessage({
id: "img-assistant-1",
format: "png",
bytes: "assistantbase64string",
role: gql.Role.Assistant,
});
const result = gqlImageMessageToAGUIMessage(validImageMsg);
expect(result).toMatchObject({
id: "img-assistant-1",
role: "assistant",
content: "",
image: {
format: "png",
bytes: "assistantbase64string",
},
});
});
});
});