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("preserves the compact contract for an allowed menu that does not exist", async () => { const { api } = createApi({ success: true, menuName: "footer-support", exists: false, version: null, lang: "th", items: [], }); const result = await handleMenuTool(api, "get_footer_menu", { menuName: "footer-support", }); expect(output(result)).toEqual({ success: true, operation: "get_footer_menu", menuName: "footer-support", exists: false, version: null, lang: "th", items: [], }); }); 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("preserves a dry-run bootstrap preview for an absent allowed menu", async () => { const { api } = createApi({ success: true, operation: "add_footer_menu_item", menuName: "footer-explore", dryRun: true, exists: false, willCreateMenu: true, version: null, item: { label: "Shop", url: "/shop", order: 1, }, }); const result = await handleMenuTool(api, "add_footer_menu_item", { menuName: "footer-explore", label: "Shop", url: "/shop", order: 1, dryRun: true, }); expect(output(result)).toEqual({ success: true, operation: "add_footer_menu_item", menuName: "footer-explore", dryRun: true, exists: false, willCreateMenu: true, version: null, item: { label: "Shop", url: "/shop", order: 1, }, }); }); 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(); }); });