x
This commit is contained in:
1 parent
bd8f5882e0
commit
e509e3f0a2
8 files changed
+581
-12
No files matched your search
@@ -17,6 +17,7 @@ const websiteBlockTypes = [
|
||||
"BANNER",
|
||||
"HERO_ARTICLE",
|
||||
"RICH_TEXT",
|
||||
"ARTICLE_MEDIA",
|
||||
"SCENT_NOTES_BREAKDOWN",
|
||||
"QUOTE_CALLOUT",
|
||||
"PRODUCT_CAROUSEL",
|
||||
@@ -67,6 +68,20 @@ describe("Page Builder block contracts", () => {
|
||||
});
|
||||
});
|
||||
|
||||
test("ARTICLE_MEDIA requires imageAlt when imageUrl is provided", () => {
|
||||
const result = validatePayloadAgainstMetadata("ARTICLE_MEDIA", {
|
||||
body: { th: "ข้อความภาษาไทย" },
|
||||
imageUrl: "/uploads/article.jpg",
|
||||
});
|
||||
|
||||
expect(result.valid).toBe(false);
|
||||
expect(result.validationIssues).toContainEqual({
|
||||
path: "imageAlt",
|
||||
message: "imageAlt is required when imageUrl is provided",
|
||||
code: "custom",
|
||||
});
|
||||
});
|
||||
|
||||
test("create_page_with_section rejects invalid payloads without calling backend", async () => {
|
||||
let postCalls = 0;
|
||||
const api = {
|
||||
@@ -133,4 +148,78 @@ describe("Page Builder block contracts", () => {
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
test("add_page_section rejects invalid payloads without calling backend", async () => {
|
||||
let postCalls = 0;
|
||||
const api = {
|
||||
post: async () => {
|
||||
postCalls += 1;
|
||||
throw new Error("Backend must not be called");
|
||||
},
|
||||
} as any;
|
||||
|
||||
const result = await handleSectionTool(api, "add_page_section", {
|
||||
pageId: 15,
|
||||
type: "HERO_V2",
|
||||
payload: {},
|
||||
});
|
||||
|
||||
expect(result && "isError" in result ? result.isError : false).toBe(true);
|
||||
expect(postCalls).toBe(0);
|
||||
});
|
||||
|
||||
test("add_page_section uses the existing-page endpoint and returns compact output", async () => {
|
||||
const calls: Array<{ url: string; payload: unknown }> = [];
|
||||
const api = {
|
||||
post: async (url: string, payload: unknown) => {
|
||||
calls.push({ url, payload });
|
||||
return {
|
||||
data: {
|
||||
success: true,
|
||||
pageId: 15,
|
||||
pageSlug: "fine-blanking-ddc-3-in-1",
|
||||
sectionId: 36,
|
||||
section: { id: 36, type: "HERO_ARTICLE", order: 2 },
|
||||
},
|
||||
};
|
||||
},
|
||||
} as any;
|
||||
const payload = getPageBlockSchema("HERO_ARTICLE").recommendedPayload;
|
||||
|
||||
const result = await handleSectionTool(api, "add_page_section", {
|
||||
pageId: 15,
|
||||
pageSlug: "fine-blanking-ddc-3-in-1",
|
||||
type: "HERO_ARTICLE",
|
||||
payload,
|
||||
order: 2,
|
||||
});
|
||||
const response = JSON.parse(result?.content[0]?.text ?? "{}");
|
||||
|
||||
expect(calls).toEqual([{
|
||||
url: "/pages/15/sections",
|
||||
payload: {
|
||||
type: "HERO_ARTICLE",
|
||||
payload,
|
||||
isActive: true,
|
||||
order: 2,
|
||||
},
|
||||
}]);
|
||||
expect(response).toEqual({
|
||||
success: true,
|
||||
tool: "add_page_section",
|
||||
pageId: 15,
|
||||
pageSlug: "fine-blanking-ddc-3-in-1",
|
||||
preview: {
|
||||
lang: "th",
|
||||
previewPath: "/th/fine-blanking-ddc-3-in-1",
|
||||
previewUrl: "/th/fine-blanking-ddc-3-in-1",
|
||||
},
|
||||
sectionId: 36,
|
||||
type: "HERO_ARTICLE",
|
||||
order: 2,
|
||||
warnings: [
|
||||
"overlayColor is accepted by backend schema but is not used by apps/website HeroArticleSection today.",
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -18,6 +18,7 @@ const WEBSITE_PAGE_SECTION_TYPE_LIST = [
|
||||
"BANNER",
|
||||
"HERO_ARTICLE",
|
||||
"RICH_TEXT",
|
||||
"ARTICLE_MEDIA",
|
||||
"SCENT_NOTES_BREAKDOWN",
|
||||
"QUOTE_CALLOUT",
|
||||
"PRODUCT_CAROUSEL",
|
||||
@@ -299,6 +300,32 @@ const PAGE_BLOCK_METADATA: Record<string, BlockMetadata> = {
|
||||
},
|
||||
},
|
||||
},
|
||||
ARTICLE_MEDIA: {
|
||||
type: "ARTICLE_MEDIA",
|
||||
requiredFields: ["body"],
|
||||
optionalFields: ["heading", "imageUrl", "imageAlt", "imageCaption", "imagePosition", "width", "background"],
|
||||
renderedFields: ["heading", "body", "imageUrl", "imageAlt", "imageCaption", "imagePosition", "width", "background"],
|
||||
storedOnlyFields: [],
|
||||
discoveryToolsNeeded: [],
|
||||
warnings: [
|
||||
"body is rendered as plain text with preserved line breaks; do not send HTML.",
|
||||
"imageAlt is required whenever imageUrl is provided.",
|
||||
],
|
||||
minimalPayload: { body: localizedObjectExample },
|
||||
recommendedPayload: {
|
||||
heading: { th: "ความแม่นยำในทุกฟันสเตอร์", en: "Precision in every sprocket tooth" },
|
||||
body: {
|
||||
th: "Fine Blanking ช่วยควบคุมผิวตัดและรูปทรงของฟันสเตอร์ให้สม่ำเสมอ เพื่อการส่งกำลังที่ราบรื่นและแม่นยำ",
|
||||
en: "Fine blanking controls the cut surface and tooth profile for smooth, precise power transmission.",
|
||||
},
|
||||
imageUrl: imageUrlExample,
|
||||
imageAlt: { th: "สเตอร์ที่ผลิตด้วยเทคโนโลยี Fine Blanking", en: "A sprocket made with fine blanking technology" },
|
||||
imageCaption: { th: "ผิวตัดเรียบและรูปทรงสม่ำเสมอ", en: "A smooth cut surface and consistent profile" },
|
||||
imagePosition: "right",
|
||||
width: "standard",
|
||||
background: "default",
|
||||
},
|
||||
},
|
||||
SCENT_NOTES_BREAKDOWN: {
|
||||
type: "SCENT_NOTES_BREAKDOWN",
|
||||
requiredFields: [],
|
||||
@@ -524,6 +551,10 @@ const ADD_PAGE_SECTION_GUIDANCE: Record<string, { required: string[]; example: s
|
||||
required: ["content"],
|
||||
example: `{ "title": ${localizedStringExample}, "content": ${localizedStringExample} }`
|
||||
},
|
||||
ARTICLE_MEDIA: {
|
||||
required: ["body"],
|
||||
example: `{ "heading": ${localizedStringExample}, "body": ${localizedStringExample}, "imageUrl": "https://example.com/article.jpg", "imageAlt": ${localizedStringExample}, "imagePosition": "right" }`
|
||||
},
|
||||
SCENT_NOTES_BREAKDOWN: {
|
||||
required: [],
|
||||
example: `{ "title": ${localizedStringExample}, "topNotes": ${localizedStringListExample} }`
|
||||
@@ -783,6 +814,23 @@ const SECTION_TOOL_CONFIGS = [
|
||||
},
|
||||
required: ["sectionId", "content"]
|
||||
},
|
||||
{
|
||||
name: "update_article_media",
|
||||
sectionType: "ARTICLE_MEDIA",
|
||||
description: `Update an ARTICLE_MEDIA section. This tool performs a partial update/merge (PATCH). Omitted fields are preserved. body is localized plain text; do not send HTML. imageAlt is required when imageUrl is present. Example payload: { "sectionId": 35, "heading": ${localizedStringExample}, "body": ${localizedStringExample}, "imageUrl": "https://...", "imageAlt": ${localizedStringExample}, "imagePosition": "right" }`,
|
||||
properties: {
|
||||
sectionId: { type: "number", description: "The numeric ID of the section to update" },
|
||||
heading: { ...localizedStringSchema, description: `Optional localized heading, for example ${localizedStringExample}` },
|
||||
body: { ...localizedStringSchema, description: `Localized plain text body, for example ${localizedStringExample}` },
|
||||
imageUrl: { type: "string", description: "Optional image URL or website-relative media path" },
|
||||
imageAlt: { ...localizedStringSchema, description: `Localized image alternative text, required with imageUrl` },
|
||||
imageCaption: { ...localizedStringSchema, description: `Optional localized image caption` },
|
||||
imagePosition: { type: "string", enum: ["left", "right", "top"], description: "Responsive image placement" },
|
||||
width: { type: "string", enum: ["narrow", "standard", "wide"], description: "Content width preset" },
|
||||
background: { type: "string", enum: ["default", "muted"], description: "Theme-aware background preset" },
|
||||
},
|
||||
required: ["sectionId"]
|
||||
},
|
||||
{
|
||||
name: "update_scent_notes_breakdown",
|
||||
sectionType: "SCENT_NOTES_BREAKDOWN",
|
||||
@@ -1020,7 +1068,7 @@ export const SECTION_TOOL_DEFINITIONS = [
|
||||
},
|
||||
{
|
||||
name: "create_page_with_section",
|
||||
description: "Create or update a dynamic CMS page and add one website-renderable section in one operation. Use this when the user asks to create a page with a single block.",
|
||||
description: "Create a new dynamic CMS page and its first website-renderable section atomically. This tool is create-only: if the slug already exists, use get_dynamic_page_detail to find the page ID, then use add_page_section.",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
@@ -1052,7 +1100,7 @@ export const SECTION_TOOL_DEFINITIONS = [
|
||||
},
|
||||
{
|
||||
name: "add_page_section",
|
||||
description: `${buildAddPageSectionDescription()}\n\nRender target guard: generic CMS pages are rendered by apps/website DynamicPageRenderer. PRODUCT_MAIN, PRODUCT_DETAILS, PRODUCT_SPECS, PRODUCT_REVIEWS, and RELATED_PRODUCTS are product detail native blocks and are rejected by default here. Use product detail CMS tools for those blocks.`,
|
||||
description: `Add a new website-renderable block to an existing dynamic CMS page. Use this instead of create_page_with_section when the page or slug already exists. Call get_dynamic_page_detail first to obtain pageId, and get_page_block_schema before composing payload.\n\n${buildAddPageSectionDescription()}\n\nRender target guard: generic CMS pages are rendered by apps/website DynamicPageRenderer. PRODUCT_MAIN, PRODUCT_DETAILS, PRODUCT_SPECS, PRODUCT_REVIEWS, and RELATED_PRODUCTS are product detail native blocks and are rejected by default here. Use product detail CMS tools for those blocks.`,
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
@@ -1398,6 +1446,20 @@ export async function handleSectionTool(api: AxiosInstance, name: string, params
|
||||
throw new Error(`Validation Error: 'pageId' is required for tool '${name}'`);
|
||||
}
|
||||
assertWebsitePageSectionType(params.type, params.allowProductNative === true);
|
||||
const validation = validatePayloadAgainstMetadata(params.type, params.payload);
|
||||
if (!validation.valid) {
|
||||
return {
|
||||
content: [{
|
||||
type: "text",
|
||||
text: JSON.stringify({
|
||||
success: false,
|
||||
error: "Payload failed MCP preflight validation.",
|
||||
validation,
|
||||
}, null, 2)
|
||||
}],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
const payload: any = {
|
||||
type: params.type,
|
||||
payload: params.payload,
|
||||
@@ -1414,8 +1476,10 @@ export async function handleSectionTool(api: AxiosInstance, name: string, params
|
||||
pageId: params.pageId,
|
||||
pageSlug: params.pageSlug ?? undefined,
|
||||
preview: params.pageSlug ? buildCmsPreview(params.pageSlug, params.lang || defaultLang) : undefined,
|
||||
renderability: buildRenderability(params.type),
|
||||
section: res.data,
|
||||
sectionId: res.data?.sectionId ?? res.data?.section?.id ?? res.data?.id ?? null,
|
||||
type: params.type,
|
||||
order: res.data?.section?.order ?? res.data?.order ?? params.order ?? null,
|
||||
warnings: buildRenderability(params.type).warnings,
|
||||
}, null, 2)
|
||||
}]
|
||||
};
|
||||
|
||||
Reference in new issue
Block a user