Files
ServiceMap-Public/backend/tests/test_v110_features.py
T

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) == []