160 lines
6.6 KiB
Python
160 lines
6.6 KiB
Python
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) == []
|