import assert from "node:assert/strict"; import { readFile } from "node:fs/promises"; import { resolve } from "node:path"; const source = await readFile(resolve("src/utils/longTextCard.js"), "utf8"); const moduleUrl = `data:text/javascript;base64,${Buffer.from(source).toString("base64")}`; const longTextCard = await import(moduleUrl); assert.doesNotMatch( source, /normalizeLongText/, "longTextCard should not contain field-specific normalize helpers" ); assert.doesNotMatch( source, /spot_longitude|spot_latitude|spot_tag/, "longTextCard should not know spot_locate child field names" ); const { LONG_TEXT_FIELD_CONFIG, LONG_TEXT_KEYS, appendLongTextChunk, createLongTextData, getLongTextParsedValue, getLongTextSections, getLongTextValue, hasLongTextExtraSections, parseLongTextDisplayValue, sanitizeLongTextDisplayValue, hasLongTextDisplayValue, formatLongTextDisplayValue, } = longTextCard; assert.equal( source.includes("REMOVED_LONG_TEXT_FIELD_KEYS"), false, "longTextCard should not carry a removed field blacklist" ); assert.equal( source.includes("hasLongTextChunkPayload"), false, "ChatMainList should not ask longTextCard to inspect plain data.content payloads" ); assert.deepEqual( parseLongTextDisplayValue('[ "see bridge", "hear water" ]'), ["see bridge", "hear water"], "serialized arrays should be parsed for display" ); assert.deepEqual( parseLongTextDisplayValue('{"spot_name":"bridge","spot_longitude":107.712345}'), { spot_name: "bridge", spot_longitude: 107.712345 }, "serialized objects should be parsed for display" ); assert.deepEqual( sanitizeLongTextDisplayValue( { preparation_section: { preparation_section_title: "keep", preparation_section_items: '[ "check piers", "count arches" ]', }, nested: { title: "keep", }, }, [] ), { preparation_section: { preparation_section_title: "keep", preparation_section_items: ["check piers", "count arches"], }, nested: { title: "keep", }, }, "sanitizing should parse JSON-like strings and ignore configured keys" ); assert.equal(hasLongTextDisplayValue('["follow up"]'), true); assert.equal(hasLongTextDisplayValue({ a: "", b: [" ", null] }), false); assert.equal(formatLongTextDisplayValue(true), "\u662f"); assert.equal(formatLongTextDisplayValue({ title: "bridge" }), '{"title":"bridge"}'); const expectedNewKeys = { tag: "tag", title: "title", contentSummary: "content_summary", sceneImage: "scene_image", preparationSection: "preparation_section", sectionSuggestion: "section_suggestion", pitfallSection: "pitfall_section", commodityList: "commodity_list", contentImage: "content_image", viewSection: "view_section", suggestionSection: "suggestion_section", lightReminder: "light_reminder", spotLocate: "spot_locate", photoSpotSection: "photo_spot_section", phoneSection: "phone_section", photoList: "photo_list", aigcComponet: "aigc_componet", decisionSection: "decision_section", routeWarning: "route_warning", tourRoutes: "tour_routes", facilitiesAlongTheWay: "facilities_along_the_way", questionSuggest: "question_suggest", }; for (const [keyName, keyValue] of Object.entries(expectedNewKeys)) { assert.equal(LONG_TEXT_KEYS[keyName], keyValue, `${keyName} should be registered`); assert.equal( LONG_TEXT_FIELD_CONFIG.some((item) => item.key === keyValue), true, `${keyValue} should be in LONG_TEXT_FIELD_CONFIG` ); } const removedKeys = [ "content", "guideConclusion", "keyFacts", "preparationSectionTitle", "preparationSectionItems", "sectionSuggestionTitle", "sectionSuggestionContent", "pitfallSectionTitle", "pitfallSectionItems", "viewSectionTitle", "viewSectionItems", "suggestionSectionTitle", "suggestionSectionContent", "lightReminderTitle", "lightReminderItems", "components", "actionZone", ]; for (const keyName of removedKeys) { assert.equal( Object.prototype.hasOwnProperty.call(LONG_TEXT_KEYS, keyName), false, `${keyName} should be removed from LONG_TEXT_KEYS` ); } const oldFlatFieldValues = [ "preparation_section_title", "preparation_section_items", "view_section_title", "view_section_items", ]; for (const keyValue of oldFlatFieldValues) { assert.equal( LONG_TEXT_FIELD_CONFIG.some((item) => item.key === keyValue), false, `${keyValue} should be removed from LONG_TEXT_FIELD_CONFIG` ); } const longTextData = createLongTextData(); appendLongTextChunk(longTextData, { contentKey: "title", contentValue: "漂流攻略" }); appendLongTextChunk(longTextData, { contentKey: "preparation_section", contentValue: { preparation_section_title: "下水前", preparation_section_items: ["带手机防水袋", "穿涉水鞋"], }, }); appendLongTextChunk(longTextData, { contentKey: "future_section", contentValue: { future_section_title: "未来新增字段", future_section_items: ["不改 LONG_TEXT_KEYS 也展示"], }, }); appendLongTextChunk(longTextData, { contentKey: "content_image", contentValue: "https://oss.nianxx.cn/XiaoQiKong/GQ00001.jpg", }); appendLongTextChunk(longTextData, { contentKey: "spot_locate", contentValue: '{"spot_name":"卧龙潭","spot_longitude":"107.712345","spot_latitude":"25.251234","spot_tag":"景点"}', }); assert.equal(getLongTextValue(longTextData, "title"), "漂流攻略"); assert.deepEqual( getLongTextParsedValue(longTextData, "preparation_section"), { preparation_section_title: "下水前", preparation_section_items: ["带手机防水袋", "穿涉水鞋"], }, "object contentValue should be serialized and parsed" ); assert.deepEqual( getLongTextSections(longTextData).map((section) => section.contentKey), ["title", "preparation_section", "future_section", "content_image", "spot_locate"], "sections should place unknown fields by receive order between configured fields" ); assert.equal(hasLongTextExtraSections(longTextData), true); assert.deepEqual( getLongTextParsedValue(longTextData, "future_section"), { future_section_title: "未来新增字段", future_section_items: ["不改 LONG_TEXT_KEYS 也展示"], }, "future top-level fields should keep parsed values" ); const mergedPayload = { tag: "攻略", title: "合并结构标题", content_summary: "合并结构摘要", preparation_section: { preparation_section_title: "下水前", preparation_section_items: ["带手机防水袋", "穿涉水鞋"], }, spot_locate: { spot_name: "卧龙潭", spot_longitude: "107.712345", spot_latitude: "25.251234", spot_tag: "景点", }, photo_list: [ { photo_name: "桥边机位", photo_description: "站在桥头拍", photo_url: "https://example.com/photo.png", }, ], photo_spot_section: "{\n\"photo_spot_section_title\":\"四个不会错的机位\",\n\"photo_spot_items\":\"- 涵碧潭半清半浊\\n- 断桥飞瀑错位玩\",\n\"best_time_suggestion\":\"下午 2 点后人少\"\n}", phone_section: "{\n\"phone_section_title\":\"手机党看这里\",\n\"phone_section_items\":\"- 手机倒过来贴近水面\\n- 瀑布用长曝光\"\n}", aigc_componet: { background: "https://example.com/aigc.png", title: "AIGC合影", description: "生成合影", jumpUrl: "https://example.com/aigc", }, question_suggest: ["还要准备什么?"], future_section: { future_section_title: "后续新增模块", future_section_items: ["仍然展示"], }, }; const mergedLongTextData = createLongTextData(); appendLongTextChunk(mergedLongTextData, { contentKey: "photo_card_payload", contentValue: mergedPayload, }); assert.deepEqual( getLongTextSections(mergedLongTextData).map((section) => section.contentKey), [ "photo_card_payload", ], "unknown contentKey fields should be kept as a single generic section" ); assert.deepEqual( getLongTextParsedValue(mergedLongTextData, "photo_card_payload"), mergedPayload, "unknown contentKey object values should keep parsed values" ); console.log("longTextCard display helpers passed");