133 lines
4.1 KiB
Python
133 lines
4.1 KiB
Python
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"}
|