Fresh history cut from cynarAI/evoo antigravity-handoff/project-timeline (Voice First + project timeline). Brand docs/titles as evOo; keep @rakazo/* packages as tech debt. Excludes node_modules, caches, secrets, voice blobs.
455 lines
16 KiB
TypeScript
455 lines
16 KiB
TypeScript
import type { ConnectorTool } from "@rakazo/adapter-kit";
|
|
import { describe, expect, it, vi } from "vitest";
|
|
import {
|
|
assertConnectorToolArgs,
|
|
catalogEntries,
|
|
catalogGroupLabel,
|
|
DIRECT_TOOL_LIMIT,
|
|
disambiguateInstalledToolNames,
|
|
executeLazyCatalogControl,
|
|
formatNameIndexText,
|
|
lazyCatalogTools,
|
|
loadCatalogEntry,
|
|
NAME_INDEX_MAX_NAMES,
|
|
resolveCatalogCall,
|
|
SELECTED_SCHEMA_MAX_BYTES,
|
|
searchCatalog,
|
|
TOOL_SEARCH_LIMIT,
|
|
uniquifyInstalledToolName,
|
|
} from "./lazy-tool-catalog.js";
|
|
|
|
function tools(count: number, group = "source"): ConnectorTool[] {
|
|
return Array.from({ length: count }, (_, index) => ({
|
|
name: `tool_${String(index).padStart(2, "0")}`,
|
|
description: "Shared catalog result",
|
|
inputSchema: {
|
|
type: "object",
|
|
properties: { value: { type: "string" } },
|
|
required: ["value"],
|
|
},
|
|
route: {
|
|
connectorId: "test",
|
|
resourceId: `source-${index}`,
|
|
toolName: `tool-${index}`,
|
|
catalogGroup: group,
|
|
},
|
|
}));
|
|
}
|
|
|
|
function multiGroupTools(): ConnectorTool[] {
|
|
return [
|
|
{
|
|
name: "mcp__gmail__send",
|
|
description: "Send mail",
|
|
inputSchema: { type: "object", properties: { to: { type: "string" } } },
|
|
route: {
|
|
connectorId: "mcp",
|
|
resourceId: "srv-gmail",
|
|
toolName: "send",
|
|
catalogGroup: "gmail",
|
|
},
|
|
},
|
|
{
|
|
name: "mcp__gmail__read",
|
|
description: "Read mail",
|
|
inputSchema: { type: "object", properties: {} },
|
|
route: {
|
|
connectorId: "mcp",
|
|
resourceId: "srv-gmail",
|
|
toolName: "read",
|
|
catalogGroup: "gmail",
|
|
},
|
|
},
|
|
{
|
|
name: "mcp__gmail__list",
|
|
description: "List mail",
|
|
inputSchema: { type: "object", properties: {} },
|
|
route: {
|
|
connectorId: "mcp",
|
|
resourceId: "srv-gmail",
|
|
toolName: "list",
|
|
catalogGroup: "gmail",
|
|
},
|
|
},
|
|
{
|
|
name: "mcp__hubspot__list_contacts",
|
|
description: "List contacts",
|
|
inputSchema: { type: "object", properties: {} },
|
|
route: {
|
|
connectorId: "mcp",
|
|
resourceId: "srv-hubspot",
|
|
toolName: "list_contacts",
|
|
catalogGroup: "hubspot",
|
|
},
|
|
},
|
|
{
|
|
name: "mcp__hubspot__get_deal",
|
|
description: "Get deal",
|
|
inputSchema: { type: "object", properties: {} },
|
|
route: {
|
|
connectorId: "mcp",
|
|
resourceId: "srv-hubspot",
|
|
toolName: "get_deal",
|
|
catalogGroup: "hubspot",
|
|
},
|
|
},
|
|
];
|
|
}
|
|
|
|
describe("lazy tool catalog", () => {
|
|
it("exposes tools directly at the limit and switches to lazy wrappers above it", () => {
|
|
expect(tools(DIRECT_TOOL_LIMIT)).toHaveLength(20);
|
|
expect(tools(DIRECT_TOOL_LIMIT + 1)).toHaveLength(21);
|
|
expect(lazyCatalogTools("connectors", "mcp", "MCP")).toHaveLength(3);
|
|
expect(lazyCatalogTools("installed", "installed", "API").map((tool) => tool.name)).toEqual([
|
|
"installed_search_tools",
|
|
"installed_load_tool",
|
|
"installed_execute_tool",
|
|
]);
|
|
expect(lazyCatalogTools("connectors", "mcp", "MCP").map((tool) => tool.name)).toEqual([
|
|
"connectors_search_tools",
|
|
"connectors_load_tool",
|
|
"connectors_execute_tool",
|
|
]);
|
|
expect(lazyCatalogTools("connectors", "mcp", "MCP")[0]?.description).toContain(
|
|
"Empty query lists sources",
|
|
);
|
|
expect(lazyCatalogTools("connectors", "mcp", "MCP")[0]?.inputSchema).not.toMatchObject({
|
|
required: expect.arrayContaining(["query"]),
|
|
});
|
|
});
|
|
|
|
it("embeds a compact name index in the search description when it fits", () => {
|
|
const entries = catalogEntries(multiGroupTools());
|
|
const [search] = lazyCatalogTools("connectors", "mcp", "MCP", entries);
|
|
expect(search?.description).toContain("gmail:");
|
|
expect(search?.description).toContain("list, read, send");
|
|
expect(search?.description).toContain("hubspot:");
|
|
expect(search?.description).toContain("get_deal, list_contacts");
|
|
expect(search?.description).not.toContain("inputSchema");
|
|
expect(search?.description).not.toContain("Send mail");
|
|
});
|
|
|
|
it("returns an empty catalog as an empty tool list", () => {
|
|
expect(catalogEntries([])).toEqual([]);
|
|
expect(searchCatalog([], { query: "anything" })).toEqual({ tools: [] });
|
|
});
|
|
|
|
it("lists grouped names for an empty query without dumping schemas", () => {
|
|
const result = searchCatalog(catalogEntries(multiGroupTools()), {});
|
|
expect(result).toEqual({
|
|
index: [
|
|
{ group: "gmail", names: ["list", "read", "send"] },
|
|
{ group: "hubspot", names: ["get_deal", "list_contacts"] },
|
|
],
|
|
});
|
|
expect(JSON.stringify(result)).not.toContain("inputSchema");
|
|
expect(JSON.stringify(result)).not.toContain("Send mail");
|
|
});
|
|
|
|
it("expands one group to its names", () => {
|
|
expect(searchCatalog(catalogEntries(multiGroupTools()), { group: "gmail" })).toEqual({
|
|
group: "gmail",
|
|
names: ["list", "read", "send"],
|
|
});
|
|
expect(searchCatalog(catalogEntries(multiGroupTools()), { group: "missing" })).toEqual({
|
|
group: "missing",
|
|
names: [],
|
|
error: "Unknown or empty group",
|
|
});
|
|
});
|
|
|
|
it("keeps case-distinct group labels separate when filtering", () => {
|
|
const mixed = [
|
|
...tools(2, "Gmail").map((tool, index) => ({
|
|
...tool,
|
|
name: `Upper_${index}`,
|
|
route: { ...tool.route!, toolName: `Upper_${index}` },
|
|
})),
|
|
...tools(2, "gmail").map((tool, index) => ({
|
|
...tool,
|
|
name: `lower_${index}`,
|
|
route: { ...tool.route!, toolName: `lower_${index}` },
|
|
})),
|
|
];
|
|
const entries = catalogEntries(mixed);
|
|
expect(searchCatalog(entries, { group: "Gmail" })).toEqual({
|
|
group: "Gmail",
|
|
names: ["Upper_0", "Upper_1"],
|
|
});
|
|
expect(searchCatalog(entries, { group: "gmail" })).toEqual({
|
|
group: "gmail",
|
|
names: ["lower_0", "lower_1"],
|
|
});
|
|
const listed = searchCatalog(entries, {});
|
|
expect("index" in listed && listed.index).toEqual(
|
|
[
|
|
{ group: "Gmail", names: ["Upper_0", "Upper_1"] },
|
|
{ group: "gmail", names: ["lower_0", "lower_1"] },
|
|
].sort((left, right) => left.group.localeCompare(right.group)),
|
|
);
|
|
});
|
|
|
|
it("returns a group count when expanding an over-budget source", () => {
|
|
const oversized = Array.from({ length: NAME_INDEX_MAX_NAMES + 5 }, (_, index) => ({
|
|
name: `tool_${index}`,
|
|
description: "x".repeat(40),
|
|
inputSchema: { type: "object", properties: { secretShape: { type: "string" } } },
|
|
route: {
|
|
connectorId: "test",
|
|
resourceId: "big",
|
|
toolName: `tool_${index}`,
|
|
catalogGroup: "huge",
|
|
},
|
|
}));
|
|
const entries = catalogEntries(oversized);
|
|
const result = searchCatalog(entries, { group: "huge" });
|
|
expect(result).toEqual({
|
|
group: "huge",
|
|
count: NAME_INDEX_MAX_NAMES + 5,
|
|
hint: "Pass query to search within this source.",
|
|
});
|
|
expect(JSON.stringify(result)).not.toContain("secretShape");
|
|
expect(JSON.stringify(result)).not.toContain("tool_0");
|
|
const searched = searchCatalog(entries, { group: "huge", query: "tool_0" });
|
|
expect("tools" in searched && searched.tools.map((item) => item.name)).toEqual(["tool_0"]);
|
|
});
|
|
|
|
it("returns groups with counts when the name index exceeds budget", () => {
|
|
const oversized = Array.from({ length: NAME_INDEX_MAX_NAMES + 5 }, (_, index) => ({
|
|
name: `tool_${index}`,
|
|
description: "x".repeat(40),
|
|
inputSchema: { type: "object", properties: { secretShape: { type: "string" } } },
|
|
route: {
|
|
connectorId: "test",
|
|
resourceId: `src-${index}`,
|
|
toolName: `tool_${index}`,
|
|
catalogGroup: `group_${Math.floor(index / 50)}`,
|
|
},
|
|
}));
|
|
const entries = catalogEntries(oversized);
|
|
const result = searchCatalog(entries, {});
|
|
expect(result).toMatchObject({
|
|
hint: "Pass group to list names in one source.",
|
|
});
|
|
expect("groups" in result && result.groups.every((item) => item.count > 0)).toBe(true);
|
|
expect("index" in result).toBe(false);
|
|
expect(JSON.stringify(result)).not.toContain("secretShape");
|
|
const [search] = lazyCatalogTools("connectors", "mcp", "MCP", entries);
|
|
expect(search?.description).toContain("Empty query lists sources");
|
|
expect(search?.description).not.toContain("tool_0");
|
|
});
|
|
|
|
it("filters query search by group and keeps schema-free hits", () => {
|
|
const catalog = tools(25, "alpha").concat(tools(5, "beta"));
|
|
const first = searchCatalog(catalogEntries(catalog), {
|
|
query: "shared",
|
|
group: "beta",
|
|
limit: 100,
|
|
});
|
|
const second = searchCatalog(catalogEntries(catalog), {
|
|
query: "shared",
|
|
group: "beta",
|
|
limit: 100,
|
|
});
|
|
|
|
expect(first).toEqual(second);
|
|
expect("tools" in first && first.tools).toHaveLength(5);
|
|
expect("tools" in first && first.tools.every((item) => item.name.startsWith("tool_"))).toBe(
|
|
true,
|
|
);
|
|
expect(JSON.stringify(first)).not.toContain("secretShape");
|
|
});
|
|
|
|
it("caps and stably orders schema-free search results", () => {
|
|
const catalog = tools(25);
|
|
const first = searchCatalog(catalogEntries(catalog), { query: "shared", limit: 100 });
|
|
const second = searchCatalog(catalogEntries(catalog), { query: "shared", limit: 100 });
|
|
|
|
expect("tools" in first && first.tools).toHaveLength(TOOL_SEARCH_LIMIT);
|
|
expect(first).toEqual(second);
|
|
expect("tools" in first && first.tools.map((item) => item.name)).toEqual(
|
|
[...(("tools" in first && first.tools.map((item) => item.name)) || [])].sort((left, right) =>
|
|
left.localeCompare(right),
|
|
),
|
|
);
|
|
expect(JSON.stringify(first)).not.toContain("secretShape");
|
|
});
|
|
|
|
it("rejects an untrusted selected schema above the byte ceiling", () => {
|
|
const entries = catalogEntries([
|
|
{
|
|
name: "oversized",
|
|
description: "Oversized schema",
|
|
inputSchema: { type: "object", description: "x".repeat(SELECTED_SCHEMA_MAX_BYTES) },
|
|
},
|
|
]);
|
|
|
|
expect(() => loadCatalogEntry(entries, { id: "oversized" })).toThrow("schema is too large");
|
|
});
|
|
|
|
it("caps load descriptions like search", () => {
|
|
const long = "d".repeat(600);
|
|
const entries = catalogEntries([
|
|
{
|
|
name: "verbose",
|
|
description: long,
|
|
inputSchema: { type: "object", properties: {} },
|
|
route: { connectorId: "test", resourceId: "src", toolName: "verbose" },
|
|
},
|
|
]);
|
|
const entry = loadCatalogEntry(entries, { id: "src:verbose" });
|
|
expect(entry.tool.description).toHaveLength(600);
|
|
const hits = searchCatalog(entries, { query: "verbose" });
|
|
expect("tools" in hits && hits.tools[0]?.description).toHaveLength(500);
|
|
});
|
|
|
|
it("loads by exposed tool name as well as catalog id", () => {
|
|
const entries = catalogEntries(multiGroupTools());
|
|
expect(loadCatalogEntry(entries, { id: "mcp__gmail__send" }).tool.name).toBe(
|
|
"mcp__gmail__send",
|
|
);
|
|
expect(loadCatalogEntry(entries, { id: "srv-gmail:send" }).tool.name).toBe("mcp__gmail__send");
|
|
expect(loadCatalogEntry(entries, { id: "send" }).tool.name).toBe("mcp__gmail__send");
|
|
expect(() => loadCatalogEntry(entries, { id: "missing" })).toThrow(/unknown/i);
|
|
});
|
|
|
|
it("does not execute args when JSON Schema is unsupported", async () => {
|
|
const entries = catalogEntries([
|
|
{
|
|
name: "exotic",
|
|
description: "Unsupported schema",
|
|
// unevaluatedProperties is rejected by z.fromJSONSchema
|
|
inputSchema: {
|
|
type: "object",
|
|
properties: { payload: { type: "string" } },
|
|
unevaluatedProperties: false,
|
|
},
|
|
route: { connectorId: "test", resourceId: "src", toolName: "exotic" },
|
|
},
|
|
]);
|
|
|
|
const executeResolved = vi.fn(async function* () {
|
|
yield { type: "result" as const, data: "executed" };
|
|
});
|
|
const execute = async () => {
|
|
for await (const _event of executeLazyCatalogControl(
|
|
{
|
|
tool: "wrapper",
|
|
args: { id: "src:exotic", arguments: { payload: "raw-bytes", extra: true } },
|
|
executionId: "exec",
|
|
route: { connectorId: "test", toolName: "__catalog_execute" },
|
|
},
|
|
entries,
|
|
executeResolved,
|
|
)) {
|
|
// Consume the catalog execution path.
|
|
}
|
|
};
|
|
|
|
await expect(execute()).rejects.toThrow();
|
|
expect(executeResolved).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("still rejects non-object args and unknown ids", () => {
|
|
const entries = catalogEntries(tools(1));
|
|
expect(() =>
|
|
resolveCatalogCall(
|
|
{
|
|
tool: "wrapper",
|
|
args: { id: "missing", arguments: {} },
|
|
executionId: "exec",
|
|
route: { connectorId: "test", toolName: "__catalog_execute" },
|
|
},
|
|
entries,
|
|
),
|
|
).toThrow("unknown or not authorized");
|
|
expect(() =>
|
|
resolveCatalogCall(
|
|
{
|
|
tool: "wrapper",
|
|
args: { id: "source-0:tool-0", arguments: ["not-an-object"] },
|
|
executionId: "exec",
|
|
route: { connectorId: "test", toolName: "__catalog_execute" },
|
|
},
|
|
entries,
|
|
),
|
|
).toThrow("must be an object");
|
|
});
|
|
|
|
it("rejects persisted bound args that no longer match the live schema", () => {
|
|
const schema = {
|
|
type: "object",
|
|
properties: { value: { type: "string" } },
|
|
required: ["value"],
|
|
additionalProperties: false,
|
|
};
|
|
expect(() => assertConnectorToolArgs(schema, { value: "ok" })).not.toThrow();
|
|
expect(() => assertConnectorToolArgs(schema, { value: 1 })).toThrow(/invalid/i);
|
|
expect(() => assertConnectorToolArgs(schema, {})).toThrow(/invalid/i);
|
|
});
|
|
|
|
it("rejects schemas that combine patternProperties with additionalProperties false", () => {
|
|
const schema = {
|
|
type: "object",
|
|
patternProperties: { "^x-": { type: "string" } },
|
|
additionalProperties: false,
|
|
};
|
|
expect(() => assertConnectorToolArgs(schema, { "x-a": "ok" })).toThrow(
|
|
/patternProperties with additionalProperties false/,
|
|
);
|
|
});
|
|
|
|
it("uniquifies installed tool names across installs", () => {
|
|
expect(uniquifyInstalledToolName("install-A", "delete_item")).toBe(
|
|
"installed__install-A__delete_item",
|
|
);
|
|
expect(uniquifyInstalledToolName("install-B", "delete_item")).toBe(
|
|
"installed__install-B__delete_item",
|
|
);
|
|
});
|
|
|
|
it("only prefixes installed names when they collide across installs", () => {
|
|
const tools = disambiguateInstalledToolNames([
|
|
{
|
|
name: "unique_op",
|
|
description: "Only on A",
|
|
inputSchema: { type: "object" },
|
|
route: { connectorId: "installed", resourceId: "install-A", toolName: "unique_op" },
|
|
},
|
|
{
|
|
name: "delete_item",
|
|
description: "On A",
|
|
inputSchema: { type: "object" },
|
|
route: { connectorId: "installed", resourceId: "install-A", toolName: "delete_item" },
|
|
},
|
|
{
|
|
name: "delete_item",
|
|
description: "On B",
|
|
inputSchema: { type: "object" },
|
|
route: { connectorId: "installed", resourceId: "install-B", toolName: "delete_item" },
|
|
},
|
|
]);
|
|
expect(tools.map((tool) => tool.name)).toEqual([
|
|
"unique_op",
|
|
"installed__install-A__delete_item",
|
|
"installed__install-B__delete_item",
|
|
]);
|
|
});
|
|
|
|
it("labels nameless installs with kind and a short id", () => {
|
|
expect(catalogGroupLabel("Gmail", "api", "abcdefghij")).toBe("Gmail");
|
|
expect(catalogGroupLabel(" ", "api", "abcdefghij")).toBe("api-abcdefgh");
|
|
expect(catalogGroupLabel(undefined, "mcp", "xyz")).toBe("mcp-xyz");
|
|
});
|
|
|
|
it("formats a compact text index for descriptions", () => {
|
|
expect(
|
|
formatNameIndexText([
|
|
{ group: "gmail", names: ["list", "read", "send"] },
|
|
{ group: "hubspot", names: ["get_deal", "list_contacts"] },
|
|
]),
|
|
).toBe("gmail:\n list, read, send\nhubspot:\n get_deal, list_contacts");
|
|
});
|
|
});
|