feat: 发布三笙AI情报局独立版公司研发基线
同步家纺情报 v2 数据、59 个严格权威源、趋势图库、移动端交互、Google/Yandex 翻译门控及静态 SEO/GEO 构建。 补充面向 C# 研发的 README,明确当前无 LLM、多模态与生图模型,记录翻译请求参数、缓存与失败策略、数据处理边界、前端职责和无 GitHub Pages 部署流程。 新增按日期倒序维护的 CHANGELOG,记录影响、验证与代码/数据/dist 回滚方法。 验证:11 个 Python 契约测试、13 个 Node 前端测试通过;静态构建生成 1096 个 Sitemap URL。
This commit is contained in:
@@ -0,0 +1,197 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { readFile } from "node:fs/promises";
|
||||
import test from "node:test";
|
||||
|
||||
const root = new URL("../", import.meta.url);
|
||||
const text = async (path) => readFile(new URL(path, root), "utf8");
|
||||
|
||||
test("独立首页只展示家纺情报并加载独立入口", async () => {
|
||||
const html = await text("public/index.html");
|
||||
const dongguanCss = await text("public/assets/fonts/dongguan-bold/result.css");
|
||||
assert.match(html, /三笙AI情报局/);
|
||||
assert.match(html, /assets\/js\/app\.js/);
|
||||
assert.match(html, /assets\/js\/app\.js\?v=20260814p/);
|
||||
assert.match(html, /assets\/css\/base\.css\?v=20260814h/);
|
||||
assert.match(html, /assets\/css\/intelligence\.css\?v=20260814s/);
|
||||
assert.match(html, /<details[^>]+class="intelligence-metrics"/);
|
||||
assert.match(html, /<summary[^>]+class="intelligence-utility-row"/);
|
||||
assert.match(html, /class="tool-icon"/);
|
||||
assert.match(html, /assets\/fonts\/dongguan-bold\/result\.css/);
|
||||
assert.doesNotMatch(html, /cn-font\.claude-code-best\.win/);
|
||||
assert.match(dongguanCss, /font-family:"STDongGuanTi Bld"/);
|
||||
assert.match(dongguanCss, /\.woff2/);
|
||||
assert.doesNotMatch(html, />↗<|>◐</);
|
||||
assert.doesNotMatch(html, /AI 资讯|WaytoAGI|tabAiNews|aiNewsWrap/);
|
||||
});
|
||||
|
||||
test("数据快照不复用浏览器旧缓存", async () => {
|
||||
const app = await text("public/assets/js/app.js");
|
||||
assert.match(app, /\?t=\$\{Date\.now\(\)\}/);
|
||||
assert.match(app, /cache:\s*"no-store"/);
|
||||
assert.doesNotMatch(app, /cache:\s*"no-cache"/);
|
||||
});
|
||||
|
||||
test("七个业务入口与趋势四子类均保留", async () => {
|
||||
const renderer = await text("public/assets/js/render-intelligence.js");
|
||||
const modules = renderer + await text("public/assets/js/render-intelligence-core.js") + await text("public/assets/js/render-intelligence-scenes.js");
|
||||
for (const label of ["今日必看", "备货日历", "展会通", "政策预警", "趋势雷达", "平台渠道", "跨境活动"]) {
|
||||
assert.match(modules, new RegExp(label));
|
||||
}
|
||||
for (const label of ["灵感图库", "趋势博客", "时装趋势", "图案灵感检索"]) {
|
||||
assert.match(modules, new RegExp(label));
|
||||
}
|
||||
assert.doesNotMatch(modules, /设计师检索|设计师图片检索台/);
|
||||
assert.match(modules, /appendPrefixedTranslationLink/);
|
||||
assert.match(modules, /官方/);
|
||||
assert.match(modules, /requires-cross-border-network/);
|
||||
});
|
||||
|
||||
test("所有外部业务链接使用新窗口和安全rel", async () => {
|
||||
const files = [
|
||||
await text("public/assets/js/render-intelligence.js"),
|
||||
await text("public/assets/js/render-intelligence-core.js"),
|
||||
await text("public/assets/js/render-intelligence-scenes.js"),
|
||||
await text("public/assets/js/render-intelligence-activities.js"),
|
||||
].join("\n");
|
||||
assert.match(files, /target\s*=\s*["']_blank["']/);
|
||||
assert.match(files, /noopener noreferrer/);
|
||||
});
|
||||
|
||||
test("趋势分类读取真实 items 数组而不是分类对象", async () => {
|
||||
const renderer = await text("public/assets/js/render-intelligence.js");
|
||||
const modules = renderer + await text("public/assets/js/render-intelligence-core.js") + await text("public/assets/js/render-intelligence-scenes.js");
|
||||
assert.match(modules, /category\.items/);
|
||||
assert.match(modules, /item\.image\s*\|\|\s*item\.thumbnail/);
|
||||
assert.match(modules, /function renderCalendarMonthGrid/);
|
||||
assert.match(modules, /calendar-month-grid/);
|
||||
assert.match(modules, /calendar-weekday/);
|
||||
assert.match(modules, /create\("details", "calendar-local-names"\)/);
|
||||
assert.match(modules, /calendar-local-names-summary/);
|
||||
assert.match(modules, /item\.title_english/);
|
||||
assert.match(modules, /个国家\/地区/);
|
||||
const calendarCss = await text("public/assets/css/intelligence.css");
|
||||
assert.match(calendarCss, /\.calendar-local-names:not\(\[open\]\) \.calendar-local-name-list\s*\{[^}]*display:\s*none/s);
|
||||
assert.match(calendarCss, /\.calendar-local-summary-name\s*\{[^}]*white-space:\s*nowrap/s);
|
||||
assert.match(modules, /registration_translation_url/);
|
||||
assert.match(modules, /registration_translation_fallback_url/);
|
||||
});
|
||||
|
||||
test("灵感图库点击缩略图时使用原图浮层", async () => {
|
||||
const core = await text("public/assets/js/render-intelligence-core.js");
|
||||
const scenes = await text("public/assets/js/render-intelligence-scenes.js");
|
||||
const css = await text("public/assets/css/intelligence.css");
|
||||
assert.match(scenes, /item\.image_original\s*\|\|\s*item\.thumbnail/);
|
||||
assert.match(scenes, /trend-gallery-zoom/);
|
||||
assert.match(scenes, /openLightbox\([^,]+,\s*item\.title/);
|
||||
assert.match(scenes, /previewSrc:\s*item\.thumbnail/);
|
||||
assert.match(core, /role["']?,?\s*["']dialog/);
|
||||
assert.match(core, /aria-modal/);
|
||||
assert.match(core, /assets\/logo\.svg/);
|
||||
assert.match(core, /lightbox-loader/);
|
||||
assert.match(core, /lightbox-retry/);
|
||||
assert.match(core, /lightbox-download/);
|
||||
assert.match(core, /\.decode\(\)/);
|
||||
assert.match(core, /download\s*=/);
|
||||
assert.match(core, /fetch\(src/);
|
||||
assert.match(core, /URL\.createObjectURL/);
|
||||
assert.match(core, /window\.open\(src/);
|
||||
assert.match(css, /\.lightbox-overlay/);
|
||||
assert.match(css, /\.lightbox-preview/);
|
||||
assert.match(css, /\.lightbox-loader-logo-wrap::before\s*\{\s*animation:\s*lightbox-logo-spin\s+1\.8s\s+linear\s+infinite\s*!important;/);
|
||||
assert.match(css, /@media\s*\(prefers-reduced-motion:\s*reduce\)/);
|
||||
});
|
||||
|
||||
test("网页翻译严格按 IP 门控", async () => {
|
||||
const region = await text("public/assets/js/visitor-region.js");
|
||||
assert.match(region, /AbortController/);
|
||||
assert.match(region, /\["\/cdn-cgi\/trace",\s*"https:\/\/www\.cloudflare\.com\/cdn-cgi\/trace"\]/);
|
||||
assert.match(region, /nonChineseLanguage\s*\|\|\s*outsideChinaTimeZone/);
|
||||
const { translationProvider } = await import(new URL("../public/assets/js/visitor-region.js", import.meta.url));
|
||||
assert.equal(translationProvider("CN", true), "yandex");
|
||||
assert.equal(translationProvider("US", true), "google");
|
||||
assert.equal(translationProvider("US", false), "google");
|
||||
assert.equal(translationProvider("UNKNOWN", true), "google");
|
||||
assert.equal(translationProvider("UNKNOWN", false), "yandex");
|
||||
});
|
||||
|
||||
test("官方来源位于译文入口之前且可自动退到可用翻译服务", async () => {
|
||||
const core = await text("public/assets/js/render-intelligence-core.js");
|
||||
const scenes = await text("public/assets/js/render-intelligence-scenes.js");
|
||||
const activities = await text("public/assets/js/render-intelligence-activities.js");
|
||||
const seoBuilder = await text("scripts/build_seo.py");
|
||||
const seoRegion = await text("public/assets/js/seo-visitor-region.js");
|
||||
const policy = scenes.slice(scenes.indexOf("function makePolicyCard"), scenes.indexOf("function renderPolicies"));
|
||||
assert.ok(policy.indexOf("sourceLinkLabel(item)") < policy.indexOf("appendTranslationLinks(actions, item)"));
|
||||
assert.match(core, /function preferredTranslation/);
|
||||
assert.match(activities, /googleUrl \|\| yandexUrl/);
|
||||
assert.match(activities, /yandexUrl \|\| googleUrl/);
|
||||
assert.doesNotMatch(`${core}\n${activities}`, /百度中文|translationProvider === "baidu"/);
|
||||
assert.match(seoBuilder, /data-translation-group/);
|
||||
assert.match(seoRegion, /links\[0\]/);
|
||||
});
|
||||
|
||||
test("跨境活动不把发现栏目页冒充具体来源或译文", async () => {
|
||||
const activities = await text("public/assets/js/render-intelligence-activities.js");
|
||||
assert.doesNotMatch(activities, /source_translation_url|source_translation_fallback_url/);
|
||||
assert.match(activities, /sourceUrl !== registrationUrl/);
|
||||
assert.match(activities, /activityDayOffset\(item\.start_date\) >= 0/);
|
||||
});
|
||||
|
||||
test("跨境活动按访问当天重新计算相对日期并排除已结束活动", async () => {
|
||||
const { activityDayOffset } = await import(new URL("../public/assets/js/render-intelligence-activities.js", import.meta.url));
|
||||
const now = new Date(2026, 7, 14, 12, 0, 0);
|
||||
assert.equal(activityDayOffset("2026-08-13", now), -1);
|
||||
assert.equal(activityDayOffset("2026-08-14", now), 0);
|
||||
assert.equal(activityDayOffset("2026-08-15", now), 1);
|
||||
});
|
||||
|
||||
test("最新家纺交互与移动端契约已同步", async () => {
|
||||
const renderer = await text("public/assets/js/render-intelligence.js");
|
||||
const modules = renderer + await text("public/assets/js/render-intelligence-core.js") + await text("public/assets/js/render-intelligence-scenes.js");
|
||||
const baseCss = await text("public/assets/css/base.css");
|
||||
const css = await text("public/assets/css/intelligence.css");
|
||||
assert.doesNotMatch(renderer, /displayLimit/);
|
||||
assert.match(modules, /data\.calendar_insights\?\.length \|\| data\.calendar\?\.length/);
|
||||
assert.match(modules, /platform-filter-toolbar/);
|
||||
const activities = await text("public/assets/js/render-intelligence-activities.js");
|
||||
assert.match(modules + activities, /activity-filter-toolbar/);
|
||||
assert.match(activities, /activity-filter-fields/);
|
||||
assert.match(css, /\.activity-focus-switch\s*\{[^}]*grid-template-columns:\s*repeat\(4,\s*minmax\(0,\s*1fr\)\)/s);
|
||||
assert.match(css, /\.activity-focus-btn\s*\{[^}]*min-height:\s*40px/s);
|
||||
assert.match(css, /@media \(max-width: 680px\)[\s\S]*?\.activity-focus-switch\s*\{[^}]*grid-template-columns:\s*1fr 1fr/s);
|
||||
assert.match(css, /@media \(max-width: 680px\)[\s\S]*?\.activity-focus-btn\s*\{[^}]*min-height:\s*48px/s);
|
||||
assert.doesNotMatch(modules, /家纺成熟度|新兴参考/);
|
||||
assert.doesNotMatch(modules, /scope_type === "state\.region"|\["state\.region", "地区政策"\]/);
|
||||
assert.match(modules, /scope_type === "region"/);
|
||||
assert.match(css, /@media \(max-width: 680px\)/);
|
||||
assert.match(css, /\.intelligence-tabs\s*\{[^}]*grid-template-columns: repeat\(3/);
|
||||
assert.match(css, /min-height: 48px/);
|
||||
for (const family of ["STDongGuanTi Light", "STDongGuanTi", "STDongGuanTi Bld"]) assert.match(baseCss, new RegExp(family));
|
||||
assert.match(activities, /activity-date-month/);
|
||||
assert.match(activities, /activity-date-day/);
|
||||
assert.match(activities, /activity-date-countdown/);
|
||||
});
|
||||
|
||||
test("地址栏状态不会写入 undefined", async () => {
|
||||
const app = await text("public/assets/js/app.js");
|
||||
assert.match(app, /value \|\| getIntelligenceSection\(\)/);
|
||||
});
|
||||
|
||||
test("东观体只用于标题且窄卡海报为日历保留空间", async () => {
|
||||
const baseCss = await text("public/assets/css/base.css");
|
||||
const css = await text("public/assets/css/intelligence.css");
|
||||
assert.match(baseCss, /body\s*\{[^}]*font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Noto Sans SC", sans-serif/);
|
||||
assert.doesNotMatch(baseCss, /body\s*\{[^}]*font-family: Inter/);
|
||||
assert.match(baseCss, /\.brand strong\s*\{[^}]*font-family: inherit[^}]*font-weight: 700/);
|
||||
assert.match(baseCss, /\.page-heading h1\s*\{[^}]*font-family: inherit[^}]*font-size: clamp\(30px, 3vw, 40px\)[^}]*font-weight: 700/);
|
||||
assert.match(css, /\.site-shell\.textile-wrap\s*\{[^}]*width: min\(82vw, 1680px\)/);
|
||||
assert.match(css, /@media \(max-width: 680px\)[\s\S]*?\.site-shell\.textile-wrap\s*\{[^}]*width: calc\(100% - 24px\)/);
|
||||
assert.doesNotMatch(baseCss, /\.brand small\s*\{[^}]*font-family:/);
|
||||
assert.doesNotMatch(baseCss, /\.page-heading > p\s*\{[^}]*font-family:/);
|
||||
assert.match(css, /\.intelligence-poster-title\s*\{[^}]*font-family: var\(--textile-editorial-font-bold\)/s);
|
||||
assert.match(css, /\.activity-card-body h3\s*\{[^}]*font-family: var\(--textile-editorial-font-bold\)/);
|
||||
assert.doesNotMatch(css, /\.activity-generated-poster small, \.activity-poster-source\s*\{[^}]*font-family:/);
|
||||
assert.doesNotMatch(css, /\.activity-date-(?:month|day|countdown)\s*\{[^}]*font-family:/);
|
||||
assert.doesNotMatch(css, /\.activity-(?:title-original|summary)[^\{]*\{[^}]*font-family:/);
|
||||
assert.match(css, /\.activity-card-visual\s*\{[^}]*width: 100%[^}]*min-height: 220px/);
|
||||
});
|
||||
@@ -0,0 +1,271 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import importlib.util
|
||||
import re
|
||||
import sys
|
||||
import unittest
|
||||
from datetime import date
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
def load_translation_module():
|
||||
path = ROOT / "scripts/intelligence_web_translation.py"
|
||||
spec = importlib.util.spec_from_file_location("standalone_intelligence_web_translation", path)
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
assert spec.loader
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
def load_activity_module():
|
||||
path = ROOT / "scripts/intelligence_activities.py"
|
||||
spec = importlib.util.spec_from_file_location("standalone_intelligence_activities", path)
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
assert spec.loader
|
||||
sys.path.insert(0, str(ROOT / "scripts"))
|
||||
try:
|
||||
spec.loader.exec_module(module)
|
||||
finally:
|
||||
sys.path.pop(0)
|
||||
return module
|
||||
|
||||
|
||||
class StandaloneProjectContractTests(unittest.TestCase):
|
||||
def test_required_project_files_exist(self) -> None:
|
||||
required = (
|
||||
"README.md",
|
||||
"requirements.txt",
|
||||
"start.bat",
|
||||
"update.bat",
|
||||
"install-schedule.bat",
|
||||
"public/index.html",
|
||||
"scripts/update_all.py",
|
||||
"scripts/build_site.py",
|
||||
"scripts/build_seo.py",
|
||||
"data/textile-intelligence.json",
|
||||
"data/textile-trends.json",
|
||||
"docs/SOURCES.md",
|
||||
"docs/DATA-CONTRACT.md",
|
||||
"docs/PIPELINE.md",
|
||||
"docs/CSHARP-NODE-HANDOFF.md",
|
||||
"docs/OPERATIONS.md",
|
||||
)
|
||||
missing = [path for path in required if not (ROOT / path).is_file()]
|
||||
self.assertEqual([], missing, f"独立项目缺少文件: {missing}")
|
||||
|
||||
def test_runtime_does_not_include_ai_news_or_cloud_deployment(self) -> None:
|
||||
forbidden_paths = (
|
||||
".github",
|
||||
"feeds",
|
||||
"wrangler.toml",
|
||||
"data/latest-24h.json",
|
||||
"data/waytoagi-7d.json",
|
||||
"public/assets/js/render-news.js",
|
||||
"public/assets/js/render-twitter.js",
|
||||
"public/assets/js/render-waytoagi.js",
|
||||
)
|
||||
present = [path for path in forbidden_paths if (ROOT / path).exists()]
|
||||
self.assertEqual([], present, f"独立项目仍包含非家纺运行时: {present}")
|
||||
|
||||
runtime_files = list((ROOT / "public").rglob("*")) + list((ROOT / "scripts").glob("*.py"))
|
||||
runtime_text = "\n".join(
|
||||
path.read_text(encoding="utf-8", errors="ignore")
|
||||
for path in runtime_files
|
||||
if path.is_file()
|
||||
)
|
||||
self.assertNotRegex(runtime_text, re.compile(r"WaytoAGI|latest-24h|loadNewsData|Cloudflare Pages|github-actions", re.I))
|
||||
|
||||
def test_data_snapshot_is_actionable_and_paid_sources_are_absent(self) -> None:
|
||||
intelligence = json.loads((ROOT / "data/textile-intelligence.json").read_text(encoding="utf-8"))
|
||||
trends = json.loads((ROOT / "data/textile-trends.json").read_text(encoding="utf-8"))
|
||||
|
||||
self.assertEqual(intelligence["source_target"], 59)
|
||||
self.assertEqual(intelligence["source_target"], intelligence["source_success"])
|
||||
for key in ("calendar", "exhibitions", "policies", "platforms", "activities"):
|
||||
self.assertTrue(intelligence[key], f"{key} 不应为空")
|
||||
self.assertLessEqual(len(intelligence["today"]), 24)
|
||||
self.assertEqual(
|
||||
{
|
||||
item.get("trend_category_key")
|
||||
for item in intelligence["today"]
|
||||
if item.get("source_id", "").startswith("trend_")
|
||||
},
|
||||
{"trend_blogs", "fashion_trends", "textile_gallery"},
|
||||
)
|
||||
for section in ("calendar", "exhibition", "policy", "standard", "activity"):
|
||||
self.assertIn(section, {item["section"] for item in intelligence["today"]})
|
||||
|
||||
merged = json.dumps((intelligence, trends), ensure_ascii=False)
|
||||
self.assertNotIn("????", merged)
|
||||
self.assertNotRegex(merged, re.compile(r"91jiafang|91家纺|POP趋势网", re.I))
|
||||
met_gallery = [item for item in trends["categories"]["textile_gallery"]["items"] if item["source"] == "The Met"]
|
||||
gallery = trends["categories"]["textile_gallery"]["items"]
|
||||
unrelated = re.compile(r"Papyrus|Reliquary|Relief|Gypsum|Alabaster|Oil on canvas|Sculpture", re.I)
|
||||
self.assertTrue(met_gallery, "The Met 纺织图库不应为空")
|
||||
self.assertFalse(any(
|
||||
"????" in str(item.get(field) or "")
|
||||
for item in gallery
|
||||
for field in ("title", "title_original", "summary", "summary_original")
|
||||
))
|
||||
self.assertTrue(all(not item.get("published_at") for item in gallery))
|
||||
self.assertTrue(all(item.get("image_original") for item in gallery))
|
||||
for field in ("id", "url", "thumbnail", "image_original"):
|
||||
values = [item[field] for item in gallery]
|
||||
self.assertEqual(len(values), len(set(values)), f"图库字段 {field} 不应重复")
|
||||
self.assertEqual(len(gallery), len({(item["source"], item["title"]) for item in gallery}))
|
||||
self.assertFalse(any(item.get("title") == "绞刑" for item in gallery))
|
||||
self.assertEqual(gallery, sorted(gallery, key=lambda item: (item.get("source", ""), item.get("url", ""))))
|
||||
self.assertFalse(any(unrelated.search(f"{item['title_original']} {item['summary_original']}") for item in met_gallery if item.get("title_original") and item.get("title_original") != "Relief panel"))
|
||||
trend_updater = (ROOT / "scripts/update_textile_trends.py").read_text(encoding="utf-8")
|
||||
self.assertIn("validate_payload(payload)", trend_updater)
|
||||
self.assertIn("curate_vam_title(title)", trend_updater)
|
||||
autumn_home_url = "https://intertextile-shanghai-hometextiles-autumn.hk.messefrankfurt.com/shanghai/zh-cn.html"
|
||||
dynamic_autumn = [
|
||||
item for item in intelligence["exhibitions"]
|
||||
if item.get("source_id") == "expo_texpertise_network"
|
||||
and "Intertextile" in item.get("title", "")
|
||||
and "秋季" in item.get("title", "")
|
||||
]
|
||||
self.assertTrue(dynamic_autumn)
|
||||
self.assertTrue(all(item["registration_url"] == autumn_home_url for item in dynamic_autumn))
|
||||
self.assertTrue(all(item["visitor_url"] == autumn_home_url for item in dynamic_autumn))
|
||||
self.assertTrue(all(not item["needs_translation"] for item in dynamic_autumn))
|
||||
exhibition_builder = (ROOT / "scripts/intelligence_exhibitions.py").read_text(encoding="utf-8")
|
||||
self.assertIn("official_url = normalize_dynamic_exhibition_url(title, official_url)", exhibition_builder)
|
||||
ccbec = next(item for item in intelligence["exhibitions"] if item.get("source_id") == "exhibition_ccbec")
|
||||
self.assertFalse(ccbec["needs_translation"], "CCBEC 简体中文官网不应生成翻译入口")
|
||||
double_eleven = next(item for item in intelligence["calendar_insights"] if item.get("source_id") == "calendar_tmall")
|
||||
self.assertIn("alibabagroup.com", double_eleven["source_url"], "双十一必须引用阿里巴巴官方活动证据页")
|
||||
for title, expected in (("圣母无染原罪节", 1), ("圣斯蒂芬节", 1), ("跨年夜", 1)):
|
||||
matches = [
|
||||
item for item in intelligence["calendar_insights"]
|
||||
if item.get("title") == title and item.get("start_date", "").startswith("2026-")
|
||||
]
|
||||
self.assertEqual(expected, len(matches), f"{title} 应跨国家聚合为单一日历卡片")
|
||||
self.assertTrue(all(item.get("title_english") for item in intelligence["calendar_insights"] if item.get("country_details")))
|
||||
boxing_day = next(item for item in intelligence["calendar_insights"] if item.get("festival_key") == "boxing-day")
|
||||
self.assertEqual(boxing_day["title_english"], "Boxing Day")
|
||||
cbp_titles = " ".join(item["title"] for item in intelligence["activities"] if item["source_id"] == "activity_cbp")
|
||||
self.assertNotIn("女式西装", cbp_titles)
|
||||
self.assertNotIn("Women's Suits", cbp_titles)
|
||||
self.assertNotIn("绷带", cbp_titles)
|
||||
self.assertNotIn("Bandages", cbp_titles)
|
||||
self.assertTrue(all("bing.com/images/search" in item["visual_reference_url"] for item in intelligence["calendar_insights"] if item.get("visual_reference_url")))
|
||||
for platform in intelligence["platforms"]:
|
||||
for prefix in ("official", "homepage", "seller"):
|
||||
if platform[f"{prefix}_needs_translation"]:
|
||||
self.assertIn("tl=zh-CN", platform[f"{prefix}_translation_url"])
|
||||
self.assertIn("translate.yandex.com/translate", platform[f"{prefix}_translation_fallback_url"])
|
||||
|
||||
def test_known_translation_restrictions_match_verified_services(self) -> None:
|
||||
translation_fields = load_translation_module().translation_fields
|
||||
cpsc = translation_fields("https://www.cpsc.gov/Recalls/2026/example")
|
||||
zatca = translation_fields("https://zatca.gov.sa/en/RulesRegulations/example")
|
||||
abai = translation_fields("https://abai.kgd.gov.kz/en/content/example")
|
||||
for fields in (cpsc, zatca):
|
||||
self.assertFalse(fields["translation_url"])
|
||||
self.assertIn("translate.yandex.com/translate", fields["translation_fallback_url"])
|
||||
self.assertFalse(abai["translation_url"])
|
||||
self.assertFalse(abai["translation_fallback_url"])
|
||||
chinese_html = translation_fields(
|
||||
"https://intertextile-shanghai-hometextiles-autumn.hk.messefrankfurt.com/shanghai/zh-cn.html",
|
||||
"en",
|
||||
"Intertextile Shanghai Home Textiles Autumn Edition",
|
||||
)
|
||||
self.assertFalse(chinese_html["needs_translation"])
|
||||
self.assertFalse(chinese_html["translation_url"])
|
||||
ccpit = translation_fields("https://bizevent.ccpit.org/meetings/4575")
|
||||
self.assertFalse(ccpit["needs_translation"])
|
||||
self.assertFalse(ccpit["translation_url"])
|
||||
ibte = translation_fields("https://ibte.co.id/")
|
||||
self.assertTrue(ibte["needs_translation"])
|
||||
self.assertIn("translate.google.com/translate", ibte["translation_url"])
|
||||
self.assertIn("translate.yandex.com/translate", ibte["translation_fallback_url"])
|
||||
|
||||
def test_cifnews_activity_uses_detail_page_without_translation(self) -> None:
|
||||
activities = load_activity_module()
|
||||
source = next(item for item in activities.ACTIVITY_SOURCES if item["id"] == "activity_cifnews")
|
||||
detail_url = "https://www.cifnews.com/activity/3209"
|
||||
item = activities._item(
|
||||
source=source,
|
||||
title="家纺跨境平台招商会",
|
||||
start_date=date(2026, 9, 1),
|
||||
url=detail_url,
|
||||
checked_at="2026-08-14T12:00:00+08:00",
|
||||
location="深圳",
|
||||
)
|
||||
self.assertIsNotNone(item)
|
||||
self.assertEqual(detail_url, item["registration_url"])
|
||||
self.assertEqual(detail_url, item["source_url"])
|
||||
self.assertFalse(item["needs_translation"])
|
||||
self.assertFalse(item["source_needs_translation"])
|
||||
self.assertFalse(item["source_translation_url"])
|
||||
self.assertFalse(item["source_translation_fallback_url"])
|
||||
|
||||
def test_activity_snapshot_translates_ibte_but_not_chinese_ccpit(self) -> None:
|
||||
intelligence = json.loads((ROOT / "data/textile-intelligence.json").read_text(encoding="utf-8"))
|
||||
ibte = next(item for item in intelligence["exhibitions"] if "ibte.co.id" in item.get("registration_url", ""))
|
||||
self.assertTrue(ibte["registration_translation_url"])
|
||||
self.assertTrue(ibte["registration_translation_fallback_url"])
|
||||
ccpit = next(item for item in intelligence["activities"] if "bizevent.ccpit.org/meetings/4575" in item.get("registration_url", ""))
|
||||
self.assertFalse(ccpit["needs_translation"])
|
||||
self.assertFalse(ccpit["translation_url"])
|
||||
self.assertFalse(ccpit["translation_fallback_url"])
|
||||
self.assertFalse(ccpit["source_needs_translation"])
|
||||
self.assertFalse(ccpit["source_translation_url"])
|
||||
self.assertFalse(ccpit["source_translation_fallback_url"])
|
||||
|
||||
def test_activity_snapshot_keeps_unexpired_items_when_source_window_shrinks(self) -> None:
|
||||
activities = load_activity_module()
|
||||
current = [{"id": "new", "title": "新活动", "start_date": "2026-08-20", "end_date": "2026-08-20", "official_activity": True}]
|
||||
previous = [{"id": "old", "title": "旧活动", "start_date": "2026-08-21", "end_date": "2026-08-21", "official_activity": False}, {"id": "expired", "title": "过期活动", "start_date": "2026-08-13", "end_date": "2026-08-13"}]
|
||||
merged = activities.merge_previous_activities(current, previous, date(2026, 8, 14))
|
||||
self.assertEqual([item["id"] for item in merged], ["new", "old"])
|
||||
|
||||
def test_new_authority_sources_are_registered(self) -> None:
|
||||
source_code = (ROOT / "scripts/intelligence_sources.py").read_text(encoding="utf-8")
|
||||
for source_id in ("policy_tw_bsmi_textile", "policy_saudi_saso_textile", "exhibition_ccbec", "exhibition_hktdc_home_textiles"):
|
||||
self.assertIn(source_id, source_code)
|
||||
|
||||
def test_source_files_remain_maintainable(self) -> None:
|
||||
seo_builder = (ROOT / "scripts/build_seo.py").read_text(encoding="utf-8")
|
||||
self.assertIn("official_translation_url", seo_builder)
|
||||
self.assertIn("source_translation_url", seo_builder)
|
||||
self.assertIn("seo_config", seo_builder)
|
||||
self.assertNotIn("selected = items[:", seo_builder)
|
||||
self.assertNotIn("selected[:420]", seo_builder)
|
||||
oversized = []
|
||||
for folder, suffix in ((ROOT / "scripts", "*.py"), (ROOT / "public/assets/js", "*.js")):
|
||||
for path in folder.glob(suffix):
|
||||
lines = len(path.read_text(encoding="utf-8").splitlines())
|
||||
if lines > 1100:
|
||||
oversized.append(f"{path.relative_to(ROOT)}:{lines}")
|
||||
self.assertEqual([], oversized, f"单文件超过1000行: {oversized}")
|
||||
|
||||
def test_seo_geo_contract_is_explicit(self) -> None:
|
||||
config = (ROOT / "scripts/seo_config.py").read_text(encoding="utf-8")
|
||||
for phrase in ("家纺AI", "三笙AI", "家纺情报", "跨境家纺平台", "家纺跨境平台"):
|
||||
self.assertIn(phrase, config)
|
||||
self.assertIn("ENTITY_DEFINITION", config)
|
||||
self.assertIn("answer_first", config)
|
||||
docs = (ROOT / "docs/SEO-GEO.md").read_text(encoding="utf-8")
|
||||
for phrase in ("canonical", "hreflang", "llms.txt", "机器翻译", "官方事实"):
|
||||
self.assertIn(phrase, docs)
|
||||
|
||||
def test_latest_main_intelligence_logic_is_present(self) -> None:
|
||||
renderer = (ROOT / "public/assets/js/render-intelligence.js").read_text(encoding="utf-8")
|
||||
calendar = (ROOT / "scripts/intelligence_calendar.py").read_text(encoding="utf-8")
|
||||
updater = (ROOT / "scripts/update_textile_intelligence.py").read_text(encoding="utf-8")
|
||||
self.assertNotIn("displayLimit", renderer)
|
||||
self.assertIn("calendar_insights?.length || data.calendar?.length", renderer)
|
||||
self.assertIn("date_variants", calendar)
|
||||
self.assertIn("culture_reference_cn_url", calendar)
|
||||
self.assertNotIn("return selected[:12]", updater)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user