"""Tests for the documented modular-monolith dependency boundaries.""" from __future__ import annotations import ast from collections.abc import Iterator from pathlib import Path SOURCE_ROOT = Path(__file__).resolve().parents[2] / "src" / "ragcore" FRAMEWORK_IMPORTS = ( "alembic", "docling", "fastapi", "httpx", "pydantic", "pydantic_settings", "qdrant_client", "sqlalchemy", "starlette", "uvicorn", ) FORBIDDEN_RAGCORE_IMPORTS = { "domain": { "ragcore.api", "ragcore.application", "ragcore.evaluation", "ragcore.generation", "ragcore.ingestion", "ragcore.infrastructure", "ragcore.platform", "ragcore.retrieval", "ragcore.web", }, "application": { "ragcore.api", "ragcore.infrastructure", "ragcore.platform", "ragcore.web", }, } def _module_name(path: Path) -> str: relative = path.relative_to(SOURCE_ROOT).with_suffix("") parts = relative.parts if parts[-1] == "__init__": parts = parts[:-1] return ".".join(("ragcore", *parts)) def _resolve_relative_import(module_name: str, node: ast.ImportFrom) -> str: if node.level == 0: return node.module or "" package_parts = module_name.split(".")[:-1] base_parts = package_parts[: len(package_parts) - node.level + 1] if node.module: base_parts.extend(node.module.split(".")) return ".".join(base_parts) def _imports(path: Path) -> Iterator[tuple[int, str]]: module_name = _module_name(path) tree = ast.parse(path.read_text(encoding="utf-8"), filename=str(path)) for node in ast.walk(tree): if isinstance(node, ast.Import): for alias in node.names: yield node.lineno, alias.name elif isinstance(node, ast.ImportFrom): yield node.lineno, _resolve_relative_import(module_name, node) def _matches(import_name: str, prefix: str) -> bool: return import_name == prefix or import_name.startswith(f"{prefix}.") def test_domain_and_application_respect_dependency_boundaries() -> None: violations: list[str] = [] for layer, forbidden_modules in FORBIDDEN_RAGCORE_IMPORTS.items(): layer_root = SOURCE_ROOT / layer for path in sorted(layer_root.rglob("*.py")): for line, imported in _imports(path): framework = next( (prefix for prefix in FRAMEWORK_IMPORTS if _matches(imported, prefix)), None, ) forbidden = next( (prefix for prefix in forbidden_modules if _matches(imported, prefix)), None, ) if framework: violations.append( f"{path}:{line}: {layer} imports framework {imported}; " "keep framework adapters at the API/infrastructure boundary" ) elif forbidden: violations.append( f"{path}:{line}: {layer} imports {imported}; " "depend on domain rules and application ports instead" ) assert not violations, "Architecture boundary violations:\n" + "\n".join(violations)