x
This commit is contained in:
1 parent
94cbb52348
commit
167d07f0c2
7 files changed
+1284
-21
No files matched your search
@@ -0,0 +1,288 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import {
|
||||
MENU_TOOL_DEFINITIONS,
|
||||
getLegacyFooterMenuDeprecation,
|
||||
handleMenuTool,
|
||||
} from "./menu-toolkit";
|
||||
|
||||
type Call = {
|
||||
method: string;
|
||||
url: string;
|
||||
body?: unknown;
|
||||
config?: unknown;
|
||||
};
|
||||
|
||||
function createApi(response: unknown = { success: true, version: "v2" }) {
|
||||
const calls: Call[] = [];
|
||||
const api = {
|
||||
get: async (url: string) => {
|
||||
calls.push({ method: "get", url });
|
||||
return { data: response };
|
||||
},
|
||||
post: async (url: string, body: unknown) => {
|
||||
calls.push({ method: "post", url, body });
|
||||
return { data: response };
|
||||
},
|
||||
patch: async (url: string, body: unknown) => {
|
||||
calls.push({ method: "patch", url, body });
|
||||
return { data: response };
|
||||
},
|
||||
put: async (url: string, body: unknown) => {
|
||||
calls.push({ method: "put", url, body });
|
||||
return { data: response };
|
||||
},
|
||||
delete: async (url: string, config: unknown) => {
|
||||
calls.push({ method: "delete", url, config });
|
||||
return { data: response };
|
||||
},
|
||||
};
|
||||
return { api: api as any, calls };
|
||||
}
|
||||
|
||||
function output(result: any) {
|
||||
return JSON.parse(result.content[0].text);
|
||||
}
|
||||
|
||||
describe("Footer MCP tool definitions", () => {
|
||||
test("registers six Footer-specific tools with constrained menu names", () => {
|
||||
const names = [
|
||||
"get_footer_menu",
|
||||
"add_footer_menu_item",
|
||||
"update_footer_menu_item",
|
||||
"delete_footer_menu_item",
|
||||
"reorder_footer_menu_items",
|
||||
"update_footer_menu_item_translation",
|
||||
];
|
||||
|
||||
for (const name of names) {
|
||||
const definition = MENU_TOOL_DEFINITIONS.find((tool) => tool.name === name) as any;
|
||||
expect(definition).toBeDefined();
|
||||
expect(definition.inputSchema.properties.menuName.enum).toEqual([
|
||||
"footer-explore",
|
||||
"footer-support",
|
||||
]);
|
||||
}
|
||||
});
|
||||
|
||||
test("all Footer writes expose dryRun and expectedVersion", () => {
|
||||
const writes = MENU_TOOL_DEFINITIONS.filter((tool) =>
|
||||
[
|
||||
"add_footer_menu_item",
|
||||
"update_footer_menu_item",
|
||||
"delete_footer_menu_item",
|
||||
"reorder_footer_menu_items",
|
||||
"update_footer_menu_item_translation",
|
||||
].includes(tool.name),
|
||||
) as any[];
|
||||
|
||||
for (const tool of writes) {
|
||||
expect(tool.inputSchema.properties.dryRun).toBeDefined();
|
||||
expect(tool.inputSchema.properties.expectedVersion).toBeDefined();
|
||||
}
|
||||
});
|
||||
|
||||
test("update_footer_menu_item exposes only supported mutable item fields", () => {
|
||||
const definition = MENU_TOOL_DEFINITIONS.find(
|
||||
(tool) => tool.name === "update_footer_menu_item",
|
||||
) as any;
|
||||
|
||||
expect(Object.keys(definition.inputSchema.properties).sort()).toEqual([
|
||||
"dryRun",
|
||||
"expectedVersion",
|
||||
"itemId",
|
||||
"label",
|
||||
"menuName",
|
||||
"order",
|
||||
"url",
|
||||
]);
|
||||
expect(definition.inputSchema.properties.isActive).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Footer MCP proxy handlers", () => {
|
||||
test("reads a compact localized Footer menu", async () => {
|
||||
const { api, calls } = createApi({
|
||||
success: true,
|
||||
menuName: "footer-explore",
|
||||
lang: "zh",
|
||||
version: "v1",
|
||||
items: [{ id: 1, label: "商店", url: "/shop", order: 1 }],
|
||||
internalMetadata: "must not leak",
|
||||
});
|
||||
const result = await handleMenuTool(api, "get_footer_menu", {
|
||||
menuName: "footer-explore",
|
||||
lang: "zh",
|
||||
fallback: false,
|
||||
});
|
||||
|
||||
expect(calls).toEqual([{
|
||||
method: "get",
|
||||
url: "/menus/footer/footer-explore?lang=zh&fallback=false",
|
||||
}]);
|
||||
expect(output(result)).toEqual({
|
||||
success: true,
|
||||
operation: "get_footer_menu",
|
||||
menuName: "footer-explore",
|
||||
version: "v1",
|
||||
lang: "zh",
|
||||
items: [{ id: 1, label: "商店", url: "/shop", order: 1 }],
|
||||
});
|
||||
});
|
||||
|
||||
test("adds an item with write controls", async () => {
|
||||
const { api, calls } = createApi();
|
||||
await handleMenuTool(api, "add_footer_menu_item", {
|
||||
menuName: "footer-explore",
|
||||
label: "Shop",
|
||||
url: "/shop",
|
||||
order: 2,
|
||||
translations: { zh: "商店" },
|
||||
dryRun: true,
|
||||
expectedVersion: "v1",
|
||||
});
|
||||
|
||||
expect(calls[0]).toEqual({
|
||||
method: "post",
|
||||
url: "/menus/footer/footer-explore/items",
|
||||
body: {
|
||||
label: "Shop",
|
||||
url: "/shop",
|
||||
order: 2,
|
||||
translations: { zh: "商店" },
|
||||
dryRun: true,
|
||||
expectedVersion: "v1",
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test("partially updates an item without forwarding unrelated fields", async () => {
|
||||
const { api, calls } = createApi();
|
||||
await handleMenuTool(api, "update_footer_menu_item", {
|
||||
menuName: "footer-support",
|
||||
itemId: 9,
|
||||
label: "Contact",
|
||||
dryRun: false,
|
||||
expectedVersion: "v4",
|
||||
translations: { zh: "ignored" },
|
||||
isActive: true,
|
||||
});
|
||||
|
||||
expect(calls[0]).toEqual({
|
||||
method: "patch",
|
||||
url: "/menus/footer/footer-support/items/9",
|
||||
body: { label: "Contact", dryRun: false, expectedVersion: "v4" },
|
||||
});
|
||||
});
|
||||
|
||||
test("deletes an item with Axios DELETE body config", async () => {
|
||||
const { api, calls } = createApi();
|
||||
await handleMenuTool(api, "delete_footer_menu_item", {
|
||||
menuName: "footer-support",
|
||||
itemId: 7,
|
||||
dryRun: true,
|
||||
expectedVersion: "v3",
|
||||
});
|
||||
|
||||
expect(calls[0]).toEqual({
|
||||
method: "delete",
|
||||
url: "/menus/footer/footer-support/items/7",
|
||||
config: { data: { dryRun: true, expectedVersion: "v3" } },
|
||||
});
|
||||
});
|
||||
|
||||
test("reorders using the complete item ID list", async () => {
|
||||
const { api, calls } = createApi();
|
||||
await handleMenuTool(api, "reorder_footer_menu_items", {
|
||||
menuName: "footer-explore",
|
||||
itemIds: [3, 1, 2],
|
||||
expectedVersion: "v8",
|
||||
});
|
||||
|
||||
expect(calls[0]).toEqual({
|
||||
method: "put",
|
||||
url: "/menus/footer/footer-explore/items/reorder",
|
||||
body: { itemIds: [3, 1, 2], dryRun: false, expectedVersion: "v8" },
|
||||
});
|
||||
});
|
||||
|
||||
test("updates one item translation through the language endpoint", async () => {
|
||||
const { api, calls } = createApi();
|
||||
await handleMenuTool(api, "update_footer_menu_item_translation", {
|
||||
menuName: "footer-explore",
|
||||
itemId: 12,
|
||||
lang: "zh",
|
||||
label: "商店",
|
||||
dryRun: true,
|
||||
expectedVersion: "v5",
|
||||
});
|
||||
|
||||
expect(calls[0]).toEqual({
|
||||
method: "patch",
|
||||
url: "/menus/footer/footer-explore/items/12/translations/zh",
|
||||
body: { label: "商店", dryRun: true, expectedVersion: "v5" },
|
||||
});
|
||||
});
|
||||
|
||||
test("preserves compact mutation, preview, and conflict response keys", async () => {
|
||||
const { api } = createApi({
|
||||
success: false,
|
||||
operation: "update_footer_menu_item",
|
||||
menuName: "footer-support",
|
||||
itemId: 18,
|
||||
itemIds: [18, 20],
|
||||
label: "Help",
|
||||
before: { label: "Support" },
|
||||
after: { label: "Help" },
|
||||
code: "MENU_VERSION_CONFLICT",
|
||||
error: "The menu changed after it was read.",
|
||||
partialWrite: false,
|
||||
version: "v9",
|
||||
databaseRecord: { mustNotLeak: true },
|
||||
});
|
||||
const result = await handleMenuTool(api, "update_footer_menu_item", {
|
||||
menuName: "footer-support",
|
||||
itemId: 18,
|
||||
label: "Help",
|
||||
expectedVersion: "v8",
|
||||
});
|
||||
|
||||
expect(output(result)).toEqual({
|
||||
success: false,
|
||||
operation: "update_footer_menu_item",
|
||||
menuName: "footer-support",
|
||||
version: "v9",
|
||||
itemId: 18,
|
||||
itemIds: [18, 20],
|
||||
label: "Help",
|
||||
before: { label: "Support" },
|
||||
after: { label: "Help" },
|
||||
code: "MENU_VERSION_CONFLICT",
|
||||
error: "The menu changed after it was read.",
|
||||
partialWrite: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("Legacy Footer mutation guard", () => {
|
||||
test("rejects every legacy menu array before any partial write", () => {
|
||||
expect(getLegacyFooterMenuDeprecation({
|
||||
brandDescription: "Must not be written",
|
||||
exploreLinks: [],
|
||||
collectionLinks: [],
|
||||
supportLinks: [],
|
||||
})).toEqual({
|
||||
success: false,
|
||||
code: "FOOTER_MENU_REPLACEMENT_DEPRECATED",
|
||||
partialWrite: false,
|
||||
rejectedFields: ["exploreLinks", "collectionLinks", "supportLinks"],
|
||||
message: expect.any(String),
|
||||
});
|
||||
});
|
||||
|
||||
test("retains legacy brand and social settings support", () => {
|
||||
expect(getLegacyFooterMenuDeprecation({
|
||||
brandDescription: "Brand",
|
||||
socialLinks: { instagramUrl: "https://instagram.com/example" },
|
||||
})).toBeNull();
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user