fix(tests): stabilize relay readiness and history timestamps

This commit is contained in:
DanConwayDev
2026-06-19 10:57:09 +00:00
parent 4f9f8934b8
commit 493e3f2668
3 changed files with 64 additions and 14 deletions
+2 -2
View File
@@ -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;
+50 -9
View File
@@ -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)
+12 -3
View File
@@ -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,