import json import sqlite3 from datetime import UTC, datetime, timedelta from pathlib import Path from uuid import uuid4 from app.config_io import export_document, import_document, parse_document from app.incidents import sync_incidents from app.models import HealthStatus, ProbeResult, ServiceCreate, ServiceKind from app.store import Store def _probe(service_id: str, status: HealthStatus, number: int) -> ProbeResult: return ProbeResult( id=f"probe-{service_id}-{number}", service_id=service_id, status=status, latency_ms=4.0, detail=status.value, checked_at=datetime.now(UTC) + timedelta(microseconds=number), ) def test_v110_schema_migrates_v101_database_without_reset(tmp_path: Path) -> None: path = tmp_path / "servicemap.db" with sqlite3.connect(path) as db: db.executescript( """ CREATE TABLE services ( id TEXT PRIMARY KEY, name TEXT NOT NULL UNIQUE, kind TEXT NOT NULL, url TEXT, host TEXT, port INTEGER, depends_on TEXT NOT NULL DEFAULT '[]', created_at TEXT NOT NULL, updated_at TEXT ); CREATE TABLE probe_results ( id TEXT PRIMARY KEY, service_id TEXT NOT NULL, status TEXT NOT NULL, latency_ms REAL, detail TEXT NOT NULL, checked_at TEXT NOT NULL ); CREATE TABLE demo_sessions ( id INTEGER PRIMARY KEY CHECK (id = 1), snapshot_services TEXT NOT NULL, snapshot_probes TEXT NOT NULL, demo_service_ids TEXT NOT NULL, started_at TEXT NOT NULL ); """ ) created = datetime.now(UTC).isoformat() db.execute( "INSERT INTO services VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)", ("legacy", "Legacy API", "http", "https://legacy.local", None, None, "[]", created, created), ) db.execute( "INSERT INTO probe_results VALUES (?, ?, ?, ?, ?, ?)", ("legacy-probe", "legacy", "healthy", 12.0, "legacy", created), ) store = Store(str(path)) service = store.get_service("legacy") assert service is not None assert service.name == "Legacy API" assert store.service_health("legacy") == HealthStatus.HEALTHY assert store.latest_probe("legacy") is not None assert store.path == path def test_failure_and_recovery_thresholds_are_explicit(tmp_path: Path) -> None: store = Store(str(tmp_path / "servicemap.db")) service = store.create_service( ServiceCreate( name="Threshold API", kind=ServiceKind.HTTP, url="https://threshold.local", failure_threshold=2, recovery_threshold=2, ) ) store.save_probe(_probe(service.id, HealthStatus.DOWN, 1)) assert store.service_health(service.id) == HealthStatus.UNKNOWN store.save_probe(_probe(service.id, HealthStatus.DOWN, 2)) assert store.service_health(service.id) == HealthStatus.DOWN store.save_probe(_probe(service.id, HealthStatus.HEALTHY, 3)) assert store.service_health(service.id) == HealthStatus.RECOVERING store.save_probe(_probe(service.id, HealthStatus.HEALTHY, 4)) assert store.service_health(service.id) == HealthStatus.HEALTHY def test_incident_journal_acknowledges_resolves_and_does_not_duplicate(tmp_path: Path) -> None: store = Store(str(tmp_path / "servicemap.db")) database = store.create_service(ServiceCreate(name="Database", kind=ServiceKind.TCP, host="db.local", port=5432)) api = store.create_service(ServiceCreate(name="API", kind=ServiceKind.HTTP, url="https://api.local", depends_on=[database.id])) store.save_probe(_probe(database.id, HealthStatus.DOWN, 1)) store.save_probe(_probe(database.id, HealthStatus.DOWN, 2)) store.save_probe(_probe(api.id, HealthStatus.HEALTHY, 1)) active = store.list_incidents(active_only=True) assert len(active) == 1 assert active[0].root_service_id == database.id assert api.id in active[0].affected_service_ids assert len([event for event in store.list_events(100) if event.event_type == "incident.opened"]) == 1 acknowledged = store.acknowledge_incident(active[0].id) assert acknowledged is not None assert acknowledged.status.value == "acknowledged" store.save_probe(_probe(database.id, HealthStatus.HEALTHY, 3)) resolved = store.get_incident(active[0].id) assert resolved is not None assert resolved.status.value == "resolved" assert resolved.resolved_at is not None assert any(event.event_type == "incident.resolved" for event in store.list_events(100)) def test_incident_reconciliation_resolves_existing_duplicate_active_roots(tmp_path: Path) -> None: store = Store(str(tmp_path / "servicemap.db")) service = store.create_service(ServiceCreate(name="Duplicate root", kind=ServiceKind.TCP, host="db.local", port=5432)) store.save_probe(_probe(service.id, HealthStatus.DOWN, 1)) store.save_probe(_probe(service.id, HealthStatus.DOWN, 2)) original = store.list_incidents(active_only=True)[0] duplicate = original.model_copy(update={"id": str(uuid4()), "detected_at": original.detected_at + timedelta(seconds=1)}) store.upsert_incident(duplicate) sync_incidents(store) active = store.list_incidents(active_only=True) assert len(active) == 1 assert active[0].root_service_id == service.id assert store.get_incident(duplicate.id).status.value == "resolved" assert any(event.event_type == "incident.duplicate_resolved" for event in store.incident_timeline(duplicate.id)) def test_maintenance_suppresses_incident_and_config_v2_round_trips(tmp_path: Path) -> None: store = Store(str(tmp_path / "servicemap.db")) document = parse_document( """ version: 2 services: - name: DNS edge kind: dns host: edge.internal group: Edge environment: staging tags: [dns, edge] criticality: high interval_seconds: 15 depends_on: [] """, "yaml", ) result = import_document(store, document, "replace") assert result.created == 1 service = store.get_service_by_name("DNS edge") assert service is not None assert service.group == "Edge" assert service.interval_seconds == 15 assert parse_document(json.dumps(export_document(store).model_dump(mode="json")), "json").version == 2 updated = store.set_maintenance(service.id, True, "planned work") assert updated is not None assert store.service_health(service.id) == HealthStatus.MAINTENANCE store.save_probe(_probe(service.id, HealthStatus.DOWN, 1)) sync_incidents(store) assert store.list_incidents(active_only=True) == []