Files
ai-exchange/tests/unit/test_documentation_consistency.py
T
2026-05-22 11:17:04 +08:00

210 lines
9.7 KiB
Python

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_DoctrineKernel市场语言规则内核_D1_2026-05-21_v1.md",
"AI_ICT_DoctrineKernel_PlaybookBacktestCohort_D2_2026-05-21_v1.md",
"AI_ICT_DoctrineKernel_LiquidityKernel_D3_2026-05-21_v1.md",
"AI_ICT_DoctrineKernel_PDArrayReactionKernel_D4_2026-05-22_v1.md",
"AI_ICT_DoctrineKernel_TimeSessionKernel_D5_2026-05-22_v1.md",
"AI_ICT_DoctrineKernel_ModelPlaybookKernel_D6_2026-05-22_v1.md",
"AI_ICT_DoctrineKernel_TraderProductGate_D7_2026-05-22_v1.md",
"AI_ICT_硬性主线约束_必须遵循ICT交易全书_2026-05-21_v1.md",
"AI_ICT_实盘辅助分析决策助手ICT主线开发方案_2026-05-21_v1.md",
"AI_ICT_主线裁剪审计_S1_2026-05-21_v1.md",
"AI_ICT_ICT主线概念字典_S2_2026-05-21_v1.md",
"AI_ICT_ReviewNow主线Gate_S3S4_2026-05-21_v1.md",
"AI_ICT_Cockpit链路句与反馈错误分类_S5S6_2026-05-21_v1.md",
"AI_ICT_DoctrineKernel全书规则内核重建_D0_2026-05-21_v1.md",
"AI_ICT_只读实盘可靠性与交易员SOP_S7S8_2026-05-21_v1.md",
"AI_ICT_TraderCockpit状态话术压缩_S9_2026-05-21_v1.md",
"AI_ICT_TraderCockpit真实反馈验证_S10_2026-05-21_v1.md",
"AI_ICT_批量历史回测验证_S16_2026-05-21_v1.md",
"AI_ICT_主线纠偏与功能裁剪方案_2026-05-20_v1.md",
"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()