This commit is contained in:
Your Name committed 2026-07-24 19:39:38 -04:00
1 parent e509e3f0a2
commit 0d4df55d93
7 files changed
+526 -3

No files matched your search

+23
View File
@@ -17,7 +17,12 @@ const websiteBlockTypes = [
"BANNER",
"HERO_ARTICLE",
"RICH_TEXT",
"ARTICLE_HEADER",
"ARTICLE_MEDIA",
"ARTICLE_GALLERY",
"ARTICLE_FACTS",
"ARTICLE_VIDEO",
"ARTICLE_REFERENCES",
"SCENT_NOTES_BREAKDOWN",
"QUOTE_CALLOUT",
"PRODUCT_CAROUSEL",
@@ -82,6 +87,24 @@ describe("Page Builder block contracts", () => {
});
});
test("ARTICLE_GALLERY rejects fewer than two images", () => {
const result = validatePayloadAgainstMetadata("ARTICLE_GALLERY", {
images: [{ imageUrl: "/uploads/one.jpg", alt: { th: "ภาพเดียว" } }],
});
expect(result.valid).toBe(false);
expect(result.validationIssues.some((issue) => issue.path === "images")).toBe(true);
});
test("ARTICLE_VIDEO rejects non-HTTPS URLs", () => {
const result = validatePayloadAgainstMetadata("ARTICLE_VIDEO", {
videoUrl: "http://example.com/video.mp4",
});
expect(result.valid).toBe(false);
expect(result.validationIssues.some((issue) => issue.path === "videoUrl")).toBe(true);
});
test("create_page_with_section rejects invalid payloads without calling backend", async () => {
let postCalls = 0;
const api = {
+113 -1
View File
@@ -18,7 +18,12 @@ const WEBSITE_PAGE_SECTION_TYPE_LIST = [
"BANNER",
"HERO_ARTICLE",
"RICH_TEXT",
"ARTICLE_HEADER",
"ARTICLE_MEDIA",
"ARTICLE_GALLERY",
"ARTICLE_FACTS",
"ARTICLE_VIDEO",
"ARTICLE_REFERENCES",
"SCENT_NOTES_BREAKDOWN",
"QUOTE_CALLOUT",
"PRODUCT_CAROUSEL",
@@ -326,6 +331,93 @@ const PAGE_BLOCK_METADATA: Record<string, BlockMetadata> = {
background: "default",
},
},
ARTICLE_HEADER: {
type: "ARTICLE_HEADER",
requiredFields: ["title"],
optionalFields: ["subtitle", "category", "author", "publishDate", "imageUrl", "imageAlt"],
renderedFields: ["title", "subtitle", "category", "author", "publishDate", "imageUrl", "imageAlt"],
storedOnlyFields: [],
discoveryToolsNeeded: [],
warnings: ["imageAlt is required whenever imageUrl is provided."],
minimalPayload: { title: localizedObjectExample },
recommendedPayload: {
title: { th: "เทคโนโลยีที่อยู่เบื้องหลังทุกการขับเคลื่อน", en: "Technology behind every ride" },
subtitle: { th: "ทำความรู้จักกระบวนการผลิตที่ให้ความสำคัญกับความแม่นยำ", en: "A closer look at precision manufacturing." },
category: { th: "เทคโนโลยี", en: "Technology" },
author: { th: "ทีมงาน DDC", en: "DDC Team" },
publishDate: "2026-07-24",
},
},
ARTICLE_GALLERY: {
type: "ARTICLE_GALLERY",
requiredFields: ["images", "images[].imageUrl", "images[].alt"],
optionalFields: ["title", "layout", "images[].caption"],
renderedFields: ["title", "layout", "images"],
storedOnlyFields: [],
discoveryToolsNeeded: ["Use real CMS media URLs; do not invent image URLs."],
warnings: ["Provide 2 to 8 images. Every image requires localized alt text."],
minimalPayload: {
images: [
{ imageUrl: imageUrlExample, alt: localizedObjectExample },
{ imageUrl: imageUrlExample, alt: localizedObjectExample },
],
},
recommendedPayload: {
title: { th: "ภาพจากกระบวนการผลิต", en: "Inside the production process" },
layout: "grid",
images: [
{ imageUrl: imageUrlExample, alt: { th: "ขั้นตอนการผลิตชิ้นงาน", en: "Component production" } },
{ imageUrl: imageUrlExample, alt: { th: "การตรวจสอบคุณภาพ", en: "Quality inspection" } },
],
},
},
ARTICLE_FACTS: {
type: "ARTICLE_FACTS",
requiredFields: ["items", "items[].title"],
optionalFields: ["title", "items[].description"],
renderedFields: ["title", "items"],
storedOnlyFields: [],
discoveryToolsNeeded: [],
warnings: ["Provide 1 to 8 concise facts."],
minimalPayload: { items: [{ title: localizedObjectExample }] },
recommendedPayload: {
title: { th: "ข้อมูลสำคัญ", en: "Key facts" },
items: [
{ title: { th: "ผิวตัดสม่ำเสมอ", en: "Consistent cut surface" }, description: { th: "ช่วยลดขั้นตอนการแต่งผิว", en: "Reduces secondary finishing." } },
{ title: { th: "รูปทรงแม่นยำ", en: "Precise geometry" }, description: { th: "ช่วยให้การขบกับโซ่ต่อเนื่อง", en: "Supports consistent chain engagement." } },
],
},
},
ARTICLE_VIDEO: {
type: "ARTICLE_VIDEO",
requiredFields: ["videoUrl"],
optionalFields: ["title", "caption", "posterUrl", "aspectRatio"],
renderedFields: ["title", "videoUrl", "caption", "posterUrl", "aspectRatio"],
storedOnlyFields: [],
discoveryToolsNeeded: [],
warnings: ["Use a full HTTPS YouTube, Vimeo, or direct video URL. Do not send embed HTML."],
minimalPayload: { videoUrl: "https://www.youtube.com/watch?v=VIDEO_ID" },
recommendedPayload: {
title: { th: "ชมกระบวนการผลิต", en: "Watch the production process" },
videoUrl: "https://www.youtube.com/watch?v=VIDEO_ID",
caption: { th: "เบื้องหลังการผลิตที่แม่นยำ", en: "Behind precision manufacturing" },
aspectRatio: "16:9",
},
},
ARTICLE_REFERENCES: {
type: "ARTICLE_REFERENCES",
requiredFields: ["items", "items[].title"],
optionalFields: ["title", "items[].url", "items[].note"],
renderedFields: ["title", "items"],
storedOnlyFields: [],
discoveryToolsNeeded: [],
warnings: ["Use verified source URLs only. A reference may omit url for offline sources."],
minimalPayload: { items: [{ title: localizedObjectExample }] },
recommendedPayload: {
title: { th: "แหล่งข้อมูล", en: "References" },
items: [{ title: { th: "เอกสารข้อมูลทางเทคนิค", en: "Technical documentation" }, note: { th: "ข้อมูลจากฝ่ายวิศวกรรม", en: "Engineering source material" } }],
},
},
SCENT_NOTES_BREAKDOWN: {
type: "SCENT_NOTES_BREAKDOWN",
requiredFields: [],
@@ -555,6 +647,26 @@ const ADD_PAGE_SECTION_GUIDANCE: Record<string, { required: string[]; example: s
required: ["body"],
example: `{ "heading": ${localizedStringExample}, "body": ${localizedStringExample}, "imageUrl": "https://example.com/article.jpg", "imageAlt": ${localizedStringExample}, "imagePosition": "right" }`
},
ARTICLE_HEADER: {
required: ["title"],
example: `{ "title": ${localizedStringExample}, "subtitle": ${localizedStringExample}, "author": ${localizedStringExample} }`
},
ARTICLE_GALLERY: {
required: ["images", "images[].imageUrl", "images[].alt"],
example: `{ "images": [{ "imageUrl": "https://example.com/one.jpg", "alt": ${localizedStringExample} }, { "imageUrl": "https://example.com/two.jpg", "alt": ${localizedStringExample} }] }`
},
ARTICLE_FACTS: {
required: ["items", "items[].title"],
example: `{ "title": ${localizedStringExample}, "items": [{ "title": ${localizedStringExample}, "description": ${localizedStringExample} }] }`
},
ARTICLE_VIDEO: {
required: ["videoUrl"],
example: `{ "videoUrl": "https://www.youtube.com/watch?v=VIDEO_ID", "title": ${localizedStringExample}, "aspectRatio": "16:9" }`
},
ARTICLE_REFERENCES: {
required: ["items", "items[].title"],
example: `{ "items": [{ "title": ${localizedStringExample}, "url": "https://example.com/source" }] }`
},
SCENT_NOTES_BREAKDOWN: {
required: [],
example: `{ "title": ${localizedStringExample}, "topNotes": ${localizedStringListExample} }`
@@ -1100,7 +1212,7 @@ export const SECTION_TOOL_DEFINITIONS = [
},
{
name: "add_page_section",
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.`,
description: "Add one website-renderable block to an existing dynamic CMS page. Call get_dynamic_page_detail to obtain pageId, then get_page_block_schema for the selected type before composing payload. Use product detail CMS tools for product-native blocks.",
inputSchema: {
type: "object",
properties: {