93 lines
3.2 KiB
Python
93 lines
3.2 KiB
Python
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()
|