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:
Jack
2026-08-14 19:26:39 +08:00
commit f1ce583fd1
231 changed files with 245559 additions and 0 deletions
+197
View File
@@ -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/);
});
+271
View File
@@ -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()