from __future__ import annotations from datetime import UTC, datetime from pathlib import Path from app.discovery_store import DiscoveryStore from app.models import ( DiscoveryConfidence, HealthStatus, MonitoringState, RelationEvidence, RelationType, Service, ServiceCreate, ServiceKind, ServiceSnapshot, TopologySuggestion, ) from app.semantic import build_semantic_topology from app.store import Store def _service(identifier: str, name: str, *, application: str | None = None, category: str = "application") -> Service: now = datetime.now(UTC) return Service( id=identifier, name=name, kind=ServiceKind.HTTP, url=f"https://{identifier}.local", application=application, category=category, created_at=now, updated_at=now, ) def _snapshot(service: Service, direct: HealthStatus, effective: HealthStatus | None = None) -> ServiceSnapshot: return ServiceSnapshot( service=service, direct_status=direct, dependency_status=effective or direct, effective_status=effective or direct, ) def test_semantic_topology_keeps_singletons_as_distinct_applications() -> None: snapshots = [ _snapshot(_service("one", "One", category="application"), HealthStatus.HEALTHY), _snapshot(_service("two", "Two", category="application"), HealthStatus.HEALTHY), ] topology = build_semantic_topology(snapshots, []) assert {app.name for app in topology.applications} == {"One", "Two"} assert all(app.component_count == 1 for app in topology.applications) def test_semantic_app_health_is_explainable_and_ignores_unmonitored_auxiliaries() -> None: postgres = _service("postgres", "Zabbix PostgreSQL", application="Zabbix", category="database") server = _service("server", "Zabbix Server", application="Zabbix", category="monitoring") agent = _service("agent", "Zabbix Agent", application="Zabbix", category="monitoring") agent.monitoring_enabled = False agent.monitoring_state = MonitoringState.UNMONITORED topology = build_semantic_topology([ _snapshot(postgres, HealthStatus.DOWN), _snapshot(server, HealthStatus.HEALTHY), _snapshot(agent, HealthStatus.UNMONITORED), ], [], root_service_ids={"postgres"}, affected_service_ids={"server"}) app = next(item for item in topology.applications if item.name == "Zabbix") assert app.status == HealthStatus.DEGRADED assert app.root_cause is True assert "PostgreSQL" in app.status_explanation assert app.monitored_components == 2 assert app.unmonitored_components == 1 components = {item.service_id: item for item in topology.components} assert components["postgres"].root_cause is True assert components["server"].root_cause is False assert components["server"].affected is True def test_logical_relation_merges_independent_evidence_and_improves_confidence(tmp_path: Path) -> None: store = Store(str(tmp_path / "servicemap.db")) source = store.create_service(ServiceCreate(name="API", kind=ServiceKind.HTTP, url="https://api.local")) target = store.create_service(ServiceCreate(name="Database", kind=ServiceKind.TCP, host="db", port=5432)) discovery = DiscoveryStore(store) now = datetime.now(UTC) for evidence_type, confidence, explanation in ( ("compose", DiscoveryConfidence.MEDIUM, "Compose depends_on declares database"), ("environment", DiscoveryConfidence.HIGH, "DATABASE_HOST resolves to database"), ): discovery.save_evidence(RelationEvidence( id=f"evidence-{evidence_type}", source_service_id=source.id, target_service_id=target.id, relation_type=RelationType.DEPENDS_ON, evidence_type=evidence_type, confidence=confidence, explanation=explanation, source_ref=f"docker:compose:stack:{evidence_type}", first_seen_at=now, last_seen_at=now, )) relations = discovery.list_logical_relations() assert len(relations) == 1 assert relations[0].confidence == DiscoveryConfidence.CONFIRMED assert {item.evidence_type for item in relations[0].evidence} == {"compose", "environment"} def test_repeated_suggestion_merges_changed_evidence_without_a_duplicate(tmp_path: Path) -> None: store = Store(str(tmp_path / "servicemap.db")) discovery = DiscoveryStore(store) run = discovery.start_run() now = datetime.now(UTC) base = { "id": "suggest-rel", "run_id": run.id, "suggestion_type": "relation", "source_ref": "docker:api", "target_ref": "docker:db", "relation_type": RelationType.DEPENDS_ON, "confidence": DiscoveryConfidence.HIGH, "created_at": now, "updated_at": now, } discovery.save_suggestion(TopologySuggestion(**base, evidence=["Compose relationship"]), "canonical-relation") second = {**base, "id": "suggest-rel-second"} saved = discovery.save_suggestion(TopologySuggestion(**second, evidence=["Environment relationship"]), "canonical-relation") assert saved.evidence == ["Compose relationship", "Environment relationship"] assert len(discovery.list_suggestions()) == 1