mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-05 01:21:35 +00:00
* fix(code-mode): dispose suspended runs and propagate cancellation * fix(code-mode): preserve request-visible provider tools * test(code-mode): require real packaged gateway execution * fix(gateway): load code mode cleanup only during shutdown
682 lines
20 KiB
TypeScript
682 lines
20 KiB
TypeScript
/** Tests connected node-hosted plugin tool materialization. */
|
|
|
|
import { expectDefined } from "@openclaw/normalization-core";
|
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
import type { NodePluginToolDescriptor } from "../../packages/gateway-protocol/src/index.js";
|
|
import {
|
|
listConnectedNodePluginTools,
|
|
removeConnectedNodePluginTools,
|
|
replaceConnectedNodePluginTools,
|
|
} from "../gateway/node-plugin-tool-snapshot.js";
|
|
import { getPluginToolMeta, setPluginToolMeta } from "../plugins/tools.js";
|
|
import { applyCodeModeCatalog, createCodeModeTools } from "./code-mode.js";
|
|
import { testing } from "./code-mode.test-support.js";
|
|
import { createNodePluginTools } from "./node-plugin-tools.js";
|
|
import { compactToolSearchCatalogEntry, createToolSearchCatalogRef } from "./tool-search.js";
|
|
import { jsonResult, type AnyAgentTool } from "./tools/common.js";
|
|
import { callGatewayTool } from "./tools/gateway.js";
|
|
|
|
vi.mock("./tools/gateway.js", () => ({
|
|
callGatewayTool: vi.fn(),
|
|
}));
|
|
|
|
function replaceNodePluginTools(
|
|
params: Omit<Parameters<typeof replaceConnectedNodePluginTools>[0], "tools"> & {
|
|
tools: NodePluginToolDescriptor[];
|
|
registered?: boolean;
|
|
},
|
|
): void {
|
|
const { registered = false, tools, ...node } = params;
|
|
replaceConnectedNodePluginTools({
|
|
...node,
|
|
tools: tools.map((descriptor) => ({ descriptor, registered })),
|
|
});
|
|
}
|
|
|
|
function createCodeModeHarness(tools: AnyAgentTool[]) {
|
|
const catalogRef = createToolSearchCatalogRef();
|
|
const config = { tools: { codeMode: true } } as never;
|
|
const ctx = {
|
|
config,
|
|
runtimeConfig: config,
|
|
sessionId: "session-node-mcp-code-mode",
|
|
sessionKey: "agent:main:node-mcp-code-mode",
|
|
runId: "run-node-mcp-code-mode",
|
|
catalogRef,
|
|
};
|
|
const codeModeTools = createCodeModeTools(ctx);
|
|
const compacted = applyCodeModeCatalog({
|
|
tools: [...codeModeTools, ...tools],
|
|
config,
|
|
sessionId: ctx.sessionId,
|
|
sessionKey: ctx.sessionKey,
|
|
runId: ctx.runId,
|
|
catalogRef,
|
|
});
|
|
return { catalogRef, codeModeTools, compacted };
|
|
}
|
|
|
|
async function runCodeMode(codeModeTools: AnyAgentTool[], code: string) {
|
|
let details = (
|
|
await expectDefined(codeModeTools[0], "codeModeTools[0] test invariant").execute(
|
|
"code-node-mcp",
|
|
{ code },
|
|
)
|
|
).details as Record<string, unknown>;
|
|
for (let index = 0; index < 8 && details.status === "waiting"; index += 1) {
|
|
details = (
|
|
await expectDefined(codeModeTools[1], "codeModeTools[1] test invariant").execute(
|
|
`wait-node-mcp-${index}`,
|
|
{ runId: details.runId },
|
|
)
|
|
).details as Record<string, unknown>;
|
|
}
|
|
return details;
|
|
}
|
|
|
|
function gatewayMcpTool(serverName: string): AnyAgentTool {
|
|
const tool: AnyAgentTool = {
|
|
name: `${serverName}__status`,
|
|
label: "status",
|
|
description: `Read ${serverName} status`,
|
|
parameters: { type: "object", properties: {} },
|
|
execute: vi.fn(async () => jsonResult({ source: "gateway" })),
|
|
};
|
|
setPluginToolMeta(tool, {
|
|
pluginId: "bundle-mcp",
|
|
optional: false,
|
|
mcp: {
|
|
serverName,
|
|
safeServerName: serverName,
|
|
toolName: "status",
|
|
operation: "tool",
|
|
},
|
|
});
|
|
return tool;
|
|
}
|
|
|
|
afterEach(() => {
|
|
for (const nodeId of new Set(listConnectedNodePluginTools().map((tool) => tool.nodeId))) {
|
|
removeConnectedNodePluginTools(nodeId);
|
|
}
|
|
vi.mocked(callGatewayTool).mockReset();
|
|
testing.activeRuns.clear();
|
|
testing.resumingRunIds.clear();
|
|
});
|
|
|
|
describe("createNodePluginTools", () => {
|
|
it("materializes connected node plugin tools and invokes their node command", async () => {
|
|
replaceNodePluginTools({
|
|
nodeId: "node-1",
|
|
displayName: "Studio Node",
|
|
tools: [
|
|
{
|
|
pluginId: "remote-demo",
|
|
name: "remote_echo",
|
|
description: "Echo through a remote node",
|
|
parameters: {
|
|
type: "object",
|
|
properties: { text: { type: "string" } },
|
|
},
|
|
command: "remote.echo",
|
|
mcp: {
|
|
server: "remote-demo",
|
|
tool: "echo",
|
|
},
|
|
},
|
|
],
|
|
});
|
|
vi.mocked(callGatewayTool).mockResolvedValueOnce({
|
|
payload: {
|
|
content: [{ type: "text", text: "pong" }],
|
|
details: { ok: true },
|
|
},
|
|
});
|
|
|
|
const tools = createNodePluginTools({
|
|
existingToolNames: new Set(["read"]),
|
|
agentSessionKey: "agent:main:canvas",
|
|
});
|
|
const result = await expectDefined(tools[0], "tools[0] test invariant").execute("call-1", {
|
|
text: "ping",
|
|
});
|
|
|
|
expect(tools.map((tool) => tool.name)).toEqual(["remote_echo"]);
|
|
expect(expectDefined(tools[0], "tools[0] test invariant").description).toContain("Studio Node");
|
|
expect(getPluginToolMeta(expectDefined(tools[0], "tools[0] test invariant"))).toMatchObject({
|
|
pluginId: "remote-demo",
|
|
mcp: {
|
|
serverName: "remote-demo",
|
|
toolName: "echo",
|
|
operation: "tool",
|
|
},
|
|
});
|
|
expect(callGatewayTool).toHaveBeenCalledWith(
|
|
"node.invoke",
|
|
{},
|
|
{
|
|
nodeId: "node-1",
|
|
command: "remote.echo",
|
|
params: { text: "ping" },
|
|
idempotencyKey: "call-1",
|
|
sessionKey: "agent:main:canvas",
|
|
},
|
|
{ scopes: ["operator.write"] },
|
|
);
|
|
expect(result.content).toEqual([{ type: "text", text: "pong" }]);
|
|
});
|
|
|
|
it("forwards the caller abort signal to node gateway invocations", async () => {
|
|
replaceNodePluginTools({
|
|
nodeId: "node-1",
|
|
tools: [
|
|
{
|
|
pluginId: "remote-demo",
|
|
name: "remote_echo",
|
|
description: "Echo through a remote node",
|
|
command: "remote.echo",
|
|
},
|
|
],
|
|
});
|
|
vi.mocked(callGatewayTool).mockResolvedValueOnce({ payload: { ok: true } });
|
|
const controller = new AbortController();
|
|
const tool = expectDefined(
|
|
createNodePluginTools({})[0],
|
|
"createNodePluginTools({})[0] test invariant",
|
|
);
|
|
|
|
await tool.execute("call-cancellable", { text: "ping" }, controller.signal);
|
|
|
|
expect(callGatewayTool).toHaveBeenCalledWith(
|
|
"node.invoke",
|
|
{},
|
|
{
|
|
nodeId: "node-1",
|
|
command: "remote.echo",
|
|
params: { text: "ping" },
|
|
idempotencyKey: "call-cancellable",
|
|
},
|
|
{ scopes: ["operator.write"], signal: controller.signal },
|
|
);
|
|
});
|
|
|
|
it("propagates caller cancellation through node gateway invocations", async () => {
|
|
replaceNodePluginTools({
|
|
nodeId: "node-1",
|
|
tools: [
|
|
{
|
|
pluginId: "remote-demo",
|
|
name: "remote_echo",
|
|
description: "Echo through a remote node",
|
|
command: "remote.echo",
|
|
},
|
|
],
|
|
});
|
|
vi.mocked(callGatewayTool).mockImplementationOnce(async (_method, _opts, _params, extra) => {
|
|
extra?.signal?.throwIfAborted();
|
|
return { payload: { ok: true } };
|
|
});
|
|
const controller = new AbortController();
|
|
const abortError = new DOMException("node call cancelled", "AbortError");
|
|
controller.abort(abortError);
|
|
const tool = expectDefined(
|
|
createNodePluginTools({})[0],
|
|
"createNodePluginTools({})[0] test invariant",
|
|
);
|
|
|
|
await expect(tool.execute("call-aborted", { text: "ping" }, controller.signal)).rejects.toBe(
|
|
abortError,
|
|
);
|
|
expect(callGatewayTool).toHaveBeenCalledWith(
|
|
"node.invoke",
|
|
{},
|
|
{
|
|
nodeId: "node-1",
|
|
command: "remote.echo",
|
|
params: { text: "ping" },
|
|
idempotencyKey: "call-aborted",
|
|
},
|
|
{ scopes: ["operator.write"], signal: controller.signal },
|
|
);
|
|
});
|
|
|
|
it("wraps node-host MCP arguments and maps MCP content", async () => {
|
|
replaceNodePluginTools({
|
|
nodeId: "node-1",
|
|
tools: [
|
|
{
|
|
pluginId: "node-mcp",
|
|
name: "docs_search",
|
|
description: "Search node-local docs",
|
|
command: "mcp.tools.call.v1",
|
|
mcp: { server: "docs", tool: "search" },
|
|
},
|
|
],
|
|
});
|
|
vi.mocked(callGatewayTool).mockResolvedValueOnce({
|
|
payload: {
|
|
content: [
|
|
{ type: "image", data: "aW1hZ2UtMQ==", mimeType: "image/png" },
|
|
{ type: "text", text: "first" },
|
|
{ type: "text", text: "second" },
|
|
{ type: "image", data: "aW1hZ2UtMg==", mimeType: "image/png" },
|
|
],
|
|
structuredContent: { hits: 2 },
|
|
},
|
|
});
|
|
|
|
const tool = expectDefined(
|
|
createNodePluginTools({})[0],
|
|
"createNodePluginTools({})[0] test invariant",
|
|
);
|
|
const result = await tool.execute("call-mcp", { query: "needle" });
|
|
|
|
expect(callGatewayTool).toHaveBeenCalledWith(
|
|
"node.invoke",
|
|
{ timeoutMs: 125_000 },
|
|
{
|
|
nodeId: "node-1",
|
|
command: "mcp.tools.call.v1",
|
|
params: { server: "docs", tool: "search", arguments: { query: "needle" } },
|
|
timeoutMs: 120_000,
|
|
idempotencyKey: "call-mcp",
|
|
},
|
|
{ scopes: ["operator.write"] },
|
|
);
|
|
expect(tool.executionMode).toBe("sequential");
|
|
expect(getPluginToolMeta(tool)?.mcp?.node).toEqual({ id: "node-1" });
|
|
expect(result.content).toEqual([
|
|
{ type: "image", data: "aW1hZ2UtMQ==", mimeType: "image/png" },
|
|
{ type: "text", text: "first" },
|
|
{ type: "text", text: "second" },
|
|
{ type: "image", data: "aW1hZ2UtMg==", mimeType: "image/png" },
|
|
{ type: "text", text: '{\n "hits": 2\n}' },
|
|
]);
|
|
});
|
|
|
|
it("projects node MCP schemas and calls through the exact namespace catalog entry", async () => {
|
|
replaceNodePluginTools({
|
|
nodeId: "node-1",
|
|
displayName: "Studio Node",
|
|
tools: [
|
|
{
|
|
pluginId: "node-mcp",
|
|
name: "docs_search",
|
|
description: "Search node-local docs",
|
|
parameters: {
|
|
type: "object",
|
|
properties: {
|
|
query: { type: "string", description: "Search phrase" },
|
|
},
|
|
required: ["query"],
|
|
},
|
|
command: "mcp.tools.call.v1",
|
|
mcp: { server: "docs", tool: "search" },
|
|
},
|
|
],
|
|
});
|
|
vi.mocked(callGatewayTool).mockResolvedValueOnce({
|
|
payload: {
|
|
content: [{ type: "text", text: "found" }],
|
|
structuredContent: { hits: 1 },
|
|
},
|
|
});
|
|
|
|
const nodeTools = createNodePluginTools({ agentSessionKey: "agent:main:node-mcp-code-mode" });
|
|
const { catalogRef, codeModeTools, compacted } = createCodeModeHarness(nodeTools);
|
|
const nodeEntry = catalogRef.current?.entries.find(
|
|
(entry) => entry.name === "docs_search" && entry.source === "mcp",
|
|
);
|
|
|
|
expect(nodeEntry?.id).toBe("mcp:docs:docs_search");
|
|
expect(nodeEntry && compactToolSearchCatalogEntry(nodeEntry).mcp).toEqual({
|
|
serverName: "docs",
|
|
safeServerName: "docs",
|
|
toolName: "search",
|
|
operation: "tool",
|
|
});
|
|
expect(compacted.tools[0]?.description).toContain("docs (node: Studio Node)");
|
|
const details = await runCodeMode(
|
|
codeModeTools,
|
|
`
|
|
const api = await API.read("mcp/docs.d.ts");
|
|
const called = await MCP.docs.search({ query: "needle" });
|
|
const direct = await tools.search("docs_search");
|
|
return {
|
|
api: api.content,
|
|
called: called.details,
|
|
allHasNodeMcp: ALL_TOOLS.some((entry) => entry.id === "mcp:docs:docs_search"),
|
|
direct,
|
|
};
|
|
`,
|
|
);
|
|
|
|
expect(details.status).toBe("completed");
|
|
expect(details.value).toEqual({
|
|
api: expect.stringContaining("query: string;"),
|
|
called: {
|
|
content: [{ type: "text", text: "found" }],
|
|
structuredContent: { hits: 1 },
|
|
},
|
|
allHasNodeMcp: false,
|
|
direct: [],
|
|
});
|
|
expect((details.value as { api: string }).api).toContain("@param query Search phrase");
|
|
expect(callGatewayTool).toHaveBeenCalledWith(
|
|
"node.invoke",
|
|
{ timeoutMs: 125_000 },
|
|
{
|
|
nodeId: "node-1",
|
|
command: "mcp.tools.call.v1",
|
|
params: { server: "docs", tool: "search", arguments: { query: "needle" } },
|
|
timeoutMs: 120_000,
|
|
idempotencyKey: expect.stringContaining("docs_search"),
|
|
sessionKey: "agent:main:node-mcp-code-mode",
|
|
},
|
|
{ scopes: ["operator.write"], signal: expect.any(AbortSignal) },
|
|
);
|
|
});
|
|
|
|
it("disambiguates gateway-node and node-node MCP server collisions", async () => {
|
|
for (const [nodeId, displayName, serverName, toolName] of [
|
|
["node-a", "Node A", "tickets", "search_a"],
|
|
["node-b", "Node B", "docs", "search_b"],
|
|
["node-c", "Node C", "docs", "search_c"],
|
|
] as const) {
|
|
replaceNodePluginTools({
|
|
nodeId,
|
|
displayName,
|
|
tools: [
|
|
{
|
|
pluginId: "node-mcp",
|
|
name: `docs_${toolName}`,
|
|
description: `Search docs from ${displayName}`,
|
|
parameters: { type: "object", properties: {} },
|
|
command: "mcp.tools.call.v1",
|
|
mcp: { server: serverName, tool: toolName },
|
|
},
|
|
],
|
|
});
|
|
}
|
|
vi.mocked(callGatewayTool).mockResolvedValueOnce({
|
|
payload: { content: [{ type: "text", text: "node-b" }] },
|
|
});
|
|
|
|
const { codeModeTools, compacted } = createCodeModeHarness([
|
|
gatewayMcpTool("tickets"),
|
|
...createNodePluginTools({}),
|
|
]);
|
|
expect(compacted.tools[0]?.description).toContain(
|
|
"visible servers: nodeATickets (node: Node A), nodeBDocs (node: Node B), nodeCDocs (node: Node C), tickets",
|
|
);
|
|
|
|
const details = await runCodeMode(
|
|
codeModeTools,
|
|
`
|
|
const files = await API.list("mcp");
|
|
const called = await MCP.nodeCDocs.searchC({});
|
|
return { files: files.files.map((file) => file.path), called: called.details };
|
|
`,
|
|
);
|
|
|
|
expect(details.status).toBe("completed");
|
|
expect(details.value).toEqual({
|
|
files: [
|
|
"mcp/index.d.ts",
|
|
"mcp/nodeATickets.d.ts",
|
|
"mcp/nodeBDocs.d.ts",
|
|
"mcp/nodeCDocs.d.ts",
|
|
"mcp/tickets.d.ts",
|
|
],
|
|
called: { content: [{ type: "text", text: "node-b" }] },
|
|
});
|
|
expect(callGatewayTool).toHaveBeenCalledWith(
|
|
"node.invoke",
|
|
{ timeoutMs: 125_000 },
|
|
expect.objectContaining({
|
|
nodeId: "node-c",
|
|
params: { server: "docs", tool: "search_c", arguments: {} },
|
|
}),
|
|
{ scopes: ["operator.write"], signal: expect.any(AbortSignal) },
|
|
);
|
|
});
|
|
|
|
it("disambiguates node tools that collide with existing tool names", () => {
|
|
replaceNodePluginTools({
|
|
nodeId: "node-1",
|
|
tools: [
|
|
{
|
|
pluginId: "remote-demo",
|
|
name: "remote_echo",
|
|
description: "Echo through a remote node",
|
|
command: "remote.echo",
|
|
},
|
|
],
|
|
});
|
|
|
|
expect(
|
|
createNodePluginTools({ existingToolNames: new Set(["remote_echo"]) }).map(
|
|
(tool) => tool.name,
|
|
),
|
|
).toEqual(["node_1_remote_echo"]);
|
|
});
|
|
|
|
it("disambiguates matching tool names from different nodes", async () => {
|
|
replaceNodePluginTools({
|
|
nodeId: "node-a",
|
|
displayName: "Node A",
|
|
tools: [
|
|
{
|
|
pluginId: "remote-demo",
|
|
name: "remote_echo",
|
|
description: "Echo through a remote node",
|
|
command: "remote.echo",
|
|
},
|
|
],
|
|
});
|
|
replaceNodePluginTools({
|
|
nodeId: "node-b",
|
|
displayName: "Node B",
|
|
tools: [
|
|
{
|
|
pluginId: "remote-demo",
|
|
name: "remote_echo",
|
|
description: "Echo through a remote node",
|
|
command: "remote.echo",
|
|
},
|
|
],
|
|
});
|
|
vi.mocked(callGatewayTool).mockResolvedValueOnce({
|
|
payload: { ok: true, node: "b" },
|
|
});
|
|
|
|
const tools = createNodePluginTools({});
|
|
const result = await expectDefined(tools[1], "tools[1] test invariant").execute("call-2", {
|
|
text: "ping",
|
|
});
|
|
|
|
expect(tools.map((tool) => tool.name)).toEqual(["node_a_remote_echo", "node_b_remote_echo"]);
|
|
expect(callGatewayTool).toHaveBeenCalledWith(
|
|
"node.invoke",
|
|
{},
|
|
{
|
|
nodeId: "node-b",
|
|
command: "remote.echo",
|
|
params: { text: "ping" },
|
|
idempotencyKey: "call-2",
|
|
},
|
|
{ scopes: ["operator.write"] },
|
|
);
|
|
expect(result.content[0]).toMatchObject({
|
|
type: "text",
|
|
text: expect.stringContaining('"node": "b"'),
|
|
});
|
|
});
|
|
|
|
it("honors policy for disambiguated node tool names", () => {
|
|
for (const nodeId of ["node-a", "node-b"]) {
|
|
replaceNodePluginTools({
|
|
nodeId,
|
|
tools: [
|
|
{
|
|
pluginId: "remote-demo",
|
|
name: "remote_echo",
|
|
description: "Echo through a remote node",
|
|
command: "remote.echo",
|
|
},
|
|
],
|
|
});
|
|
}
|
|
|
|
expect(
|
|
createNodePluginTools({
|
|
toolAllowlist: ["node_b_remote_echo"],
|
|
}).map((tool) => tool.name),
|
|
).toEqual(["node_b_remote_echo"]);
|
|
expect(
|
|
createNodePluginTools({
|
|
toolDenylist: ["node_b_remote_echo"],
|
|
}).map((tool) => tool.name),
|
|
).toEqual(["node_a_remote_echo"]);
|
|
});
|
|
|
|
it("keeps numeric node fragments provider-safe", () => {
|
|
replaceNodePluginTools({
|
|
nodeId: "123",
|
|
tools: [
|
|
{
|
|
pluginId: "remote-demo",
|
|
name: "remote_echo",
|
|
description: "Echo through a remote node",
|
|
command: "remote.echo",
|
|
},
|
|
],
|
|
});
|
|
|
|
expect(
|
|
createNodePluginTools({ existingToolNames: new Set(["remote_echo"]) }).map(
|
|
(tool) => tool.name,
|
|
),
|
|
).toEqual(["node_123_remote_echo"]);
|
|
});
|
|
|
|
it("keeps numeric disambiguation when node fragments collide", () => {
|
|
for (const nodeId of ["node-a", "node_a"]) {
|
|
replaceNodePluginTools({
|
|
nodeId,
|
|
tools: [
|
|
{
|
|
pluginId: "remote-demo",
|
|
name: "remote_echo",
|
|
description: "Echo through a remote node",
|
|
command: "remote.echo",
|
|
},
|
|
],
|
|
});
|
|
}
|
|
|
|
expect(createNodePluginTools({}).map((tool) => tool.name)).toEqual([
|
|
"node_a_remote_echo",
|
|
"node_a_remote_echo_2",
|
|
]);
|
|
});
|
|
|
|
it("keeps disambiguated node tool names provider-safe", () => {
|
|
const longName = `a${"b".repeat(63)}`;
|
|
for (const nodeId of ["node-a", "node-b"]) {
|
|
replaceNodePluginTools({
|
|
nodeId,
|
|
tools: [
|
|
{
|
|
pluginId: "remote-demo",
|
|
name: longName,
|
|
description: "Echo through a remote node",
|
|
command: "remote.echo",
|
|
},
|
|
],
|
|
});
|
|
}
|
|
|
|
const names = createNodePluginTools({}).map((tool) => tool.name);
|
|
|
|
expect(names).toHaveLength(2);
|
|
expect(names.every((name) => /^[A-Za-z][A-Za-z0-9_-]{0,63}$/.test(name))).toBe(true);
|
|
expect(names[0]).not.toBe(names[1]);
|
|
});
|
|
|
|
it("honors plugin tool allow and deny policy", () => {
|
|
replaceNodePluginTools({
|
|
nodeId: "node-1",
|
|
tools: [
|
|
{
|
|
pluginId: "remote-demo",
|
|
name: "remote_echo",
|
|
description: "Echo through a remote node",
|
|
command: "remote.echo",
|
|
},
|
|
{
|
|
pluginId: "remote-demo",
|
|
name: "remote_status",
|
|
description: "Read remote status",
|
|
command: "remote.status",
|
|
},
|
|
],
|
|
registered: true,
|
|
});
|
|
|
|
expect(
|
|
createNodePluginTools({
|
|
toolAllowlist: ["remote-demo"],
|
|
toolDenylist: ["remote_status"],
|
|
}).map((tool) => tool.name),
|
|
).toEqual(["remote_echo"]);
|
|
expect(createNodePluginTools({ toolAllowlist: ["other-plugin"] })).toEqual([]);
|
|
});
|
|
|
|
it("trusts plugin-id allowlist entries only for registered tools and node-mcp", () => {
|
|
const githubDescriptor: NodePluginToolDescriptor = {
|
|
pluginId: "github",
|
|
name: "remote_repo_search",
|
|
description: "Search repositories through a remote node",
|
|
command: "remote.search",
|
|
};
|
|
|
|
replaceNodePluginTools({
|
|
nodeId: "node-1",
|
|
tools: [githubDescriptor],
|
|
});
|
|
expect(createNodePluginTools({ toolAllowlist: ["github"] })).toEqual([]);
|
|
|
|
replaceNodePluginTools({
|
|
nodeId: "node-1",
|
|
tools: [githubDescriptor],
|
|
registered: true,
|
|
});
|
|
expect(createNodePluginTools({ toolAllowlist: ["github"] }).map((tool) => tool.name)).toEqual([
|
|
"remote_repo_search",
|
|
]);
|
|
|
|
replaceNodePluginTools({
|
|
nodeId: "node-1",
|
|
tools: [{ ...githubDescriptor, pluginId: "node-mcp" }],
|
|
});
|
|
expect(createNodePluginTools({ toolAllowlist: ["node-mcp"] }).map((tool) => tool.name)).toEqual(
|
|
["remote_repo_search"],
|
|
);
|
|
|
|
replaceNodePluginTools({
|
|
nodeId: "node-1",
|
|
tools: [githubDescriptor],
|
|
});
|
|
expect(
|
|
createNodePluginTools({ toolAllowlist: ["remote_repo_search"] }).map((tool) => tool.name),
|
|
).toEqual(["remote_repo_search"]);
|
|
expect(
|
|
createNodePluginTools({
|
|
toolAllowlist: ["remote_repo_search"],
|
|
toolDenylist: ["github"],
|
|
}),
|
|
).toEqual([]);
|
|
});
|
|
});
|