test: cover graceful-leave instant-evict and rejoin-after-eviction edges

Two reconnect-eviction edges were logically handled but had no explicit
assertion:

- A graceful ConnEvent::Left must emit PeerLeft *immediately* (not wait on
  the grace timer). New graceful_leave_evicts_immediately asserts the leave
  lands well inside the grace window.
- After a grace-timer eviction scrubs the peer from seen_connected, a later
  rejoin's initial dial must be a fresh first-dial, not a reconnect that
  arms a new eviction. New rejoin_after_grace_eviction_dials_cleanly covers
  this (distinct from the existing graceful-leave scrub path).

Also reorder the eviction task to scrub timers/seen_connected before
emitting PeerConnectionFailed, so an observer (or a redial racing the
eviction) always sees clean internal state — this also makes the new
rejoin-after-eviction test deterministic.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-01 15:46:15 -04:00
co-authored by Claude Opus 4.8
parent e3b63c0856
commit ead3031f3c
2 changed files with 68 additions and 1 deletions
+5 -1
View File
@@ -100,9 +100,13 @@ fn arm_grace_timer(
crate::log_msg(&format!("Reconnect grace expired; evicting peer {:?}", peer_id));
transport_evict.disconnect_peer(peer_id).await;
jitter_evict.lock().await.remove(&peer_id);
let _ = ui_evict.send(UiEvent::PeerConnectionFailed { id: peer_id }).await;
// Scrub our internal state *before* announcing the eviction, so anything
// that observes `PeerConnectionFailed` (or a rejoin racing it) sees a clean
// slate — a later dial for this identity is then a fresh first-dial, not a
// reconnect.
timers_evict.lock().unwrap().remove(&peer_id);
seen_evict.lock().unwrap().remove(&peer_id);
let _ = ui_evict.send(UiEvent::PeerConnectionFailed { id: peer_id }).await;
});
timers_guard.insert(peer_id, handle);
}