""" tests/test_deployment_rules.py ---------------------------------- Regression test for a real bug: build_architecture_graph() used to chain cache -> database -> vector_db serially off each other instead of connecting each directly from the "app" node. That produced a topologically incorrect graph, which fed both the Mermaid diagram and the ArchitectureAdvisor's prompt — causing the AI to reason about a system that didn't exist (observed in practice: a real Hugging Face response claiming "the database is not directly accessible from the application; instead, it's accessed via the Weaviate vector database"). This test locks in the correct, parallel topology so that bug can't silently come back. Run with: python -m unittest discover -s tests """ from __future__ import annotations import unittest from utils.config import InfraConfig from rules.deployment_rules import build_architecture_graph def _make_config(**overrides) -> InfraConfig: defaults = dict( project_name="test-svc", app_framework="FastAPI", cloud_provider="AWS", deployment_target="Kubernetes", database="Cloud SQL (PostgreSQL)", vector_db="Qdrant", cache="Redis", cicd="GitHub Actions", monitoring="Prometheus + Grafana", auth="None", num_users="1,000-10,000/day", budget="$500-$2,000/mo", high_availability=True, ) defaults.update(overrides) return InfraConfig(**defaults) class TestArchitectureGraphTopology(unittest.TestCase): def test_cache_database_vector_db_are_parallel_children_of_app(self): """ cache, database, and vector_db must each connect directly FROM "app" — not from each other. This is the exact bug: a prior version produced app -> cache -> database -> vector_db (a chain), which implied (both visually and to the AI reading the topology) that e.g. the vector database sits between the app and the relational database. """ graph = build_architecture_graph(_make_config()) edge_pairs = {(e.source, e.target) for e in graph.edges} # Each data service must be a direct child of "app"... self.assertIn(("app", "cache"), edge_pairs) self.assertIn(("app", "database"), edge_pairs) self.assertIn(("app", "vector_db"), edge_pairs) # ...and must NOT be chained off each other, in any order. for a, b in [("cache", "database"), ("database", "vector_db"), ("cache", "vector_db"), ("database", "cache"), ("vector_db", "database"), ("vector_db", "cache")]: self.assertNotIn((a, b), edge_pairs, f"found unexpected chained edge {a} -> {b}") def test_absent_services_produce_no_dangling_nodes_or_edges(self): """If cache/database/vector_db are 'None', they must not appear at all.""" graph = build_architecture_graph(_make_config(cache="None", database="None", vector_db="None")) node_ids = {n.id for n in graph.nodes} self.assertNotIn("cache", node_ids) self.assertNotIn("database", node_ids) self.assertNotIn("vector_db", node_ids) # Every edge must reference nodes that actually exist. for edge in graph.edges: self.assertIn(edge.source, node_ids) self.assertIn(edge.target, node_ids) def test_partial_data_services_still_parallel(self): """With only some data services active, the active ones are still parallel, not chained.""" graph = build_architecture_graph(_make_config(cache="Redis", database="None", vector_db="Pinecone")) edge_pairs = {(e.source, e.target) for e in graph.edges} self.assertIn(("app", "cache"), edge_pairs) self.assertNotIn("database", {n.id for n in graph.nodes}) # Pinecone (managed/no local container) still gets a graph node — # the graph models logical architecture, not deployability. self.assertIn(("app", "vector_db"), edge_pairs) self.assertNotIn(("cache", "vector_db"), edge_pairs) def test_monitoring_and_cicd_remain_dashed_side_branches(self): """Monitoring/CI-CD must stay dashed side branches, not on the main data path.""" graph = build_architecture_graph(_make_config()) monitoring_edges = [e for e in graph.edges if "monitoring" in (e.source, e.target)] self.assertTrue(monitoring_edges, "expected a monitoring edge for this config") self.assertTrue(all(e.style == "dashed" for e in monitoring_edges)) cicd_edges = [e for e in graph.edges if "cicd" in (e.source, e.target)] self.assertTrue(cicd_edges, "expected a cicd edge for this config") self.assertTrue(all(e.style == "dashed" for e in cicd_edges)) def test_main_request_path_unchanged(self): """The client -> lb -> deploy_target -> app spine should be untouched by this fix.""" graph = build_architecture_graph(_make_config()) edge_pairs = [(e.source, e.target) for e in graph.edges] self.assertIn(("client", "lb"), edge_pairs) self.assertIn(("lb", "deploy_target"), edge_pairs) self.assertIn(("deploy_target", "app"), edge_pairs) if __name__ == "__main__": unittest.main()