e30e75b5d4
Changesets / Create Version PR (push) Has been cancelled
Deploy Shadcn Registry / Deploy Production (push) Has been cancelled
Template Metrics / LOC + Bundle Size (push) Has been cancelled
Code Quality / Oxlint + Oxfmt (push) Has been cancelled
Code Quality / Template Sync (push) Has been cancelled
Code Quality / Build Changed Packages (push) Has been cancelled
Code Quality / Test Changed Packages (push) Has been cancelled
Deploy Expo Example / Deploy Production (push) Has been cancelled
Deploy Ink Example / Deploy Production (push) Has been cancelled
Python Tests / pytest (assistant-stream, 3.10) (push) Has been cancelled
Python Tests / pytest (assistant-stream, 3.12) (push) Has been cancelled
Python Tests / pytest (assistant-ui-sync-server-api, 3.10) (push) Has been cancelled
Python Tests / pytest (assistant-ui-sync-server-api, 3.12) (push) Has been cancelled
729 lines
20 KiB
TypeScript
729 lines
20 KiB
TypeScript
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
import { defineMcpToolkit } from "@assistant-ui/core/react";
|
|
import { AISDKToolkit } from "./generativeTools";
|
|
import { wrapModelContentEnvelope } from "./modelContentEnvelope";
|
|
|
|
const mocks = vi.hoisted(() => ({
|
|
close: vi.fn(),
|
|
tools: vi.fn(),
|
|
createMCPClient: vi.fn(),
|
|
}));
|
|
|
|
vi.mock("@ai-sdk/mcp", () => ({
|
|
createMCPClient: mocks.createMCPClient,
|
|
}));
|
|
|
|
vi.mock("@ai-sdk/mcp/mcp-stdio", () => ({
|
|
Experimental_StdioMCPTransport: vi.fn((config) => ({
|
|
type: "stdio",
|
|
config,
|
|
})),
|
|
}));
|
|
|
|
const never = <T>() => new Promise<T>(() => {});
|
|
|
|
describe("AISDKToolkit.tools()", () => {
|
|
it("merges frontend tools with toolkit tools", async () => {
|
|
const toolSet = await new AISDKToolkit({
|
|
toolkit: {
|
|
serverTool: {
|
|
type: "backend",
|
|
description: "Server tool",
|
|
parameters: { type: "object", properties: {} },
|
|
execute: async () => "ok",
|
|
} as never,
|
|
},
|
|
}).tools({
|
|
frontend: {
|
|
clientTool: {
|
|
parameters: { type: "object", properties: {} },
|
|
},
|
|
},
|
|
});
|
|
|
|
expect(toolSet.clientTool).toBeDefined();
|
|
expect(toolSet.serverTool?.description).toBe("Server tool");
|
|
expect(toolSet.serverTool?.execute).toBeTypeOf("function");
|
|
});
|
|
|
|
it("keeps a flat toolkit tool named tools", async () => {
|
|
const toolSet = await new AISDKToolkit({
|
|
toolkit: {
|
|
tools: {
|
|
type: "backend",
|
|
description: "Actually a tool, not config",
|
|
parameters: { type: "object", properties: {} },
|
|
execute: async () => "ok",
|
|
} as never,
|
|
},
|
|
}).tools();
|
|
|
|
expect(toolSet.tools?.description).toBe("Actually a tool, not config");
|
|
expect(toolSet.tools?.execute).toBeTypeOf("function");
|
|
});
|
|
|
|
it("converts provider tools without an execute function", async () => {
|
|
const toolSet = await new AISDKToolkit({
|
|
toolkit: {
|
|
web_search: {
|
|
type: "provider",
|
|
providerId: "openai.web_search_preview",
|
|
args: { searchContextSize: "low" },
|
|
},
|
|
},
|
|
}).tools();
|
|
|
|
expect(toolSet.web_search).toMatchObject({
|
|
type: "provider",
|
|
id: "openai.web_search_preview",
|
|
args: { searchContextSize: "low" },
|
|
});
|
|
expect(toolSet.web_search).not.toHaveProperty("inputSchema");
|
|
expect(toolSet.web_search).not.toHaveProperty("execute");
|
|
});
|
|
|
|
it("forwards provider tool parameters and providerOptions when present", async () => {
|
|
const toolSet = await new AISDKToolkit({
|
|
toolkit: {
|
|
web_search: {
|
|
type: "provider",
|
|
providerId: "openai.web_search_preview",
|
|
args: { searchContextSize: "low" },
|
|
parameters: {
|
|
type: "object",
|
|
properties: {
|
|
query: { type: "string" },
|
|
},
|
|
required: ["query"],
|
|
},
|
|
providerOptions: {
|
|
openai: { rankingOptions: { scoreThreshold: 0.5 } },
|
|
},
|
|
},
|
|
},
|
|
}).tools();
|
|
|
|
expect(toolSet.web_search).toMatchObject({
|
|
type: "provider",
|
|
id: "openai.web_search_preview",
|
|
args: { searchContextSize: "low" },
|
|
providerOptions: {
|
|
openai: { rankingOptions: { scoreThreshold: 0.5 } },
|
|
},
|
|
});
|
|
expect(toolSet.web_search).toHaveProperty("inputSchema");
|
|
});
|
|
|
|
it("forwards explicit false supportsDeferredResults", async () => {
|
|
const toolSet = await new AISDKToolkit({
|
|
toolkit: {
|
|
web_search: {
|
|
type: "provider",
|
|
providerId: "openai.web_search_preview",
|
|
args: { searchContextSize: "low" },
|
|
supportsDeferredResults: false,
|
|
},
|
|
},
|
|
}).tools();
|
|
|
|
expect(toolSet.web_search).toMatchObject({
|
|
supportsDeferredResults: false,
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("AISDKToolkit", () => {
|
|
beforeEach(() => {
|
|
mocks.close.mockReset();
|
|
mocks.tools.mockReset();
|
|
mocks.createMCPClient.mockReset();
|
|
});
|
|
|
|
it("loads MCP tools through pooled clients", async () => {
|
|
mocks.tools.mockResolvedValue({ echo: { inputSchema: {} } });
|
|
mocks.createMCPClient.mockResolvedValue({
|
|
tools: mocks.tools,
|
|
close: mocks.close,
|
|
});
|
|
|
|
const toolkit = new AISDKToolkit({
|
|
toolkit: {
|
|
local: {
|
|
type: "mcp",
|
|
server: { type: "http", url: "http://localhost:3001/mcp" },
|
|
},
|
|
},
|
|
});
|
|
|
|
await expect(toolkit.tools()).resolves.toHaveProperty("echo");
|
|
await toolkit.tools();
|
|
|
|
expect(mocks.createMCPClient).toHaveBeenCalledTimes(1);
|
|
expect(mocks.createMCPClient).toHaveBeenCalledWith({
|
|
transport: {
|
|
type: "http",
|
|
url: "http://localhost:3001/mcp",
|
|
},
|
|
});
|
|
expect(mocks.tools).toHaveBeenCalledTimes(2);
|
|
});
|
|
|
|
it("does not connect disabled MCP toolkit entries", async () => {
|
|
const toolkit = new AISDKToolkit({
|
|
toolkit: {
|
|
local: {
|
|
type: "mcp",
|
|
server: { type: "http", url: "http://localhost:3001/mcp" },
|
|
disabled: true,
|
|
},
|
|
},
|
|
});
|
|
|
|
await expect(toolkit.tools()).resolves.toEqual({});
|
|
|
|
expect(mocks.createMCPClient).not.toHaveBeenCalled();
|
|
expect(mocks.tools).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("filters disabled MCP tools from enabled toolkit entries", async () => {
|
|
mocks.tools.mockResolvedValue({
|
|
publicSearch: { inputSchema: {} },
|
|
privateSearch: { inputSchema: {} },
|
|
});
|
|
mocks.createMCPClient.mockResolvedValue({
|
|
tools: mocks.tools,
|
|
close: mocks.close,
|
|
});
|
|
|
|
const toolkit = new AISDKToolkit({
|
|
toolkit: {
|
|
local: {
|
|
type: "mcp",
|
|
server: { type: "http", url: "http://localhost:3001/mcp" },
|
|
tools: {
|
|
privateSearch: {
|
|
disabled: true,
|
|
},
|
|
},
|
|
} as never,
|
|
},
|
|
});
|
|
|
|
await expect(toolkit.tools()).resolves.toEqual({
|
|
publicSearch: { inputSchema: {} },
|
|
});
|
|
|
|
expect(mocks.createMCPClient).toHaveBeenCalledTimes(1);
|
|
expect(mocks.tools).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it("closes pooled MCP clients", async () => {
|
|
mocks.tools.mockResolvedValue({});
|
|
mocks.createMCPClient.mockResolvedValue({
|
|
tools: mocks.tools,
|
|
close: mocks.close,
|
|
});
|
|
|
|
const toolkit = new AISDKToolkit({
|
|
toolkit: {
|
|
local: {
|
|
type: "mcp",
|
|
server: { type: "sse", url: "http://localhost:3001/sse" },
|
|
},
|
|
},
|
|
});
|
|
|
|
await toolkit.tools();
|
|
await toolkit.close();
|
|
|
|
expect(mocks.close).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it("clears pooled MCP clients even when initialization fails", async () => {
|
|
const error = new Error("connect failed");
|
|
const closeError = new Error("close failed");
|
|
const close = vi.fn().mockRejectedValue(closeError);
|
|
mocks.tools.mockResolvedValue({});
|
|
mocks.createMCPClient
|
|
.mockResolvedValueOnce({
|
|
tools: mocks.tools,
|
|
close,
|
|
})
|
|
.mockRejectedValueOnce(error);
|
|
|
|
const toolkit = new AISDKToolkit({
|
|
toolkit: {
|
|
first: {
|
|
type: "mcp",
|
|
server: { type: "http", url: "http://localhost:3001/mcp" },
|
|
},
|
|
second: {
|
|
type: "mcp",
|
|
server: { type: "http", url: "http://localhost:3002/mcp" },
|
|
},
|
|
},
|
|
});
|
|
|
|
const toolsPromise = toolkit.tools();
|
|
await expect(toolkit.close()).rejects.toMatchObject({
|
|
errors: [error, closeError],
|
|
});
|
|
await expect(toolsPromise).rejects.toMatchObject({
|
|
message: 'MCP toolkit entry "second" failed to connect: connect failed',
|
|
cause: error,
|
|
});
|
|
expect(close).toHaveBeenCalledTimes(1);
|
|
|
|
await expect(toolkit.close()).resolves.toBeUndefined();
|
|
});
|
|
|
|
it("evicts failed MCP client initialization so later calls can retry", async () => {
|
|
const error = new Error("connect failed");
|
|
mocks.createMCPClient.mockRejectedValueOnce(error).mockResolvedValueOnce({
|
|
tools: vi.fn().mockResolvedValue({ echo: { inputSchema: {} } }),
|
|
close: mocks.close,
|
|
});
|
|
|
|
const toolkit = new AISDKToolkit({
|
|
toolkit: {
|
|
local: {
|
|
type: "mcp",
|
|
server: { type: "http", url: "http://localhost:3001/mcp" },
|
|
},
|
|
},
|
|
});
|
|
|
|
await expect(toolkit.tools()).rejects.toMatchObject({
|
|
message: 'MCP toolkit entry "local" failed to connect: connect failed',
|
|
cause: error,
|
|
});
|
|
await expect(toolkit.tools()).resolves.toHaveProperty("echo");
|
|
expect(mocks.createMCPClient).toHaveBeenCalledTimes(2);
|
|
});
|
|
|
|
it("times out MCP client creation", async () => {
|
|
vi.useFakeTimers();
|
|
mocks.createMCPClient.mockReturnValue(never());
|
|
|
|
const toolkit = new AISDKToolkit({
|
|
toolkit: {
|
|
docs: {
|
|
type: "mcp",
|
|
server: {
|
|
type: "http",
|
|
url: "http://localhost:3001/mcp",
|
|
connectionTimeout: 10_000,
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
try {
|
|
const toolsPromise = toolkit.tools();
|
|
const expectedRejection = expect(toolsPromise).rejects.toThrow(
|
|
'MCP toolkit entry "docs" timed out while connecting after 10000ms.',
|
|
);
|
|
await vi.advanceTimersByTimeAsync(10_000);
|
|
await expectedRejection;
|
|
|
|
mocks.createMCPClient.mockResolvedValueOnce({
|
|
tools: vi.fn().mockResolvedValue({ echo: { inputSchema: {} } }),
|
|
close: mocks.close,
|
|
});
|
|
await expect(toolkit.tools()).resolves.toHaveProperty("echo");
|
|
expect(mocks.createMCPClient).toHaveBeenCalledTimes(2);
|
|
} finally {
|
|
vi.useRealTimers();
|
|
}
|
|
});
|
|
|
|
it("times out MCP tool listing", async () => {
|
|
vi.useFakeTimers();
|
|
const close = vi.fn().mockResolvedValue(undefined);
|
|
mocks.createMCPClient.mockResolvedValue({
|
|
tools: vi.fn(() => never()),
|
|
close,
|
|
});
|
|
|
|
const toolkit = new AISDKToolkit({
|
|
toolkit: {
|
|
docs: {
|
|
type: "mcp",
|
|
server: {
|
|
type: "http",
|
|
url: "http://localhost:3001/mcp",
|
|
connectionTimeout: 10_000,
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
try {
|
|
const toolsPromise = toolkit.tools();
|
|
const expectedRejection = expect(toolsPromise).rejects.toThrow(
|
|
'MCP toolkit entry "docs" timed out while listing tools after 10000ms.',
|
|
);
|
|
await vi.advanceTimersByTimeAsync(10_000);
|
|
await expectedRejection;
|
|
expect(close).toHaveBeenCalledTimes(1);
|
|
|
|
mocks.createMCPClient.mockResolvedValueOnce({
|
|
tools: vi.fn().mockResolvedValue({ echo: { inputSchema: {} } }),
|
|
close: mocks.close,
|
|
});
|
|
await expect(toolkit.tools()).resolves.toHaveProperty("echo");
|
|
expect(mocks.createMCPClient).toHaveBeenCalledTimes(2);
|
|
} finally {
|
|
vi.useRealTimers();
|
|
}
|
|
});
|
|
|
|
it("includes the MCP toolkit entry name when client initialization fails", async () => {
|
|
const error = new Error("connect failed");
|
|
mocks.createMCPClient.mockRejectedValue(error);
|
|
|
|
const toolkit = new AISDKToolkit({
|
|
toolkit: {
|
|
github: {
|
|
type: "mcp",
|
|
server: { type: "http", url: "http://localhost:3001/mcp" },
|
|
},
|
|
},
|
|
});
|
|
|
|
await expect(toolkit.tools()).rejects.toMatchObject({
|
|
message: 'MCP toolkit entry "github" failed to connect: connect failed',
|
|
cause: error,
|
|
});
|
|
});
|
|
|
|
it("includes the MCP toolkit entry name when listing tools fails", async () => {
|
|
const error = new Error("list failed");
|
|
mocks.tools.mockRejectedValue(error);
|
|
mocks.createMCPClient.mockResolvedValue({
|
|
tools: mocks.tools,
|
|
close: mocks.close,
|
|
});
|
|
|
|
const toolkit = new AISDKToolkit({
|
|
toolkit: {
|
|
docs: {
|
|
type: "mcp",
|
|
server: { type: "http", url: "http://localhost:3001/mcp" },
|
|
},
|
|
},
|
|
});
|
|
|
|
await expect(toolkit.tools()).rejects.toMatchObject({
|
|
message: 'MCP toolkit entry "docs" failed to list tools: list failed',
|
|
cause: error,
|
|
});
|
|
});
|
|
|
|
it("rejects duplicate MCP tool names", async () => {
|
|
mocks.createMCPClient
|
|
.mockResolvedValueOnce({
|
|
tools: vi.fn().mockResolvedValue({ echo: { inputSchema: {} } }),
|
|
close: mocks.close,
|
|
})
|
|
.mockResolvedValueOnce({
|
|
tools: vi.fn().mockResolvedValue({ echo: { inputSchema: {} } }),
|
|
close: mocks.close,
|
|
});
|
|
|
|
const toolkit = new AISDKToolkit({
|
|
toolkit: {
|
|
first: {
|
|
type: "mcp",
|
|
server: { type: "http", url: "http://localhost:3001/mcp" },
|
|
},
|
|
second: {
|
|
type: "mcp",
|
|
server: { type: "http", url: "http://localhost:3002/mcp" },
|
|
},
|
|
},
|
|
});
|
|
|
|
await expect(toolkit.tools()).rejects.toThrow(
|
|
/MCP tool name collision: "echo"/,
|
|
);
|
|
});
|
|
|
|
it("prefixes MCP tool names when configured", async () => {
|
|
const docsExecute = vi.fn().mockResolvedValue("docs result");
|
|
mocks.createMCPClient
|
|
.mockResolvedValueOnce({
|
|
tools: vi.fn().mockResolvedValue({
|
|
search: { inputSchema: {}, execute: docsExecute },
|
|
}),
|
|
close: mocks.close,
|
|
})
|
|
.mockResolvedValueOnce({
|
|
tools: vi.fn().mockResolvedValue({ search: { inputSchema: {} } }),
|
|
close: mocks.close,
|
|
});
|
|
|
|
const toolkit = new AISDKToolkit({
|
|
toolkit: defineMcpToolkit({
|
|
docs: {
|
|
server: { type: "http", url: "http://localhost:3001/mcp" },
|
|
prefix: "docs_",
|
|
},
|
|
github: {
|
|
server: { type: "http", url: "http://localhost:3002/mcp" },
|
|
prefix: "github_",
|
|
},
|
|
}),
|
|
});
|
|
|
|
const toolSet = await toolkit.tools();
|
|
|
|
expect(toolSet).toHaveProperty("docs_search");
|
|
expect(toolSet).toHaveProperty("github_search");
|
|
expect(toolSet).not.toHaveProperty("search");
|
|
|
|
expect(toolSet.docs_search?.execute).toBeTypeOf("function");
|
|
const executeOptions = {
|
|
toolCallId: "call-docs-search",
|
|
messages: [],
|
|
};
|
|
await expect(
|
|
toolSet.docs_search?.execute?.({ query: "assistant-ui" }, executeOptions),
|
|
).resolves.toBe("docs result");
|
|
expect(docsExecute).toHaveBeenCalledWith(
|
|
{ query: "assistant-ui" },
|
|
executeOptions,
|
|
);
|
|
});
|
|
|
|
it("rejects MCP tool names that collide with toolkit tools", async () => {
|
|
mocks.tools.mockResolvedValue({ search: { inputSchema: {} } });
|
|
mocks.createMCPClient.mockResolvedValue({
|
|
tools: mocks.tools,
|
|
close: mocks.close,
|
|
});
|
|
|
|
const toolkit = new AISDKToolkit({
|
|
toolkit: {
|
|
docs: {
|
|
type: "mcp",
|
|
server: { type: "http", url: "http://localhost:3001/mcp" },
|
|
},
|
|
search: {
|
|
type: "backend",
|
|
parameters: { type: "object", properties: {} },
|
|
execute: async () => "local search",
|
|
} as never,
|
|
},
|
|
});
|
|
|
|
await expect(toolkit.tools()).rejects.toThrow(
|
|
'MCP tool "search" from "docs" conflicts with toolkit tool "search"',
|
|
);
|
|
});
|
|
|
|
it("rejects MCP tool names that collide with provider tools", async () => {
|
|
mocks.tools.mockResolvedValue({ web_search: { inputSchema: {} } });
|
|
mocks.createMCPClient.mockResolvedValue({
|
|
tools: mocks.tools,
|
|
close: mocks.close,
|
|
});
|
|
|
|
const toolkit = new AISDKToolkit({
|
|
toolkit: {
|
|
docs: {
|
|
type: "mcp",
|
|
server: { type: "http", url: "http://localhost:3001/mcp" },
|
|
},
|
|
web_search: {
|
|
type: "provider",
|
|
providerId: "openai.web_search_preview",
|
|
args: { searchContextSize: "low" },
|
|
},
|
|
},
|
|
});
|
|
|
|
await expect(toolkit.tools()).rejects.toThrow(
|
|
'MCP tool "web_search" from "docs" conflicts with provider tool "web_search"',
|
|
);
|
|
});
|
|
|
|
it("rejects MCP tool names that collide with uploaded frontend tools", async () => {
|
|
mocks.tools.mockResolvedValue({ clientTool: { inputSchema: {} } });
|
|
mocks.createMCPClient.mockResolvedValue({
|
|
tools: mocks.tools,
|
|
close: mocks.close,
|
|
});
|
|
|
|
const toolkit = new AISDKToolkit({
|
|
toolkit: {
|
|
docs: {
|
|
type: "mcp",
|
|
server: { type: "http", url: "http://localhost:3001/mcp" },
|
|
},
|
|
},
|
|
});
|
|
|
|
await expect(
|
|
toolkit.tools({
|
|
frontend: {
|
|
clientTool: {
|
|
parameters: { type: "object", properties: {} },
|
|
},
|
|
},
|
|
}),
|
|
).rejects.toThrow(
|
|
'MCP tool "clientTool" from "docs" conflicts with frontend tool "clientTool"',
|
|
);
|
|
});
|
|
|
|
it("ignores disabled MCP tools during name collision checks", async () => {
|
|
mocks.createMCPClient
|
|
.mockResolvedValueOnce({
|
|
tools: vi.fn().mockResolvedValue({ echo: { inputSchema: {} } }),
|
|
close: mocks.close,
|
|
})
|
|
.mockResolvedValueOnce({
|
|
tools: vi.fn().mockResolvedValue({ echo: { inputSchema: {} } }),
|
|
close: mocks.close,
|
|
});
|
|
|
|
const toolkit = new AISDKToolkit({
|
|
toolkit: {
|
|
first: {
|
|
type: "mcp",
|
|
server: { type: "http", url: "http://localhost:3001/mcp" },
|
|
tools: {
|
|
echo: {
|
|
disabled: true,
|
|
},
|
|
},
|
|
} as never,
|
|
second: {
|
|
type: "mcp",
|
|
server: { type: "http", url: "http://localhost:3002/mcp" },
|
|
},
|
|
},
|
|
});
|
|
|
|
await expect(toolkit.tools()).resolves.toEqual({
|
|
echo: { inputSchema: {} },
|
|
});
|
|
});
|
|
|
|
it("includes provider tools alongside MCP tools", async () => {
|
|
mocks.tools.mockResolvedValue({ echo: { inputSchema: {} } });
|
|
mocks.createMCPClient.mockResolvedValue({
|
|
tools: mocks.tools,
|
|
close: mocks.close,
|
|
});
|
|
|
|
const toolkit = new AISDKToolkit({
|
|
toolkit: {
|
|
local: {
|
|
type: "mcp",
|
|
server: { type: "http", url: "http://localhost:3001/mcp" },
|
|
},
|
|
web_search: {
|
|
type: "provider",
|
|
providerId: "openai.web_search_preview",
|
|
args: { searchContextSize: "low" },
|
|
supportsDeferredResults: false,
|
|
},
|
|
},
|
|
});
|
|
|
|
await expect(toolkit.tools()).resolves.toMatchObject({
|
|
echo: { inputSchema: {} },
|
|
web_search: {
|
|
type: "provider",
|
|
id: "openai.web_search_preview",
|
|
args: { searchContextSize: "low" },
|
|
supportsDeferredResults: false,
|
|
},
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("AISDKToolkit toModelOutput", () => {
|
|
const createWeatherTools = (toModelOutput?: any) =>
|
|
new AISDKToolkit({
|
|
toolkit: {
|
|
get_weather: {
|
|
...(toModelOutput && { toModelOutput }),
|
|
},
|
|
} as any,
|
|
}).tools();
|
|
|
|
it("adapts assistant-ui model content parts to the AI SDK tool output shape", async () => {
|
|
const tools = await createWeatherTools(({ output }: any) => [
|
|
{ type: "text", text: `Weather card displayed: ${output.location}` },
|
|
]);
|
|
|
|
const output = await tools.get_weather!.toModelOutput!({
|
|
toolCallId: "tc-weather",
|
|
input: {},
|
|
output: { location: "San Francisco" },
|
|
});
|
|
|
|
expect(output).toEqual({
|
|
type: "content",
|
|
value: [{ type: "text", text: "Weather card displayed: San Francisco" }],
|
|
});
|
|
});
|
|
|
|
it("uses stored model content envelopes without re-running the custom projector", async () => {
|
|
let called = false;
|
|
const tools = await createWeatherTools(() => {
|
|
called = true;
|
|
return [{ type: "text", text: "recomputed" }];
|
|
});
|
|
|
|
const output = await tools.get_weather!.toModelOutput!({
|
|
toolCallId: "tc-weather",
|
|
input: {},
|
|
output: wrapModelContentEnvelope({ location: "San Francisco" }, [
|
|
{ type: "text", text: "cached weather receipt" },
|
|
]),
|
|
});
|
|
|
|
expect(called).toBe(false);
|
|
expect(output).toEqual({
|
|
type: "content",
|
|
value: [{ type: "text", text: "cached weather receipt" }],
|
|
});
|
|
});
|
|
|
|
it("falls back to default model output when no custom projector is defined", async () => {
|
|
const tools = await createWeatherTools();
|
|
|
|
const output = await tools.get_weather!.toModelOutput!({
|
|
toolCallId: "tc-weather",
|
|
input: {},
|
|
output: { location: "San Francisco" },
|
|
});
|
|
|
|
expect(output).toEqual({
|
|
type: "json",
|
|
value: { location: "San Francisco" },
|
|
});
|
|
});
|
|
|
|
it("uses stored model content envelopes when no custom projector is defined", async () => {
|
|
const tools = await createWeatherTools();
|
|
|
|
const output = await tools.get_weather!.toModelOutput!({
|
|
toolCallId: "tc-weather",
|
|
input: {},
|
|
output: wrapModelContentEnvelope({ location: "San Francisco" }, [
|
|
{ type: "text", text: "cached weather receipt" },
|
|
]),
|
|
});
|
|
|
|
expect(output).toEqual({
|
|
type: "content",
|
|
value: [{ type: "text", text: "cached weather receipt" }],
|
|
});
|
|
});
|
|
});
|