Files
kandev/apps/web/lib/settings/external-mcp-tools.test.ts
T
nova28 c82668c789 feat(mcp): expose agent clarification questions and permission requests externally (#2875)
Co-authored-by: Vojtěch Biberle <vojtech.biberle@gmail.com>
Co-authored-by: nova28 <17953305+nova28@users.noreply.github.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: Carlos Florencio <carlosmflorencio@gmail.com>
2026-08-22 15:39:40 +00:00

80 lines
3.7 KiB
TypeScript

import { describe, it, expect } from "vitest";
import { EXTERNAL_MCP_TOOL_GROUPS, countExternalMcpTools } from "./external-mcp-tools";
import enSettings from "@/src/locales/en/settings.json";
describe("external MCP tool catalog", () => {
// Pins the catalog against `ModeExternal`'s registered tool count: 13
// workflow (incl. list_repositories + import_workflow + export_workflow) +
// 4 agent + 4 mcp + 5 executor + 7 task (incl. list_task_sessions +
// get_task_conversation) + 1 create_task + 2 task-dependency + 2
// question-answering (list_pending_questions + answer_question) + 2 agent
// permission (list_pending_agent_permissions + resolve_agent_permission) =
// 40 — see `TestServerModeExternal_ToolCount`.
it("lists 40 tools, matching every tool ModeExternal registers", () => {
expect(countExternalMcpTools()).toBe(40);
});
it("every tool name is unique and ends with the kandev suffix", () => {
const names = EXTERNAL_MCP_TOOL_GROUPS.flatMap((g) => g.tools.map((t) => t.name));
expect(new Set(names).size).toBe(names.length);
for (const name of names) {
expect(name.endsWith("_kandev")).toBe(true);
}
});
it("includes create_task_kandev (the only task-spawning tool exposed externally)", () => {
const names = EXTERNAL_MCP_TOOL_GROUPS.flatMap((g) => g.tools.map((t) => t.name));
expect(names).toContain("create_task_kandev");
});
it("includes external agent permission discovery and exact resolution", () => {
const names = EXTERNAL_MCP_TOOL_GROUPS.flatMap((g) => g.tools.map((t) => t.name));
expect(names).toContain("list_pending_agent_permissions_kandev");
expect(names).toContain("resolve_agent_permission_kandev");
});
it("does not expose session-scoped tools (plan, ask_user_question)", () => {
const names = EXTERNAL_MCP_TOOL_GROUPS.flatMap((g) => g.tools.map((t) => t.name));
expect(names).not.toContain("ask_user_question_kandev");
expect(names).not.toContain("create_task_plan_kandev");
});
it("every group has at least one tool and a non-empty title key", () => {
for (const group of EXTERNAL_MCP_TOOL_GROUPS) {
expect(group.titleKey.length).toBeGreaterThan(0);
expect(group.tools.length).toBeGreaterThan(0);
}
});
// The catalog is a plain `.ts` module with no JSX, so `i18next/no-literal-string`
// never inspects it and a key that never made it into the catalog would render
// as the raw `settings:externalMcp…` string with nothing failing.
it("every title and description key resolves in the en catalog", () => {
const catalog = enSettings as Record<string, string>;
const keys = EXTERNAL_MCP_TOOL_GROUPS.flatMap((group) => [
group.titleKey,
group.descriptionKey,
...group.tools.map((tool) => tool.descriptionKey),
]);
// Derived, not a literal: this asserts every entry carries keys, and must
// not quietly re-pin the tool count the test above owns.
expect(keys).toHaveLength(EXTERNAL_MCP_TOOL_GROUPS.length * 2 + countExternalMcpTools());
for (const key of keys) {
const [ns, name] = key.split(":");
expect(ns).toBe("settings");
expect(catalog[name], `missing catalog entry for ${key}`).toBeTruthy();
}
});
// A message that interpolates a wire token renders a dead `{{placeholder}}`
// if the tool forgets to carry the value.
it("supplies a value for every interpolated description", () => {
const catalog = enSettings as Record<string, string>;
for (const tool of EXTERNAL_MCP_TOOL_GROUPS.flatMap((g) => g.tools)) {
const message = catalog[tool.descriptionKey.split(":")[1]];
const placeholders = [...message.matchAll(/\{\{(\w+)\}\}/g)].map((m) => m[1]);
expect(Object.keys(tool.descriptionValues ?? {}).sort()).toEqual(placeholders.sort());
}
});
});