Files
FabledSteward/tests/plugins/docker/test_ingest_events.py
T
bvandeusen 578cc33cc0
CI / lint (push) Successful in 2s
CI / unit (push) Successful in 48s
CI / integration (push) Successful in 2m22s
CI / publish (push) Successful in 1m8s
feat(docker): ingest swarm topology + lifecycle events + health/restart alerts
Wires the agent's enriched + swarm payloads through the docker.persist_host_
samples capability:

  * Swarm topology — persist sample["swarm"] into docker_swarm_services /
    docker_swarm_nodes (upsert + prune stale, host-scoped so two managers don't
    clobber). Migration docker_005 adds services.placement_json for the
    task→node placement the agent now reports.
  * Lifecycle events — _derive_events (pure, unit-tested) diffs the newest
    snapshot against stored per-container state: start / stop / die (non-zero
    exit) / oom / health_change → docker_events rows. Skipped on a host's first
    snapshot so the baseline doesn't emit a start per existing container.
  * Alerts — record restart_count (always) and is_healthy (1.0/0.0, only when a
    HEALTHCHECK exists) alongside cpu/mem, under host-scoped resource names;
    METRIC_CATALOG[docker] gains restart_count + is_healthy so they're alertable.

host_agent ingest captures the newest sample's swarm object and threads it to
the capability (now persist_host_docker(session, host, snapshots, swarm=None));
invoked when containers OR swarm are present, under the same SAVEPOINT. Unit
tests cover the event-diff matrix; integration tests cover event derivation
across two snapshots and swarm topology round-trip (incl. placement).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016Jg27rgypiW2efULXJDtMC
2026-06-18 21:07:40 -04:00

68 lines
2.8 KiB
Python

"""Unit tests for the docker plugin's lifecycle-event derivation.
_derive_events is a pure function (no DB), and ingest.py imports its models
lazily inside functions, so importing it here doesn't register ORM tables —
safe for the no-DB unit lane.
"""
from plugins.docker.ingest import _derive_events
def _c(name, status, **kw):
return {"name": name, "status": status, **kw}
def test_start_on_running_after_stopped():
old = {"web": {"status": "exited", "health": None, "oom_killed": False, "exit_code": 0}}
events = _derive_events(old, [_c("web", "running", image="nginx")])
assert events == [("web", "start", "nginx")]
def test_start_for_new_container_when_state_exists():
# old_state is non-empty (host already seen), a fresh container appears running.
old = {"other": {"status": "running", "health": None, "oom_killed": False, "exit_code": None}}
events = _derive_events(old, [
_c("other", "running"),
_c("new", "running", image="redis"),
])
assert ("new", "start", "redis") in events
# the unchanged 'other' produces nothing
assert all(e[0] != "other" for e in events)
def test_stop_on_clean_exit():
old = {"job": {"status": "running", "health": None, "oom_killed": False, "exit_code": None}}
events = _derive_events(old, [_c("job", "exited", exit_code=0)])
assert events == [("job", "stop", None)]
def test_die_on_nonzero_exit():
old = {"job": {"status": "running", "health": None, "oom_killed": False, "exit_code": None}}
events = _derive_events(old, [_c("job", "exited", exit_code=137)])
assert events == [("job", "die", "exit 137")]
def test_oom_on_flip_to_true():
old = {"hog": {"status": "running", "health": None, "oom_killed": False, "exit_code": None}}
# OOM-killed → also no longer running with a non-zero exit, so die + oom both fire.
events = _derive_events(old, [_c("hog", "exited", exit_code=137, oom_killed=True)])
assert ("hog", "oom", None) in events
assert ("hog", "die", "exit 137") in events
def test_health_change_emitted():
old = {"svc": {"status": "running", "health": "healthy", "oom_killed": False, "exit_code": None}}
events = _derive_events(old, [_c("svc", "running", health="unhealthy")])
assert events == [("svc", "health_change", "healthy→unhealthy")]
def test_removed_running_container_stops():
old = {"gone": {"status": "running", "health": None, "oom_killed": False, "exit_code": None}}
events = _derive_events(old, []) # vanished from the listing entirely
assert events == [("gone", "stop", "removed")]
def test_no_event_when_unchanged():
old = {"web": {"status": "running", "health": "healthy", "oom_killed": False, "exit_code": None}}
events = _derive_events(old, [_c("web", "running", health="healthy")])
assert events == []