from datetime import UTC, datetime, timedelta from app.discovery_store import DiscoveryStore from app.evidence import resolve_health from app.models import ( EvidenceProvider, EvidenceScope, HealthEvidence, HealthStatus, ServiceCreate, ServiceKind, ) from app.store import Store def _evidence( evidence_id: str, *, provider: EvidenceProvider, status: HealthStatus, scope: EvidenceScope = EvidenceScope.COMPONENT, service_id: str | None = "svc", observed_at: datetime | None = None, ) -> HealthEvidence: observed = observed_at or datetime.now(UTC) return HealthEvidence( id=evidence_id, subject_type="service" if scope == EvidenceScope.COMPONENT else "application", subject_id=service_id or "appops:demo", service_id=service_id, provider=provider, provider_ref=provider.value, status=status, scope=scope, confidence=0.8, observed_at=observed, valid_until=observed + timedelta(minutes=5), received_at=observed, summary="test evidence", ) def test_application_scope_evidence_never_becomes_component_direct_health(tmp_path) -> None: store = Store(str(tmp_path / "servicemap.db")) service = store.create_service( ServiceCreate(name="API", kind=ServiceKind.HTTP, url="https://api.local") ) store.record_health_evidence( _evidence( "appops-application", provider=EvidenceProvider.APPOPS, status=HealthStatus.DOWN, scope=EvidenceScope.APPLICATION, service_id=None, ), service_ids=[service.id], ) resolution = store.health_resolution(service.id) assert resolution.selected_evidence_id is None assert resolution.status == HealthStatus.UNKNOWN assert "application-scoped" in resolution.explanation def test_evidence_conflict_and_priority_are_visible(tmp_path) -> None: store = Store(str(tmp_path / "servicemap.db")) service = store.create_service( ServiceCreate(name="API", kind=ServiceKind.HTTP, url="https://api.local") ) store.record_health_evidence( _evidence( "docker-down", provider=EvidenceProvider.DOCKER, status=HealthStatus.DOWN, service_id=service.id, ) ) store.record_health_evidence( _evidence( "probe-healthy", provider=EvidenceProvider.PROBE, status=HealthStatus.HEALTHY, service_id=service.id, ) ) resolution = store.health_resolution(service.id) assert resolution.status == HealthStatus.HEALTHY assert resolution.selected_provider == EvidenceProvider.PROBE assert resolution.conflict is True def test_expired_evidence_is_explained_but_not_selected() -> None: old = datetime.now(UTC) - timedelta(hours=2) resolution = resolve_health( [ _evidence( "expired", provider=EvidenceProvider.DOCKER, status=HealthStatus.DOWN, observed_at=old, ), ] ) assert resolution.selected_evidence_id is None assert resolution.evidence[0].freshness.value == "expired" def test_topology_snapshots_diff_canonical_service_and_dependency_changes(tmp_path) -> None: store = Store(str(tmp_path / "servicemap.db")) discovery = DiscoveryStore(store) database = store.create_service( ServiceCreate(name="Database", kind=ServiceKind.TCP, host="db.local", port=5432) ) first = discovery.capture_snapshot(run_id=None) store.create_service( ServiceCreate( name="API", kind=ServiceKind.HTTP, url="https://api.local", depends_on=[database.id], ) ) second = discovery.capture_snapshot(run_id=None) diff = discovery.topology_diff(after_snapshot_id=second.id, before_snapshot_id=first.id) assert diff.summary["added"] == 2 # API service and its canonical dependency assert {item.entity_type for item in diff.added} == {"service", "relationship"}