Files
Sansheng-Intelligence/tests/test_standalone_contract.py
T
Jack f1ce583fd1 feat: 发布三笙AI情报局独立版公司研发基线
同步家纺情报 v2 数据、59 个严格权威源、趋势图库、移动端交互、Google/Yandex 翻译门控及静态 SEO/GEO 构建。

补充面向 C# 研发的 README,明确当前无 LLM、多模态与生图模型,记录翻译请求参数、缓存与失败策略、数据处理边界、前端职责和无 GitHub Pages 部署流程。

新增按日期倒序维护的 CHANGELOG,记录影响、验证与代码/数据/dist 回滚方法。

验证:11 个 Python 契约测试、13 个 Node 前端测试通过;静态构建生成 1096 个 Sitemap URL。
2026-08-14 19:26:39 +08:00

272 lines
15 KiB
Python

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()