import ast import unittest from pathlib import Path from scripts import check_local_demo_readiness, check_trading_delivery_readiness, run_offline_checks ROOT = Path(__file__).resolve().parents[2] README = ROOT / "README.md" RUNBOOK = ROOT / "RUNBOOK.md" TRAINING_PUBLIC_DOC = ROOT / "docs" / "training_console_public_surface_v1.md" TRAINING_PACKAGE_SMOKE_DOC = ROOT / "docs" / "examples" / "training_case_package_smoke_m6.md" TRADING_INDEX_HTML = ROOT / "src" / "web" / "index.html" TRADING_APP_JS = ROOT / "src" / "web" / "app.js" TRAINING_HTML = ROOT / "src" / "web" / "training.html" TRAINING_JS = ROOT / "src" / "web" / "training.js" OLD_TRAINING_DOC = ROOT / "docs" / "training_case_engine_m1_m6_product_readme.md" NONEXISTENT_VISUAL_PLAN_NAME = "AI_ICT_" + "视觉盯盘与主动告警增强方案_v1.md" ACTIVE_TOP_LEVEL_DOCS = { "AI_ICT_实盘辅助分析决策助手上线必做清单_2026-05-18_v1.md", "AI_ICT_实盘辅助分析决策助手使用说明_2026-05-18_v1.md", "AI_ICT_敏感凭据低风险清理记录_2026-05-18_v1.md", "AI_ICT_分析助手主线增强开发计划_2026-05-14_v1.md", "AI_ICT_实盘辅助分析决策助手可用性评估与补齐方案_2026-05-17_v1.md", "AI_ICT_全自动开发执行流程_v1.md", "AI_ICT_实盘交易分析决策助手下一阶段开发方案_v1.md", "AI_ICT_实盘交易分析系统开发计划_v1.md", "AI_ICT_实盘辅助决策系统开发方案_v2.md", "AI_ICT_自动化执行层_P15-PXX闭环任务清单_v1.md", "AI_ICT_需求文档剩余功能与延期项清单_2026-05-14_v1.md", "ICT交易全书_出版终校版.md", "training_console_public_surface_v1.md", } ARCHIVED_DOCS = { "AI_ICT_TradingAgents_P0-P6合理性审查与优化结论_v1.md", "AI_ICT_TradingAgents_P0自动任务清单_v1.md", "AI_ICT_TradingAgents_P1自动任务清单_v1.md", "AI_ICT_TradingAgents_P2自动任务清单_v1.md", "AI_ICT_TradingAgents_P3自动任务清单_v1.md", "AI_ICT_TradingAgents_P4自动任务清单_v1.md", "AI_ICT_TradingAgents_P5自动任务清单_v1.md", "AI_ICT_TradingAgents_P6自动任务清单_v1.md", "AI_ICT_TradingAgents融合实施方案_v1.md", "AI_ICT_TradingAgents融合实施方案_最终版.md", "AI_ICT_下一阶段开发任务清单_v1.md", "AI_ICT_下一阶段开发计划_v1.md", "AI_ICT_repair_bridge_protocol_v1.md", "AI_ICT_research_council脚手架与数据表草案_v1.md", "AI_ICT_外部repair_agent接入说明_v1.md", "AI_ICT_实盘交易分析决策助手未完成需求清单_2026-05-14_v1.md", "AI_ICT_方案一系统边界图与模块融合图_v1.md", "AI_ICT_盯盘告警开发任务清单_v1.md", "AI_ICT_盯盘告警技术设计稿_v1.md", "AI_ICT_盯盘告警架构图_v1.md", "AI_ICT_自动化执行层_P7监控成熟度与晋级路由_v1.md", "AI_ICT_自动化流程使用教程_v1.md", } class DocumentationConsistencyTests(unittest.TestCase): def test_docs_inventory_is_current_and_archived_docs_are_not_top_level(self) -> None: top_level_docs = { path.name for path in (ROOT / "docs").iterdir() if path.is_file() } self.assertEqual(top_level_docs, ACTIVE_TOP_LEVEL_DOCS) archive_docs = { path.name for path in (ROOT / "docs" / "archive").iterdir() if path.is_file() } self.assertTrue(ARCHIVED_DOCS <= archive_docs) self.assertFalse(OLD_TRAINING_DOC.exists()) def test_readme_and_runbook_use_current_training_entry_and_not_old_doc(self) -> None: for path in (README, RUNBOOK): text = path.read_text(encoding="utf-8") with self.subTest(path=path.name): self.assertIn("/training", text) self.assertNotIn("training_case_engine_m1_m6_product_readme.md", text) self.assertIn("Training Console at `/training`", README.read_text(encoding="utf-8")) def test_repo_no_longer_references_nonexistent_visual_plan_doc(self) -> None: search_roots = [ ROOT / "docs", ROOT / "scripts", ROOT / "tests", ] allowed_suffixes = {".md", ".py", ".html", ".js", ".json"} for root in search_roots: for path in root.rglob("*"): if not path.is_file() or path.suffix not in allowed_suffixes: continue text = path.read_text(encoding="utf-8") with self.subTest(path=str(path.relative_to(ROOT))): self.assertNotIn(NONEXISTENT_VISUAL_PLAN_NAME, text) def test_training_public_doc_matches_current_console_and_routes(self) -> None: text = TRAINING_PUBLIC_DOC.read_text(encoding="utf-8") for snippet in ( "The public browser entry is `/training`.", "The trading workbench stays at `/`.", "| GET | `/training/cases` |", "| GET | `/training/progress` |", "| POST | `/training/attempts` |", "Training public error catalog:", ): self.assertIn(snippet, text) def test_training_package_smoke_doc_matches_current_fixture_and_cli(self) -> None: text = TRAINING_PACKAGE_SMOKE_DOC.read_text(encoding="utf-8") for snippet in ( "tests/fixtures/training_case_package_m6.json", ".venv/bin/python scripts/run_training_case_package.py", "--input", "--output", "public package", "public export", "public output", ): self.assertIn(snippet, text) def test_trading_root_and_training_console_are_split_cleanly(self) -> None: trading_html = TRADING_INDEX_HTML.read_text(encoding="utf-8") trading_js = TRADING_APP_JS.read_text(encoding="utf-8") training_html = TRAINING_HTML.read_text(encoding="utf-8") training_js = TRAINING_JS.read_text(encoding="utf-8") self.assertNotIn('id="trainingSection"', trading_html) self.assertIn('id="trainingSection"', training_html) self.assertNotIn("TRAINING_PUBLIC_ROUTES", trading_js) self.assertIn("TRAINING_PUBLIC_ROUTES", training_js) def test_offline_checks_include_training_console_syntax(self) -> None: commands = dict(run_offline_checks.build_check_commands(env={})) self.assertEqual(commands["dashboard_js_syntax"], ["node", "--check", "src/web/app.js"]) self.assertEqual(commands["training_console_js_syntax"], ["node", "--check", "src/web/training.js"]) def test_local_demo_readiness_uses_current_docs(self) -> None: self.assertEqual( check_local_demo_readiness.LOCAL_DEMO_DOCS, (README, RUNBOOK), ) def test_trading_delivery_readiness_uses_canonical_smoke_route(self) -> None: commands = dict(check_trading_delivery_readiness.TRADING_DELIVERY_COMMAND_GROUPS)["deploy_and_health"] self.assertIn('curl -fsS "http://127.0.0.1:8000/trading/candidates?instrument_id=1&limit=1"', commands) def test_readme_lists_current_json_output_variables(self) -> None: text = README.read_text(encoding="utf-8") for variable in ( "LIVE_ADAPTER_OUTPUT_JSON", "TRADING_JOURNAL_OUTPUT_JSON", "TRADE_PLAN_CANDIDATES_OUTPUT_JSON", ): self.assertIn(variable, text) def test_scripts_do_not_reference_deleted_training_product_doc(self) -> None: for path in ( ROOT / "scripts" / "check_local_demo_readiness.py", ROOT / "scripts" / "run_offline_checks.py", ROOT / "scripts" / "check_trading_delivery_readiness.py", ): text = path.read_text(encoding="utf-8") with self.subTest(path=path.name): self.assertNotIn("training_case_engine_m1_m6_product_readme.md", text) def test_current_readiness_scripts_are_static_and_offline(self) -> None: for module_path in ( ROOT / "scripts" / "check_local_demo_readiness.py", ROOT / "scripts" / "check_trading_delivery_readiness.py", ): source = module_path.read_text(encoding="utf-8") tree = ast.parse(source) imported_modules = { alias.name for node in ast.walk(tree) if isinstance(node, ast.Import) for alias in node.names } with self.subTest(path=module_path.name): self.assertNotIn("subprocess", imported_modules) if __name__ == "__main__": unittest.main()