Files
ngit-grasp/tests/sync/reconnect_backoff.rs
DanConwayDev 113b456d7f test(sync): wait for complete reconnect recovery evidence
A delayed test task may observe more than the requested two reconnects.
Treat the fixture's minimum session count as the readiness precondition,
then wait for the complete relay-specific retained-failure-streak log line.

This preserves the backoff assertion without assuming when the test task
runs or accepting another relay's log as readiness. Production reconnect
behavior and deadlines are unchanged; unrelated fixture cleanup is excluded.

Validation: full workspace suite passed (3,566 passed, 16 ignored); both
focused reconnect tests passed after final import cleanup. Formatting and
whitespace checks passed. Independent review found no remaining findings.

Assisted-by: Codex (GPT-6)
2026-09-21 15:01:40 +00:00

109 lines
4.0 KiB
Rust

//! Reconnect failure history must survive short-lived successful handshakes.
use std::time::Duration;
use nostr_sdk::prelude::*;
use crate::common::flapping_relay::FlappingRelay;
use crate::common::{TestClient, TestRelay};
#[tokio::test]
async fn discovery_only_mailbox_keeps_failure_history_through_cleanup() {
use crate::common::{send_to_relay_url, setup_announcement_on_relay, MockRelay};
let index = MockRelay::start().await;
let flapping = FlappingRelay::start().await;
let owner = Keys::generate();
let relay_list = EventBuilder::new(Kind::RelayList, "")
.tags([Tag::custom("r", vec![flapping.url(), "read"])])
.finalize(&owner)
.unwrap();
send_to_relay_url(index.url(), &relay_list).await.unwrap();
let syncing = TestRelay::start_with_sync(Some(index.url().to_string())).await;
let domain = syncing.domain();
let (_announcement, _git) =
setup_announcement_on_relay(&syncing, &owner, &[&domain], "discovery-mailbox-backoff")
.await;
// Owner inbox discovery has no ordinary repository/root live target.
// The two-second cleanup pass previously erased its first failure before
// the next handshake, so it could never reach this recovery state.
flapping
.wait_for_connections(2, Duration::from_secs(60))
.await;
wait_for_retained_failure_streak(&syncing, flapping.url(), Duration::from_secs(15)).await;
syncing.stop().await;
flapping.stop().await;
index.stop().await;
}
async fn wait_for_retained_failure_streak(relay: &TestRelay, url: &str, timeout: Duration) {
tokio::time::timeout(timeout, async {
loop {
let contents = std::fs::read_to_string(relay.log_path()).unwrap_or_default();
if contents.lines().any(|line| {
line.contains(url)
&& line.contains("consecutive_failures=1")
&& line.contains("preserving failure streak until stable")
}) {
return;
}
tokio::task::yield_now().await;
}
})
.await
.unwrap_or_else(|_| panic!("relay {url} never reconnected with its first failure retained"));
}
#[tokio::test]
async fn flapping_relay_handshakes_do_not_reset_exponential_backoff() {
let flapping = FlappingRelay::start().await;
let syncing = TestRelay::start_with_sync(None).await;
let keys = Keys::generate();
let identifier = "flapping-reconnect-backoff";
let npub = keys.public_key().to_bech32().expect("npub");
let announcement = EventBuilder::new(Kind::GitRepoAnnouncement, "")
.tags(vec![
Tag::identifier(identifier),
Tag::custom(
"clone",
vec![format!(
"http://{}/{npub}/{identifier}.git",
syncing.domain()
)],
),
Tag::custom(
"relays",
vec![
format!("ws://{}", syncing.domain()),
flapping.url().to_string(),
],
),
])
.finalize(&keys)
.expect("sign announcement");
let client = TestClient::new(syncing.url(), keys)
.await
.expect("connect publishing client");
client
.send_event(&announcement)
.await
.expect("publish flapping relay announcement");
// The recovered session must retain the first failure instead of treating
// its WebSocket handshake as proof of stability. Each session reaches a
// normal REQ before the fixture drops it. Unit coverage below the actor
// boundary verifies that the retained count drives the next backoff step.
// The fixture reports all sessions observed so far. A delayed test task
// may resume after further reconnects; only the minimum is a precondition.
flapping
.wait_for_connections(2, Duration::from_secs(25))
.await;
wait_for_retained_failure_streak(&syncing, flapping.url(), Duration::from_secs(20)).await;
client.disconnect().await;
syncing.stop().await;
flapping.stop().await;
}