homelab-codex-ws/services/control-plane/tests
oskar 71a7af5b3f fix(control-plane): unwedge incidents that never get service_healthy
_resolve_incident() only ever fires from process_event() on a
service_healthy/service_recovered event. A service that is removed,
renamed, or was only ever a one-off test never emits that event again,
so its incident stays "active" in world/incidents.json forever — this
is what left 5 incidents wedged on VPS until a manual on-node edit
during the 2026-08-26 recon session (docs/sessions/2026-08-26.md).

Two independent unwedging mechanisms, both in observer._prune_stale_world
(runs every cycle, so no new event is required to trigger either):

(a) Time-based fallback: any active incident with last_occurrence older
    than INCIDENT_STALE_RESOLVE_SECS (env, default 24h) auto-resolves
    with resolved_reason="auto_stale_no_events_24h". Unlike the existing
    orphan case (Case 2, 5-min guard, only unlinked incidents), this
    also clears a service's lingering incident_id link — that link is
    exactly what a decommissioned service's incident never gets a
    chance to clear via the normal event path.

(b) Manual path: an operator touches
    world/resolve-requests/<incident-id>; the observer consumes the
    flag file each cycle, force-resolves with resolved_reason=
    "manual_operator", and always removes the flag (even for an
    unknown/already-resolved id) so a mistyped flag can't sit forever
    looking unprocessed.

    Chose a flag file over adding a mutation endpoint to operator_ui.py:
    /action/mutate only knows actions/<status>/<id>.json, there is no
    incidents equivalent, and world/incidents.json is exclusively
    observer-owned (rewritten wholesale every cycle by _save_world) —
    a second writer (the HTTP handler thread) would race the observer's
    own writes. A flag file needs no new HTTP surface and reuses the
    same "operator drops a file, the owning process consumes it"
    pattern the actions pending/approved queue already uses. Smaller
    diff, no new attack surface on a server with no auth on writes.

Tests added to test_incident_lifecycle.py: stale-resolve past the
threshold (service still linked), negative case (fresh active incident
stays active), configurable threshold, manual-flag resolve + flag
removal, flag for an unknown incident, flag for an already-resolved
incident. Full control-plane suite: 179 passed.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017WDKj5LRY8vdQMx57dfNnu
2026-08-26 21:05:38 +02:00
..
__init__.py feat(control-plane): route ha-diag-agent events through supervisor 2026-05-29 15:59:23 +02:00
test_dormant_nodes.py feat(supervisor): duty-cycle nodes — liveness transitions logged, not actioned 2026-08-05 13:48:29 +02:00
test_executor_dispatch.py fix(dispatch): inbox 0o775 + rsync rc=23 przestaje byc cichy 2026-08-06 13:42:47 +02:00
test_executor_redeploy.py fix(control-plane): redeploy wykonywalny — dispatch do host-side deploy-runnera 2026-08-03 18:26:30 +02:00
test_incident_lifecycle.py fix(control-plane): unwedge incidents that never get service_healthy 2026-08-26 21:05:38 +02:00
test_liveness.py feat(observer): 3-state node liveness (fresh/stale/dead) + transitions + read-time net 2026-06-17 20:07:25 +02:00
test_observer_container_events.py fix(supervisor): route healthcheck_failed to container_restart 2026-07-29 19:24:01 +02:00
test_observer_shadow_liveness.py feat(observer): persist SHADOW_LIVENESS_MISMATCH to mounted file — survives container recreate (cutover evidence) 2026-07-15 15:30:44 +02:00
test_operator_ui_liveness.py feat(observer): 3-state node liveness (fresh/stale/dead) + transitions + read-time net 2026-06-17 20:07:25 +02:00
test_state_reliability.py test(control-plane): atomic write and resilient loader coverage 2026-06-03 12:27:05 +02:00
test_supervisor_duty_cycle.py feat(supervisor): duty-cycle nodes — liveness transitions logged, not actioned 2026-08-05 13:48:29 +02:00
test_supervisor_ha.py feat(observer): 3-state node liveness (fresh/stale/dead) + transitions + read-time net 2026-06-17 20:07:25 +02:00
test_supervisor_loop_resilience.py fix(supervisor): odpornosc petli na zawieszenie + timeout blokujacych wywolan — petla stanela cicho po ha_websocket_dead (mozg martwy 24h, healthy ale nie tika) 2026-07-16 16:17:20 +02:00
test_supervisor_node_events.py feat(observer): 3-state node liveness (fresh/stale/dead) + transitions + read-time net 2026-06-17 20:07:25 +02:00