From 493e3f2668361fcc60b4f0a4a5c80b93a156a53a Mon Sep 17 00:00:00 2001 From: DanConwayDev Date: Fri, 19 Jun 2026 10:57:09 +0000 Subject: [PATCH] fix(tests): stabilize relay readiness and history timestamps --- grasp-audit/src/client.rs | 4 +-- tests/common/relay.rs | 59 ++++++++++++++++++++++++++++++------ tests/replaceable_history.rs | 15 +++++++-- 3 files changed, 64 insertions(+), 14 deletions(-) diff --git a/grasp-audit/src/client.rs b/grasp-audit/src/client.rs index 434ae87..d841317 100644 --- a/grasp-audit/src/client.rs +++ b/grasp-audit/src/client.rs @@ -74,7 +74,7 @@ impl AuditClient { // Wait for connection to establish (with retries) let mut attempts = 0; let mut connected = false; - while attempts < 20 { + while attempts < 50 { tokio::time::sleep(Duration::from_millis(100)).await; let relays = client.relays().await; @@ -144,7 +144,7 @@ impl AuditClient { // Wait for connection to establish (with retries) let mut attempts = 0; let mut connected = false; - while attempts < 20 { + while attempts < 50 { tokio::time::sleep(Duration::from_millis(100)).await; let relays = client.relays().await; diff --git a/tests/common/relay.rs b/tests/common/relay.rs index b42d7a1..2f14d9c 100644 --- a/tests/common/relay.rs +++ b/tests/common/relay.rs @@ -16,19 +16,21 @@ use nostr_sdk::prelude::ToBech32; use std::path::PathBuf; use std::process::{Child, Command, Stdio}; use std::time::{Duration, Instant}; +use tokio::io::{AsyncReadExt, AsyncWriteExt}; use tokio::time::sleep; use crate::common::port::{self, PortReservation}; -/// How long to wait for the spawned ngit-grasp subprocess to accept TCP -/// connections before giving up on a single attempt. +/// How long to wait for the spawned ngit-grasp subprocess to handle HTTP +/// requests before giving up on a single attempt. const READY_TIMEOUT: Duration = Duration::from_secs(5); /// How often to retry the TCP probe while waiting for readiness. const READY_POLL: Duration = Duration::from_millis(100); -/// Extra grace after the listener accepts before declaring the relay -/// ready. Mirrors the existing fixture; without it the first WebSocket -/// upgrade can race the handler wiring. +/// Extra grace after the HTTP service responds before declaring the relay +/// ready. const READY_GRACE: Duration = Duration::from_millis(100); +/// Per-attempt timeout for the HTTP readiness probe once TCP connects. +const READY_PROBE_TIMEOUT: Duration = Duration::from_secs(1); /// How many fresh port reservations to attempt before giving up. The /// subprocess binds itself from `NGIT_BIND_ADDRESS`, so there is a /// microsecond-scale TOCTOU window between [`PortReservation::release`] @@ -554,10 +556,10 @@ impl TestRelay { } } - match tokio::net::TcpStream::connect(("127.0.0.1", self.port)).await { - Ok(_) => { - // Connection successful, relay is ready - // Give it a tiny bit more time to fully initialize + match self.probe_http_ready().await { + Ok(()) => { + // HTTP service handled a request successfully, so the + // accept loop, Hyper service, and relay wiring are ready. sleep(READY_GRACE).await; return ReadyOutcome::Ready; } @@ -574,6 +576,45 @@ impl TestRelay { } } + async fn probe_http_ready(&self) -> std::io::Result<()> { + let probe = async { + let mut stream = tokio::net::TcpStream::connect(("127.0.0.1", self.port)).await?; + let request = format!( + "GET / HTTP/1.1\r\nHost: 127.0.0.1:{}\r\nConnection: close\r\n\r\n", + self.port + ); + stream.write_all(request.as_bytes()).await?; + + let mut response = [0_u8; 64]; + let read = stream.read(&mut response).await?; + if read == 0 { + return Err(std::io::Error::new( + std::io::ErrorKind::UnexpectedEof, + "readiness probe got empty HTTP response", + )); + } + + let status = String::from_utf8_lossy(&response[..read]); + if status.starts_with("HTTP/1.1 200") || status.starts_with("HTTP/1.0 200") { + Ok(()) + } else { + Err(std::io::Error::new( + std::io::ErrorKind::InvalidData, + format!("readiness probe got non-200 response: {status:?}"), + )) + } + }; + + tokio::time::timeout(READY_PROBE_TIMEOUT, probe) + .await + .unwrap_or_else(|_| { + Err(std::io::Error::new( + std::io::ErrorKind::TimedOut, + "HTTP readiness probe timed out", + )) + }) + } + /// Stop the relay pub async fn stop(mut self) { // Kill the process (gracefully if possible) diff --git a/tests/replaceable_history.rs b/tests/replaceable_history.rs index f56c31c..bebe755 100644 --- a/tests/replaceable_history.rs +++ b/tests/replaceable_history.rs @@ -74,7 +74,11 @@ async fn newer_30618_supersedes_older_and_preserves_old_in_history() { .expect("create audit client"); let (_announcement, repo_id) = publish_served_repo(&client, "history-30618").await; - let base_ts = Timestamp::now(); + // The fixture published by `publish_served_repo` already saved a 30618 + // for this coordinate. Use a deterministic later timestamp so this test's + // "old" version is always a valid replacement instead of racing the + // fixture event within the same one-second Nostr timestamp bucket. + let base_ts = Timestamp::from_secs(Timestamp::now().as_secs() + 1); let old_state = build_state_version(&client, &repo_id, base_ts, "state-v1"); let new_state = build_state_version( &client, @@ -96,7 +100,10 @@ async fn newer_30618_supersedes_older_and_preserves_old_in_history() { let coordinate = format!("30618:{}:{}", client.public_key().to_hex(), repo_id); let history = open_history(relay.relay_data_path()).await; let records = history - .superseded_records_for_coordinate_before(&coordinate, Timestamp::now()) + .superseded_records_for_coordinate_before( + &coordinate, + Timestamp::from_secs(new_state.created_at.as_secs() + 1), + ) .await; assert!( @@ -244,7 +251,9 @@ async fn serving_behavior_unchanged_latest_version_is_still_served() { .expect("create audit client"); let (_announcement, repo_id) = publish_served_repo(&client, "history-serving").await; - let base_ts = Timestamp::now(); + // The helper publishes an initial 30618 for this coordinate. Ensure the + // state versions created here are strictly newer than that fixture event. + let base_ts = Timestamp::from_secs(Timestamp::now().as_secs() + 1); let old_state = build_state_version(&client, &repo_id, base_ts, "state-old"); let new_state = build_state_version( &client,