@assistant-ui/react
Version:
TypeScript/React library for AI Chat
354 lines (290 loc) • 10.3 kB
text/typescript
/**
* @vitest-environment jsdom
*/
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
import { AssistantFrameProvider } from "./AssistantFrameProvider";
import { AssistantFrameHost } from "./AssistantFrameHost";
import { ModelContextRegistry } from "../registry/ModelContextRegistry";
import z from "zod";
describe("AssistantFrame Integration", () => {
let messageHandlers: Map<string, (event: MessageEvent) => void>;
let iframeWindow: Window;
let parentWindow: any;
beforeEach(() => {
messageHandlers = new Map();
// Create a mock parent window that the iframe can post back to
parentWindow = {
postMessage: vi.fn((data: any) => {
// When iframe posts to parent, deliver to parent handler
const parentHandler = messageHandlers.get("parent");
if (parentHandler) {
Promise.resolve().then(() => {
parentHandler({
data,
source: iframeWindow,
origin: "*",
} as MessageEvent);
});
}
}),
};
// Create mock iframe window with proper message routing
iframeWindow = {
postMessage: vi.fn((data: any) => {
// Route message to iframe handler (provider)
const iframeHandler = messageHandlers.get("iframe");
if (iframeHandler) {
Promise.resolve().then(() => {
iframeHandler({
data,
source: parentWindow, // parent window is the source for subscription
origin: "*",
} as MessageEvent);
});
}
}),
} as any;
// Mock window.parent for iframe to broadcast to
Object.defineProperty(window, "parent", {
value: parentWindow,
writable: true,
configurable: true,
});
// Mock window methods for message passing
vi.spyOn(window, "addEventListener").mockImplementation(
(event: string, handler: any) => {
if (event === "message") {
// Store both handlers - we'll determine which is which based on usage
if (!messageHandlers.has("iframe")) {
messageHandlers.set("iframe", handler); // First registration is provider
} else {
messageHandlers.set("parent", handler); // Second is host
}
}
},
);
vi.spyOn(window, "removeEventListener").mockImplementation(() => {});
vi.spyOn(window, "postMessage").mockImplementation(() => {
// This shouldn't be called in our test setup
});
});
afterEach(() => {
// Clean up
vi.restoreAllMocks();
AssistantFrameProvider.dispose();
messageHandlers.clear();
});
it("should establish connection between host and provider", async () => {
// Setup provider in iframe
const registry = new ModelContextRegistry();
const unsubscribe =
AssistantFrameProvider.addModelContextProvider(registry);
// Setup host in parent
const host = new AssistantFrameHost(iframeWindow);
// Wait for connection
await vi.waitFor(() => {
expect(iframeWindow.postMessage).toHaveBeenCalledWith(
expect.objectContaining({
channel: "assistant-ui-frame",
message: expect.objectContaining({
type: "model-context-request",
}),
}),
"*",
);
});
// Clean up
host.dispose();
unsubscribe();
});
it("should sync tools from provider to host", async () => {
// Setup provider with tools
const registry = new ModelContextRegistry();
const toolExecute = vi.fn().mockResolvedValue({ result: "search results" });
registry.addTool({
toolName: "search",
description: "Search the web",
parameters: z.object({ query: z.string() }),
execute: toolExecute,
});
const unsubscribe =
AssistantFrameProvider.addModelContextProvider(registry);
// Setup host
const host = new AssistantFrameHost(iframeWindow);
// Wait for connection and initial sync
await vi.waitFor(() => {
const context = host.getModelContext();
expect(context.tools).toBeDefined();
expect(context.tools?.search).toBeDefined();
expect(context.tools?.search.description).toBe("Search the web");
});
// Clean up
host.dispose();
unsubscribe();
});
it("should execute tools through the frame boundary", async () => {
// Setup provider with executable tool
const registry = new ModelContextRegistry();
const toolExecute = vi
.fn()
.mockResolvedValue({ results: ["result1", "result2"] });
registry.addTool({
toolName: "search",
description: "Search the web",
parameters: z.object({ query: z.string() }),
execute: toolExecute,
});
const unsubscribe =
AssistantFrameProvider.addModelContextProvider(registry);
// Setup host
const host = new AssistantFrameHost(iframeWindow);
// Wait for tools to be available
await vi.waitFor(() => {
const context = host.getModelContext();
expect(context.tools?.search).toBeDefined();
});
// Execute tool through host
const context = host.getModelContext();
const searchTool = context.tools?.search;
const resultPromise = searchTool!.execute!(
{ query: "test query" },
{} as any,
);
// Wait for tool execution
await vi.waitFor(() => {
expect(toolExecute).toHaveBeenCalledWith(
{ query: "test query" },
expect.objectContaining({
toolCallId: expect.any(String),
abortSignal: expect.any(AbortSignal),
}),
);
});
const result = await resultPromise;
expect(result).toEqual({ results: ["result1", "result2"] });
// Clean up
host.dispose();
unsubscribe();
});
it("should handle tool execution errors", async () => {
// Setup provider with failing tool
const registry = new ModelContextRegistry();
const toolExecute = vi
.fn()
.mockRejectedValue(new Error("Tool execution failed"));
registry.addTool({
toolName: "failingTool",
description: "A tool that fails",
parameters: z.object({ input: z.string() }),
execute: toolExecute,
});
const unsubscribe =
AssistantFrameProvider.addModelContextProvider(registry);
// Setup host
const host = new AssistantFrameHost(iframeWindow);
// Wait for tools to be available
await vi.waitFor(() => {
const context = host.getModelContext();
expect(context.tools?.failingTool).toBeDefined();
});
// Execute tool and expect error
const context = host.getModelContext();
const failingTool = context.tools?.failingTool;
await expect(
failingTool!.execute!({ input: "test" }, {} as any),
).rejects.toThrow("Tool execution failed");
// Clean up
host.dispose();
unsubscribe();
});
it("should handle multiple providers", async () => {
// Setup multiple providers
const registry1 = new ModelContextRegistry();
const registry2 = new ModelContextRegistry();
registry1.addTool({
toolName: "tool1",
description: "First tool",
parameters: z.object({ input: z.string() }),
execute: async () => ({ from: "tool1" }),
});
registry2.addTool({
toolName: "tool2",
description: "Second tool",
parameters: z.object({ input: z.string() }),
execute: async () => ({ from: "tool2" }),
});
const unsub1 = AssistantFrameProvider.addModelContextProvider(registry1);
const unsub2 = AssistantFrameProvider.addModelContextProvider(registry2);
// Setup host
const host = new AssistantFrameHost(iframeWindow);
// Wait for both tools to be available
await vi.waitFor(() => {
const context = host.getModelContext();
expect(context.tools?.tool1).toBeDefined();
expect(context.tools?.tool2).toBeDefined();
});
// Clean up
host.dispose();
unsub1();
unsub2();
});
it("should merge system instructions from multiple providers", async () => {
// Setup providers with system instructions
const registry1 = new ModelContextRegistry();
const registry2 = new ModelContextRegistry();
registry1.addInstruction("You are a helpful assistant.");
registry2.addInstruction("Always be concise.");
const unsub1 = AssistantFrameProvider.addModelContextProvider(registry1);
const unsub2 = AssistantFrameProvider.addModelContextProvider(registry2);
// Setup host
const host = new AssistantFrameHost(iframeWindow);
// Wait for instructions to be synced
await vi.waitFor(() => {
const context = host.getModelContext();
expect(context.system).toBeDefined();
expect(context.system).toContain("You are a helpful assistant.");
expect(context.system).toContain("Always be concise.");
});
// Clean up
host.dispose();
unsub1();
unsub2();
});
it("should act as empty ModelContextProvider when iframe has no providers", async () => {
// Don't register any providers in the iframe
// This simulates an iframe that doesn't respond to model-context requests
// Setup host
const host = new AssistantFrameHost(iframeWindow);
// Host should immediately return empty context
const context = host.getModelContext();
expect(context).toEqual({});
// Wait a bit to ensure no errors occur
await new Promise((resolve) => setTimeout(resolve, 100));
// Context should still be empty
expect(host.getModelContext()).toEqual({});
// Clean up
host.dispose();
});
it("should clean up properly on dispose", async () => {
// Setup provider
const registry = new ModelContextRegistry();
const unsubscribe =
AssistantFrameProvider.addModelContextProvider(registry);
// Setup host
const host = new AssistantFrameHost(iframeWindow);
// Wait for connection
await vi.waitFor(() => {
expect(iframeWindow.postMessage).toHaveBeenCalled();
});
// Dispose host
host.dispose();
// Verify event listener was removed (no unsubscribe message in new design)
expect(window.removeEventListener).toHaveBeenCalledWith(
"message",
expect.any(Function),
);
// Clean up provider
unsubscribe();
AssistantFrameProvider.dispose();
});
});