- node-agent: service_healthy emitowany tylko przy przejsciu w stan zdrowy (per-service in-memory state), nie co cykl dla kazdego zdrowego serwisu. To samo dla control-plane HTTP probe. healthcheck_failed/containers_not_running/ incydenty pozostaja emitowane bez zmian (realne sygnaly). - node-agent: naprawiono _cleanup_control_plane_fs — istniejaca retencja eventow byla martwa od migracji observer_checkpoint.json na epoch-int (str(path) <= int rzucal TypeError, lapane cicho przez broad except). Teraz porownuje epoch-do-epoch i czysci wylacznie service_healthy/node_health starsze niz checkpoint + 3-dniowy bufor; healthcheck_failed/incydenty/ha_* zachowane bezterminowo. - scripts/maintenance/cleanup_event_backlog.py: jednorazowy skrypt do bezpiecznego czyszczenia backlogu na VPS (dry-run domyslnie, --apply do usuniecia). Ten sam warunek: typ szumu + starsze niz checkpoint + 1h bufor.
65 lines
2.3 KiB
Python
65 lines
2.3 KiB
Python
"""Tests for NodeAgent._check_control_plane_health() transition-only emission.
|
|
|
|
Regression coverage for the event-flood fix: the VPS control-plane HTTP probe
|
|
used to emit service_healthy every single cycle it was reachable, which was
|
|
part of the same flood as check_containers()'s per-service confirmation.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from unittest.mock import MagicMock
|
|
|
|
import node_agent
|
|
|
|
|
|
def run_probe(agent, monkeypatch, *, status=200, raise_exc=None):
|
|
"""Run _check_control_plane_health() with urlopen mocked, capturing emitted events."""
|
|
emitted = []
|
|
|
|
def fake_emit(event_type, severity, service, message, payload=None):
|
|
emitted.append((event_type, severity, service, message, payload or {}))
|
|
|
|
monkeypatch.setattr(agent, "emit_event", fake_emit)
|
|
|
|
if raise_exc is not None:
|
|
def fake_urlopen(*a, **kw):
|
|
raise raise_exc
|
|
else:
|
|
resp = MagicMock()
|
|
resp.status = status
|
|
|
|
def fake_urlopen(*a, **kw):
|
|
return resp
|
|
|
|
monkeypatch.setattr("urllib.request.urlopen", fake_urlopen)
|
|
agent._check_control_plane_health()
|
|
return emitted
|
|
|
|
|
|
def test_reachable_emits_service_healthy(agent, monkeypatch):
|
|
events = run_probe(agent, monkeypatch, status=200)
|
|
assert len(events) == 1
|
|
assert events[0][0] == "service_healthy"
|
|
assert events[0][2] == "control-plane"
|
|
|
|
|
|
def test_reachable_second_cycle_is_silent(agent, monkeypatch):
|
|
run_probe(agent, monkeypatch, status=200)
|
|
events = run_probe(agent, monkeypatch, status=200)
|
|
assert events == []
|
|
|
|
|
|
def test_unreachable_reemits_service_unhealthy_every_cycle(agent, monkeypatch):
|
|
"""service_unhealthy is a real fault signal — must stay unconditional."""
|
|
run_probe(agent, monkeypatch, status=200)
|
|
events = run_probe(agent, monkeypatch, raise_exc=OSError("connection refused"))
|
|
assert events[0][0] == "service_unhealthy"
|
|
events = run_probe(agent, monkeypatch, raise_exc=OSError("connection refused"))
|
|
assert events[0][0] == "service_unhealthy"
|
|
|
|
|
|
def test_recovery_after_unreachable_reemits_service_healthy(agent, monkeypatch):
|
|
run_probe(agent, monkeypatch, raise_exc=OSError("connection refused"))
|
|
events = run_probe(agent, monkeypatch, status=200)
|
|
assert len(events) == 1
|
|
assert events[0][0] == "service_healthy"
|