| """Additional cascading-failure tests for service dependency propagation.""" | |
| from __future__ import annotations | |
| from env.services import ServiceMesh | |
| def test_database_failure_reaches_notifications_transitively() -> None: | |
| """Database outage should propagate through payments to notifications.""" | |
| mesh = ServiceMesh(seed=101) | |
| mesh.inject_failure("database", "oom") | |
| summary = mesh.get_health_summary() | |
| assert summary["database"] == "down" | |
| assert summary["payments"] == "degraded" | |
| assert summary["notifications"] == "degraded" | |
| def test_root_fix_heals_transitive_chain() -> None: | |
| """Fixing the root service should recover all cascade-only dependents.""" | |
| mesh = ServiceMesh(seed=202) | |
| mesh.inject_failure("database", "oom") | |
| result = mesh.apply_fix("database", "memory_increase") | |
| assert result.success is True | |
| assert set(result.healed_services) >= {"database", "payments", "analytics", "notifications"} | |
| assert mesh.get_health_summary()["payments"] == "healthy" | |
| assert mesh.get_health_summary()["notifications"] == "healthy" | |
| def test_wrong_fix_on_root_damages_dependents_when_cascade_enabled() -> None: | |
| """Blast radius should degrade dependents for cascade-enabled wrong fixes.""" | |
| mesh = ServiceMesh(seed=303) | |
| mesh.inject_failure("database", "oom") | |
| blast = mesh.apply_wrong_fix("database", "config_change") | |
| assert blast.cascade is True | |
| assert "database" in blast.damaged_services | |
| assert "payments" in blast.damaged_services | |
| assert mesh.get_health_summary()["database"] == "down" | |
| assert mesh.get_health_summary()["payments"] == "degraded" | |
| def test_symptom_fix_does_not_clear_root_cause_flag() -> None: | |
| """Fixing a downstream symptom must not clear the actual root-cause marker.""" | |
| mesh = ServiceMesh(seed=404) | |
| mesh.inject_failure("database", "oom") | |
| result = mesh.apply_fix("payments", "restart_service") | |
| assert result.success is False | |
| assert mesh.services["database"].is_root_cause is True | |
| def test_deterministic_cascade_state_for_same_seed() -> None: | |
| """Same seed and same failure should produce identical cascade snapshots.""" | |
| first = ServiceMesh(seed=505) | |
| second = ServiceMesh(seed=505) | |
| first.inject_failure("database", "oom") | |
| second.inject_failure("database", "oom") | |
| assert first.get_health_summary() == second.get_health_summary() | |
| def test_auth_failure_cascades_to_payments_and_notifications() -> None: | |
| """Auth outage should degrade payments which degrades notifications.""" | |
| mesh = ServiceMesh(seed=606) | |
| mesh.inject_failure("auth", "token_expiry") | |
| summary = mesh.get_health_summary() | |
| assert summary["auth"] == "down" | |
| assert summary["payments"] == "degraded" | |
| assert summary["notifications"] == "degraded" | |
| def test_analytics_failure_does_not_cascade_upstream() -> None: | |
| """Analytics has no downstream dependents.""" | |
| mesh = ServiceMesh(seed=707) | |
| mesh.inject_failure("analytics", "batch_job_runaway") | |
| summary = mesh.get_health_summary() | |
| assert summary["analytics"] == "down" | |
| assert summary["database"] == "healthy" | |
| assert summary["payments"] == "healthy" | |
| def test_notifications_failure_is_isolated() -> None: | |
| """Notifications is a leaf node with no dependents.""" | |
| mesh = ServiceMesh(seed=808) | |
| mesh.inject_failure("notifications", "queue_overflow") | |
| summary = mesh.get_health_summary() | |
| assert summary["notifications"] == "down" | |
| assert summary["payments"] == "healthy" | |
| assert summary["auth"] == "healthy" | |
| def test_fix_wrong_service_does_not_clear_root() -> None: | |
| """Fixing a non-root service should not clear the root-cause outage.""" | |
| mesh = ServiceMesh(seed=909) | |
| mesh.inject_failure("database", "oom") | |
| mesh.apply_fix("analytics", "restart_service") | |
| assert mesh.services["database"].is_root_cause is True | |
| assert mesh.services["database"].health == "down" | |
| def test_monitoring_snapshot_after_failure() -> None: | |
| """Monitoring data should reflect injected failures.""" | |
| mesh = ServiceMesh(seed=1010) | |
| mesh.inject_failure("database", "oom") | |
| snapshot = mesh.get_monitoring_data() | |
| db_record = next(r for r in snapshot.services if r.service == "database") | |
| assert db_record.health == "down" | |
| assert db_record.error_rate > 0.5 | |
| def test_blast_radius_non_cascade_fix() -> None: | |
| """Restart has cascade=False, so only the targeted service is damaged.""" | |
| mesh = ServiceMesh(seed=1111) | |
| mesh.inject_failure("auth", "rate_limiting") | |
| result = mesh.apply_wrong_fix("database", "restart_service") | |
| assert result.cascade is False | |
| assert result.damaged_services == ["database"] | |
| def test_double_failure_both_roots_marked() -> None: | |
| """Two independent failures should both be root causes.""" | |
| mesh = ServiceMesh(seed=1212) | |
| mesh.inject_failure("auth", "config_corruption") | |
| mesh.inject_failure("analytics", "query_timeout") | |
| assert mesh.services["auth"].is_root_cause is True | |
| assert mesh.services["analytics"].is_root_cause is True | |
| def test_heal_does_not_affect_independent_root() -> None: | |
| """Fixing one root cause should not heal another independent outage.""" | |
| mesh = ServiceMesh(seed=1313) | |
| mesh.inject_failure("auth", "config_corruption") | |
| mesh.inject_failure("analytics", "query_timeout") | |
| mesh.apply_fix("auth", "config_change") | |
| assert mesh.services["auth"].health == "healthy" | |
| assert mesh.services["analytics"].health == "down" | |
| def test_get_health_summary_returns_all_services() -> None: | |
| mesh = ServiceMesh(seed=1414) | |
| summary = mesh.get_health_summary() | |
| assert set(summary.keys()) == {"auth", "database", "payments", "analytics", "notifications"} | |