210 lines
9.7 KiB
Python
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()
|