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"): for field in ("needs_translation", "translation_url", "translation_fallback_url"): self.assertNotIn(f"{prefix}_{field}", platform) if platform["seller_needs_translation"]: self.assertIn("tl=zh-CN", platform["seller_translation_url"]) self.assertIn("translate.yandex.com/translate", platform["seller_translation_fallback_url"]) platforms = {platform["id"]: platform for platform in intelligence["platforms"]} self.assertTrue(platforms["temu"]["seller_needs_translation"]) self.assertFalse(platforms["shopee"]["seller_needs_translation"]) self.assertFalse(platforms["shopify"]["seller_needs_translation"]) 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"]) shopify_zh = translation_fields("https://www.shopify.com/zh") self.assertFalse(shopify_zh["needs_translation"]) self.assertFalse(shopify_zh["translation_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/", "zh-CN", "国际展会") 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("seller_translation_url", seo_builder) self.assertNotIn("official_translation_url", seo_builder) self.assertNotIn("homepage_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()