from __future__ import annotations import ast import unittest from pathlib import Path ROOT = Path(__file__).resolve().parents[1] FEATURES = ROOT / "backend" / "features" RETIRED_ROOT_MODULES = { "advanced_strategies", "alert_service", "app_config", "assistant_agent", "chart_data_provider", "heaven_agent", "heaven_engine", "ifind_client", "llm_strategy", "llm_stream", "market_insights", "mentor_agent", "realtime_aggregator", "screener", "security", "sentiment_engine", "strategy_tracking", "trade_journal", "tushare_client", } class FeatureBoundaryTests(unittest.TestCase): def test_feature_services_do_not_import_http_or_provider_adapters(self) -> None: forbidden = { "server", "tushare_client", "ifind_client", "chart_data_provider", "realtime_aggregator", } violations = [] for path in FEATURES.rglob("*.py"): tree = ast.parse(path.read_text(encoding="utf-8"), filename=str(path)) for node in ast.walk(tree): names = [] if isinstance(node, ast.Import): names = [alias.name for alias in node.names] elif isinstance(node, ast.ImportFrom) and node.module: names = [node.module] for name in names: if name.split(".")[0] in forbidden: violations.append(f"{path.relative_to(ROOT)} -> {name}") self.assertEqual(violations, []) def test_backend_does_not_import_retired_root_modules(self) -> None: violations = [] for path in (ROOT / "backend").rglob("*.py"): relative = path.relative_to(ROOT).as_posix() tree = ast.parse(path.read_text(encoding="utf-8"), filename=str(path)) for node in ast.walk(tree): names = [] if isinstance(node, ast.Import): names = [alias.name for alias in node.names] elif isinstance(node, ast.ImportFrom) and node.module: names = [node.module] for name in names: root_name = name.split(".")[0] if root_name in RETIRED_ROOT_MODULES: violations.append(f"{relative} -> {name}") self.assertEqual(violations, []) def test_retired_root_modules_are_absent(self) -> None: present = sorted( name for name in RETIRED_ROOT_MODULES if (ROOT / f"{name}.py").exists() ) self.assertEqual(present, []) def test_each_migrated_feature_owns_one_application_service(self) -> None: expected = { "alerts/service.py": "AlertService", "mentor/service.py": "MentorServiceMixin", "review/trade_journal.py": "TradeJournalService", "screener/tracking.py": "StrategyTrackingService", } for relative, class_name in expected.items(): tree = ast.parse((FEATURES / relative).read_text(encoding="utf-8")) self.assertIn(class_name, {node.name for node in tree.body if isinstance(node, ast.ClassDef)}) if __name__ == "__main__": unittest.main()