mirror of
https://github.com/jmcorgan/fips.git
synced 2026-08-11 17:17:54 +00:00
Pin discovery state machine: open-discovery sweep + per-attempt lookup timeout
Cover two adjacent runtime behaviors in the discovery state machine
that were previously unpinned at the test level.
1. Open-discovery startup sweep iterate-filter-queue contract.
Cover the runtime sweep behavior: iterate advert cache, apply
skip-filters (own-pubkey, already-connected peers), queue eligible
entries to retry_pending. The config layer was tested but the sweep's
own filtering logic was unpinned.
src/discovery/nostr/runtime.rs: add #[cfg(test)] impl block with
three pub(crate) helpers — new_for_test() builds a minimal
NostrDiscovery with empty cache and no relays/background tasks (uses
fresh nostr::Keys signer + Client::builder().autoconnect(false));
cached_advert_for_test() wraps an OverlayEndpointAdvert into a
CachedOverlayAdvert valid for 1h; insert_advert_for_test() writes
direct to the advert_cache RwLock. All three vanish from release
builds via cfg-gating.
src/node/lifecycle.rs: visibility-only widen on
run_open_discovery_sweep from private async fn to
pub(in crate::node) async fn so the in-tree test can drive it
directly. Same pattern as already-pub(in crate::node) handlers in
src/node/handlers/.
src/node/tests/discovery.rs: add #[tokio::test]
test_open_discovery_sweep_queues_eligible_skips_filtered. Builds
Node + Arc<NostrDiscovery>, injects 3 adverts (eligible, already-
connected peer, own-pubkey), invokes the sweep, asserts retry_pending
contains exactly the eligible entry with matching peer_config npub
and the two filtered entries do NOT appear.
2. Per-attempt timeout state machine in check_pending_lookups.
Cover the central new behavior of f16b837: the [1, 2, 4, 8] retry
sequence (cumulative deadlines 1100/3100/7100/15100ms), one fresh
LookupRequest per attempt, and final-timeout reaching the unreachable
state. The opt-in DiscoveryBackoff machinery was well-tested but inert
at default config; this pins the state machine that runs by default.
Add test_check_pending_lookups_default_sequence_unreachable to
src/node/tests/discovery.rs. Constructs a Node with a peer that has
the target in its bloom but cannot respond (no Noise session — the
state-machine bookkeeping is independent of wire-send success).
Drives check_pending_lookups deterministically through:
t=1100 → second attempt; entry.attempt advances; req_initiated++
t=3100 → third attempt; entry.attempt advances; req_initiated++
t=7100 → fourth attempt; entry.attempt advances; req_initiated++
t=15099 → no-op (one ms before final deadline)
t=15100 → final timeout
At t=15100 asserts: pending_lookups[target] removed; resp_timed_out
counter +1 (this is the actual counter name); pending_tun_packets
[target] removed (queued packet dropped); a frame on the TUN sender
with IPv6 + next_header=58 + ICMPv6 type=1 (Destination Unreachable).
Fresh-request_id-per-attempt is structurally guaranteed: LookupRequest
::generate() unconditionally calls rand::random::<u64>(), and the
test asserts req_initiated increments by exactly 1 per retry (proving
initiate_lookup runs fresh each time, not a resend of cached state).
The originator's request_id isn't stored on the originator side
(deliberately omitted from recent_requests so the response is
recognized as "ours"), so direct request_id capture is not feasible
and counter-tick is the load-bearing observable.
No production logic touched; no visibility widening needed
(check_pending_lookups was already pub(in crate::node)).
This commit is contained in:
@@ -1100,3 +1100,69 @@ impl NostrDiscovery {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
impl NostrDiscovery {
|
||||
/// Build a minimal `NostrDiscovery` for unit tests. No relay client is
|
||||
/// connected and no background tasks are spawned; only the in-memory
|
||||
/// `advert_cache` and `npub` are usable. Intended for cache-injection
|
||||
/// tests of consumers (e.g. `Node::run_open_discovery_sweep`).
|
||||
pub(crate) fn new_for_test() -> Self {
|
||||
let keys = nostr::Keys::generate();
|
||||
let pubkey = keys.public_key();
|
||||
let npub = pubkey.to_bech32().expect("bech32 encode");
|
||||
let client = Client::builder()
|
||||
.signer(keys.clone())
|
||||
.opts(ClientOptions::new().autoconnect(false))
|
||||
.build();
|
||||
let config = NostrDiscoveryConfig::default();
|
||||
let offer_slots = Arc::new(Semaphore::new(config.max_concurrent_incoming_offers));
|
||||
let (event_tx, event_rx) = mpsc::unbounded_channel();
|
||||
Self {
|
||||
client,
|
||||
keys,
|
||||
pubkey,
|
||||
npub,
|
||||
config,
|
||||
advert_cache: RwLock::new(HashMap::new()),
|
||||
local_advert: RwLock::new(None),
|
||||
current_advert_event_id: RwLock::new(None),
|
||||
pending_answers: Mutex::new(HashMap::new()),
|
||||
active_initiators: Mutex::new(HashSet::new()),
|
||||
seen_sessions: Mutex::new(HashMap::new()),
|
||||
offer_slots,
|
||||
event_tx,
|
||||
event_rx: Mutex::new(event_rx),
|
||||
notify_task: Mutex::new(None),
|
||||
advertise_task: Mutex::new(None),
|
||||
}
|
||||
}
|
||||
|
||||
/// Build a `CachedOverlayAdvert` for tests with a single endpoint and
|
||||
/// a generous validity window (one hour from `now_ms()`).
|
||||
pub(crate) fn cached_advert_for_test(
|
||||
author_npub: String,
|
||||
endpoint: OverlayEndpointAdvert,
|
||||
created_at_secs: u64,
|
||||
) -> CachedOverlayAdvert {
|
||||
CachedOverlayAdvert {
|
||||
author_npub: author_npub.clone(),
|
||||
advert: OverlayAdvert {
|
||||
identifier: ADVERT_IDENTIFIER.to_string(),
|
||||
version: ADVERT_VERSION,
|
||||
endpoints: vec![endpoint],
|
||||
signal_relays: None,
|
||||
stun_servers: None,
|
||||
},
|
||||
created_at: created_at_secs,
|
||||
valid_until_ms: now_ms().saturating_add(3_600_000),
|
||||
}
|
||||
}
|
||||
|
||||
/// Insert a cached advert directly into the in-memory cache. Used by
|
||||
/// unit tests to set up consumer-side state without needing live relays.
|
||||
pub(crate) async fn insert_advert_for_test(&self, npub: String, advert: CachedOverlayAdvert) {
|
||||
let mut cache = self.advert_cache.write().await;
|
||||
cache.insert(npub, advert);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user