Skip to content

Commit 2bd1c5b

Browse files
authored
Merge pull request #159 from awslabs/refactor/tdd-guard-rails
test: add architecture boundary enforcement tests (TDD guard rails)
2 parents cc887bb + c9f20af commit 2bd1c5b

12 files changed

Lines changed: 894 additions & 0 deletions
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
"""Shared helpers for architecture boundary tests."""
2+
3+
from __future__ import annotations
4+
5+
import ast
6+
from pathlib import Path
7+
8+
# Root of the orb source tree — resolved relative to this file's location
9+
_TESTS_ARCH_DIR = Path(__file__).parent
10+
_REPO_ROOT = _TESTS_ARCH_DIR.parent.parent.parent
11+
SRC_ORB = _REPO_ROOT / "src" / "orb"
12+
13+
14+
def collect_python_files(directory: Path) -> list[Path]:
15+
"""Return all .py files under *directory*, sorted for stable parametrize IDs."""
16+
return sorted(directory.rglob("*.py"))
17+
18+
19+
def extract_imports(filepath: Path) -> list[str]:
20+
"""AST-parse *filepath* and return every imported module name."""
21+
try:
22+
tree = ast.parse(filepath.read_text(encoding="utf-8"))
23+
except (SyntaxError, UnicodeDecodeError):
24+
return []
25+
imports: list[str] = []
26+
for node in ast.walk(tree):
27+
if isinstance(node, ast.Import):
28+
for alias in node.names:
29+
imports.append(alias.name)
30+
elif isinstance(node, ast.ImportFrom):
31+
if node.module:
32+
imports.append(node.module)
33+
return imports
34+
35+
36+
# Paths that are explicitly allowed to cross certain boundaries because they
37+
# perform DI wiring (registering concrete implementations against ports).
38+
EXCEPTION_PATHS: frozenset[str] = frozenset(
39+
str(p) for p in collect_python_files(SRC_ORB / "infrastructure" / "di")
40+
)
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
"""Task 1740: Application layer import boundary tests.
2+
3+
Asserts that no file under src/orb/application/ imports from infrastructure,
4+
providers, interface, api, or cli layers.
5+
6+
Known violations are tracked in violation_counts.json (ratchet).
7+
"""
8+
9+
from __future__ import annotations
10+
11+
from pathlib import Path
12+
13+
import pytest
14+
15+
from tests.unit.architecture.conftest import (
16+
SRC_ORB,
17+
collect_python_files,
18+
extract_imports,
19+
)
20+
21+
_APP_DIR = SRC_ORB / "application"
22+
23+
_FORBIDDEN_PREFIXES = (
24+
"orb.infrastructure",
25+
"orb.providers",
26+
"orb.interface",
27+
"orb.api",
28+
"orb.cli",
29+
)
30+
31+
# Known violations that exist in the current codebase — whitelisted so tests
32+
# pass today while still catching any NEW violations introduced.
33+
_KNOWN_VIOLATIONS: frozenset[tuple[str, str]] = frozenset(
34+
{
35+
("application/queries/template_query_handlers.py", "orb.infrastructure.template.dtos"),
36+
(
37+
"application/commands/cleanup_handlers.py",
38+
"orb.infrastructure.events.infrastructure_events",
39+
),
40+
(
41+
"application/services/provisioning_orchestration_service.py",
42+
"orb.infrastructure.resilience.exceptions",
43+
),
44+
(
45+
"application/services/provisioning_orchestration_service.py",
46+
"orb.infrastructure.resilience.strategy.circuit_breaker",
47+
),
48+
("application/services/provider_registry_service.py", "orb.providers.registry"),
49+
}
50+
)
51+
52+
_APP_FILES = collect_python_files(_APP_DIR)
53+
54+
55+
@pytest.mark.parametrize("filepath", _APP_FILES, ids=lambda p: str(p.relative_to(SRC_ORB)))
56+
@pytest.mark.unit
57+
@pytest.mark.architecture
58+
def test_application_file_has_no_new_forbidden_imports(filepath: Path) -> None:
59+
"""Application file must not introduce NEW imports from outer layers."""
60+
rel = str(filepath.relative_to(SRC_ORB))
61+
imports = extract_imports(filepath)
62+
new_violations = [
63+
imp
64+
for imp in imports
65+
if any(imp == prefix or imp.startswith(prefix + ".") for prefix in _FORBIDDEN_PREFIXES)
66+
and (rel, imp) not in _KNOWN_VIOLATIONS
67+
]
68+
assert new_violations == [], (
69+
f"{rel} has NEW forbidden imports (not in known-violations whitelist): {new_violations}"
70+
)
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
"""Task 1746: Cross-layer circular dependency test.
2+
3+
Builds a module-level dependency graph from all src/orb/ files and asserts
4+
there are no circular dependencies between top-level packages.
5+
6+
Current known cycles are whitelisted so the test passes today while preventing
7+
any NEW cycles from being introduced.
8+
"""
9+
10+
from __future__ import annotations
11+
12+
import collections
13+
14+
from tests.unit.architecture.conftest import (
15+
SRC_ORB,
16+
collect_python_files,
17+
extract_imports,
18+
)
19+
20+
_TOP_PACKAGES = frozenset(
21+
[
22+
"domain",
23+
"application",
24+
"infrastructure",
25+
"interface",
26+
"api",
27+
"cli",
28+
"providers",
29+
"config",
30+
"mcp",
31+
"monitoring",
32+
"sdk",
33+
]
34+
)
35+
36+
# Known cycles that exist in the current codebase. Each cycle is stored as a
37+
# frozenset of the participating package names (direction-independent, duplicate-free).
38+
# A detected cycle is "known" if its node-set is a subset of any known cycle node-set.
39+
_KNOWN_CYCLE_NODESETS: list[frozenset[str]] = [
40+
frozenset({"api", "application", "config", "infrastructure"}),
41+
frozenset({"application", "config", "infrastructure"}),
42+
frozenset({"application", "config", "infrastructure", "cli"}),
43+
frozenset({"infrastructure", "cli"}),
44+
frozenset({"api", "application", "config", "infrastructure", "cli", "interface"}),
45+
frozenset({"application", "config", "infrastructure", "cli", "interface"}),
46+
frozenset({"cli", "interface"}),
47+
frozenset({"config", "infrastructure", "cli", "interface"}),
48+
frozenset({"infrastructure", "cli", "interface"}),
49+
frozenset({"application", "config", "infrastructure", "cli", "interface", "mcp", "sdk"}),
50+
frozenset({"infrastructure", "cli", "interface", "mcp", "sdk"}),
51+
frozenset({"config", "infrastructure", "cli", "interface", "monitoring"}),
52+
frozenset({"infrastructure", "cli", "interface", "monitoring"}),
53+
frozenset({"application", "config", "infrastructure", "cli", "interface", "providers"}),
54+
frozenset({"cli", "interface", "providers"}),
55+
frozenset({"config", "infrastructure", "cli", "interface", "providers"}),
56+
frozenset({"infrastructure", "cli", "interface", "providers"}),
57+
frozenset({"config", "infrastructure"}),
58+
]
59+
60+
61+
def _build_graph() -> dict[str, set[str]]:
62+
graph: dict[str, set[str]] = collections.defaultdict(set)
63+
for f in collect_python_files(SRC_ORB):
64+
parts = f.relative_to(SRC_ORB).parts
65+
if not parts:
66+
continue
67+
src_pkg = parts[0]
68+
if src_pkg not in _TOP_PACKAGES:
69+
continue
70+
for imp in extract_imports(f):
71+
if imp.startswith("orb."):
72+
rest = imp[4:].split(".")[0]
73+
if rest in _TOP_PACKAGES and rest != src_pkg:
74+
graph[src_pkg].add(rest)
75+
return dict(graph)
76+
77+
78+
def _find_cycles(graph: dict[str, set[str]]) -> list[list[str]]:
79+
cycles: list[list[str]] = []
80+
visited: set[str] = set()
81+
rec_stack: set[str] = set()
82+
path: list[str] = []
83+
seen_keys: set[tuple[str, ...]] = set()
84+
85+
def dfs(node: str) -> None:
86+
visited.add(node)
87+
rec_stack.add(node)
88+
path.append(node)
89+
for neighbor in sorted(graph.get(node, [])):
90+
if neighbor not in visited:
91+
dfs(neighbor)
92+
elif neighbor in rec_stack:
93+
start = path.index(neighbor)
94+
cycle = path[start:] + [neighbor]
95+
key = tuple(sorted(set(cycle)))
96+
if key not in seen_keys:
97+
seen_keys.add(key)
98+
cycles.append(cycle)
99+
path.pop()
100+
rec_stack.discard(node)
101+
102+
for node in sorted(graph.keys()):
103+
if node not in visited:
104+
dfs(node)
105+
return cycles
106+
107+
108+
def test_no_new_circular_dependencies() -> None:
109+
"""No new circular dependencies may be introduced between top-level packages."""
110+
graph = _build_graph()
111+
cycles = _find_cycles(graph)
112+
113+
new_cycles = []
114+
for cycle in cycles:
115+
cycle_nodeset = frozenset(cycle) # includes the repeated last node, frozenset dedupes
116+
is_known = any(cycle_nodeset <= known for known in _KNOWN_CYCLE_NODESETS)
117+
if not is_known:
118+
new_cycles.append(" -> ".join(cycle))
119+
120+
assert new_cycles == [], (
121+
f"NEW circular dependencies detected between top-level packages:\n"
122+
+ "\n".join(f" {c}" for c in new_cycles)
123+
)
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
"""Task 1747: CLI-to-infrastructure direct import test.
2+
3+
Asserts that no file under src/orb/cli/ imports directly from orb.infrastructure
4+
except through the DI container (which is an allowed entry point).
5+
6+
Known violations are whitelisted so tests pass today while catching NEW ones.
7+
"""
8+
9+
from __future__ import annotations
10+
11+
from pathlib import Path
12+
13+
import pytest
14+
15+
from tests.unit.architecture.conftest import (
16+
SRC_ORB,
17+
collect_python_files,
18+
extract_imports,
19+
)
20+
21+
_CLI_DIR = SRC_ORB / "cli"
22+
_CLI_FILES = collect_python_files(_CLI_DIR)
23+
24+
# Known violations — CLI files that currently import infrastructure directly.
25+
# Remove entries as they are cleaned up.
26+
_KNOWN_VIOLATIONS: frozenset[tuple[str, str]] = frozenset(
27+
{
28+
("cli/console.py", "orb.infrastructure.constants"),
29+
("cli/registry.py", "orb.infrastructure.di.buses"),
30+
("cli/registry.py", "orb.infrastructure.di.container"),
31+
("cli/router.py", "orb.infrastructure.di.container"),
32+
("cli/response_formatter.py", "orb.infrastructure.logging.logger"),
33+
("cli/main.py", "orb.infrastructure.logging.logger"),
34+
("cli/main.py", "orb.infrastructure.mocking.dry_run_context"),
35+
("cli/main.py", "orb.infrastructure.di.container"),
36+
("cli/factories/provider_command_factory.py", "orb.infrastructure.utilities.json_utils"),
37+
}
38+
)
39+
40+
41+
@pytest.mark.parametrize("filepath", _CLI_FILES, ids=lambda p: str(p.relative_to(SRC_ORB)))
42+
@pytest.mark.unit
43+
@pytest.mark.architecture
44+
def test_cli_has_no_new_infrastructure_import(filepath: Path) -> None:
45+
"""CLI file must not introduce NEW direct imports from orb.infrastructure."""
46+
rel = str(filepath.relative_to(SRC_ORB))
47+
imports = extract_imports(filepath)
48+
new_violations = [
49+
imp
50+
for imp in imports
51+
if (imp == "orb.infrastructure" or imp.startswith("orb.infrastructure."))
52+
and (rel, imp) not in _KNOWN_VIOLATIONS
53+
]
54+
assert new_violations == [], (
55+
f"{rel} has NEW direct infrastructure imports — route through application layer "
56+
f"or DI ports instead: {new_violations}"
57+
)
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
"""Task 1739: Domain layer import boundary tests.
2+
3+
Asserts that no file under src/orb/domain/ imports from infrastructure,
4+
providers, interface, api, or cli layers.
5+
"""
6+
7+
from __future__ import annotations
8+
9+
from pathlib import Path
10+
11+
import pytest
12+
13+
from tests.unit.architecture.conftest import (
14+
SRC_ORB,
15+
collect_python_files,
16+
extract_imports,
17+
)
18+
19+
_DOMAIN_DIR = SRC_ORB / "domain"
20+
21+
_FORBIDDEN_PREFIXES = (
22+
"orb.infrastructure",
23+
"orb.providers",
24+
"orb.interface",
25+
"orb.api",
26+
"orb.cli",
27+
)
28+
29+
_DOMAIN_FILES = collect_python_files(_DOMAIN_DIR)
30+
31+
32+
@pytest.mark.parametrize("filepath", _DOMAIN_FILES, ids=lambda p: str(p.relative_to(SRC_ORB)))
33+
@pytest.mark.unit
34+
@pytest.mark.architecture
35+
def test_domain_file_has_no_forbidden_imports(filepath: Path) -> None:
36+
"""Domain file must not import from outer layers."""
37+
imports = extract_imports(filepath)
38+
violations = [
39+
imp
40+
for imp in imports
41+
if any(imp == prefix or imp.startswith(prefix + ".") for prefix in _FORBIDDEN_PREFIXES)
42+
]
43+
assert violations == [], (
44+
f"{filepath.relative_to(SRC_ORB)} imports from forbidden layers: {violations}"
45+
)

0 commit comments

Comments
 (0)