From 625dfa71c21bbe2973f93f81c80e0304fb7ff52c Mon Sep 17 00:00:00 2001 From: DanConwayDev Date: Fri, 18 Sep 2026 14:29:28 +0000 Subject: [PATCH] test(nip09): wait on served state through the recovery suite The recovery suite paused 300-900 ms after each deletion, re-announcement and Git push before inspecting served events, holding records and the restored repository. Deletions and archive recovery run inside the write policy before the relay replies `OK`; promotion after a Git push completes in a spawned task. Drop the pauses that followed an awaited send whose reply already covers the assertion, and wait on the served state with `wait_until_served` and `wait_until_not_served` where a deletion, recovery or push changes it. The restored-repository check waits on the directory itself. The assertions that follow are unchanged. Validation: measured against master with the same binaries, 20 unloaded runs and 30 samples as three concurrent instances under CPU spinners; see the pull request description. Assisted-by: Claude Fable 5.1 Co-Authored-By: Claude Fable 5.1 --- tests/lifecycle/nip09_recovery.rs | 62 ++++++++++++++++++------------- 1 file changed, 37 insertions(+), 25 deletions(-) diff --git a/tests/lifecycle/nip09_recovery.rs b/tests/lifecycle/nip09_recovery.rs index 6e6266e..1f73ee9 100644 --- a/tests/lifecycle/nip09_recovery.rs +++ b/tests/lifecycle/nip09_recovery.rs @@ -8,7 +8,8 @@ use common::{ announcement_coordinate, build_deletion, create_pr_event, create_state_event, create_test_repo_with_commit, publish_served_announcement_for_identifier, publish_served_repo, publish_served_repo_with_maintainers, publish_served_repo_with_state_event, push_to_relay, - CommitVariant, TestRelay, + wait_for, wait_until_not_served, wait_until_served, CommitVariant, TestRelay, + OBSERVABLE_EFFECT_TIMEOUT, }; use grasp_audit::git_command; use grasp_audit::{AuditClient, AuditConfig}; @@ -127,7 +128,8 @@ async fn publish_served_repo_for_identifier( .expect("send served state event"); push_to_relay(temp_dir.path(), &relay_domain, &npub, repo_id).expect("push served repo data"); - tokio::time::sleep(Duration::from_millis(900)).await; + wait_until_served(&client, announcement.id).await; + wait_until_served(&client, state_event.id).await; assert!( client @@ -203,7 +205,8 @@ async fn publish_served_repo_for_identifier_with_maintainers( push_to_relay(temp_dir.path(), &relay_domain, &npub, repo_id) .expect("push served repo data with maintainers"); - tokio::time::sleep(Duration::from_millis(900)).await; + wait_until_served(&client, announcement.id).await; + wait_until_served(&client, state_event.id).await; assert!( client @@ -346,7 +349,7 @@ async fn setup_parked_announcement_with_failed_ingest_recovery( .send_event(deletion.clone()) .await .expect("delete old state by coordinate"); - tokio::time::sleep(Duration::from_millis(500)).await; + wait_until_not_served(&client, old_state_event.id).await; } let deletion = build_deletion(&client, &[], std::slice::from_ref(&announcement_coord)); @@ -354,7 +357,7 @@ async fn setup_parked_announcement_with_failed_ingest_recovery( .send_event(deletion.clone()) .await .expect("delete announcement coordinate"); - tokio::time::sleep(Duration::from_millis(800)).await; + wait_until_not_served(&client, announcement.id).await; let store = open_holding(&relay).await; let archive_rel = store @@ -378,7 +381,6 @@ async fn setup_parked_announcement_with_failed_ingest_recovery( .send_event(reannouncement.clone()) .await .expect("send re-announcement expected to park in purgatory"); - tokio::time::sleep(Duration::from_millis(700)).await; assert!( !client @@ -454,7 +456,7 @@ async fn submit_new_state_and_push_for_promotion( push_to_relay(temp_dir.path(), &relay_domain, &npub, &fixture.repo_id) .unwrap_or_else(|e| panic!("push {state_label} state commit for promotion: {e}")); - tokio::time::sleep(Duration::from_millis(900)).await; + wait_until_served(&fixture.client, state_event.id).await; state_event } @@ -562,7 +564,8 @@ async fn purgatory_promotion_tombstoned_pr_events_do_not_restore_pr_refs() { )) .await .expect("submit PR tombstone deletion request before promotion"); - tokio::time::sleep(Duration::from_millis(300)).await; + wait_until_not_served(&fixture.client, fixture.pr.id).await; + wait_until_not_served(&fixture.client, fixture.pr_update.id).await; std::fs::write(&fixture.archive_path, &fixture.archive_backup) .expect("restore archive bytes before PR tombstone promotion"); @@ -636,7 +639,6 @@ async fn repeated_purgatory_promotion_attempts_remain_idempotent() { .send_event(second_reannouncement.clone()) .await .expect("send second re-announcement after promotion"); - tokio::time::sleep(Duration::from_millis(500)).await; assert!(fixture .client @@ -976,7 +978,8 @@ async fn reannouncement_restores_events_and_git_archive_happy_path() { .send_event(deletion.clone()) .await .expect("send deletion"); - tokio::time::sleep(Duration::from_millis(700)).await; + wait_until_not_served(&client, issue.id).await; + wait_until_not_served(&client, state_event.id).await; assert!(!client .is_event_on_relay(issue.id) @@ -1000,7 +1003,8 @@ async fn reannouncement_restores_events_and_git_archive_happy_path() { .send_event(reannounce.clone()) .await .expect("send re-announcement"); - tokio::time::sleep(Duration::from_millis(800)).await; + wait_until_served(&client, reannounce.id).await; + wait_until_served(&client, issue.id).await; assert!(client .is_event_on_relay(reannounce.id) @@ -1042,7 +1046,6 @@ async fn replacement_announcement_delisting_this_relay_triggers_runtime_deletion .create_issue(&announcement, "runtime delist issue", "body", vec![]) .expect("build issue"); client.send_event(issue.clone()).await.expect("send issue"); - tokio::time::sleep(Duration::from_millis(600)).await; assert!(client .is_event_on_relay(issue.id) @@ -1072,7 +1075,8 @@ async fn replacement_announcement_delisting_this_relay_triggers_runtime_deletion rejection.is_err(), "replacement delist announcement should be rejected" ); - tokio::time::sleep(Duration::from_millis(900)).await; + wait_until_not_served(&client, announcement.id).await; + wait_until_not_served(&client, state_event.id).await; assert!( !client @@ -1133,7 +1137,7 @@ async fn out_of_retention_reannouncement_behaves_as_fresh_new_repo() { .send_event(deletion.clone()) .await .expect("send deletion"); - tokio::time::sleep(Duration::from_millis(700)).await; + wait_until_not_served(&client, announcement.id).await; let store = open_holding(&relay).await; store @@ -1149,7 +1153,6 @@ async fn out_of_retention_reannouncement_behaves_as_fresh_new_repo() { .send_event(reannounce.clone()) .await .expect("send re-announcement"); - tokio::time::sleep(Duration::from_millis(500)).await; assert!( !client @@ -1190,7 +1193,7 @@ async fn partial_recovery_missing_archive_restores_events_only_deterministically .send_event(deletion.clone()) .await .expect("send deletion"); - tokio::time::sleep(Duration::from_millis(700)).await; + wait_until_not_served(&client, announcement.id).await; let store = open_holding(&relay).await; let archive_rel = store @@ -1208,7 +1211,9 @@ async fn partial_recovery_missing_archive_restores_events_only_deterministically .send_event(reannounce.clone()) .await .expect("send re-announcement"); - tokio::time::sleep(Duration::from_millis(800)).await; + wait_until_served(&client, reannounce.id).await; + wait_until_served(&client, issue.id).await; + wait_until_served(&client, state_event.id).await; assert!(client .is_event_on_relay(reannounce.id) @@ -1248,7 +1253,7 @@ async fn partial_recovery_missing_holding_payload_restores_git_only_deterministi .send_event(deletion.clone()) .await .expect("send deletion"); - tokio::time::sleep(Duration::from_millis(700)).await; + wait_until_not_served(&client, announcement.id).await; let store = open_holding(&relay).await; store @@ -1269,7 +1274,12 @@ async fn partial_recovery_missing_holding_payload_restores_git_only_deterministi .send_event(reannounce.clone()) .await .expect("send re-announcement"); - tokio::time::sleep(Duration::from_millis(800)).await; + wait_for( + "git repository restored from archive", + OBSERVABLE_EFFECT_TIMEOUT, + || async { repo_path.is_dir() }, + ) + .await; assert!( repo_path.is_dir(), @@ -1305,21 +1315,24 @@ async fn repeated_reannouncement_recovery_trigger_is_idempotent() { .send_event(deletion.clone()) .await .expect("send deletion"); - tokio::time::sleep(Duration::from_millis(700)).await; + wait_until_not_served(&client, announcement.id).await; + wait_until_not_served(&client, issue.id).await; let first = build_reannouncement(&client, &relay, &repo_id, deletion.created_at); client .send_event(first.clone()) .await .expect("first re-announcement"); - tokio::time::sleep(Duration::from_millis(700)).await; + wait_until_served(&client, first.id).await; + wait_until_served(&client, issue.id).await; let second = build_reannouncement(&client, &relay, &repo_id, first.created_at); client .send_event(second.clone()) .await .expect("second re-announcement"); - tokio::time::sleep(Duration::from_millis(700)).await; + wait_until_served(&client, second.id).await; + wait_until_served(&client, issue.id).await; assert!(client .is_event_on_relay(issue.id) @@ -1392,7 +1405,7 @@ async fn multi_maintainer_reannouncement_restores_deleted_owner_slice() { .send_event(deletion.clone()) .await .expect("delete maintainer a announcement"); - tokio::time::sleep(Duration::from_millis(800)).await; + wait_until_not_served(&client_a, deleted_patch.id).await; assert!(!client_a .is_event_on_relay(deleted_patch.id) @@ -1412,7 +1425,7 @@ async fn multi_maintainer_reannouncement_restores_deleted_owner_slice() { .send_event(reannounce_a) .await .expect("reannounce maintainer a"); - tokio::time::sleep(Duration::from_millis(800)).await; + wait_until_served(&client_a, deleted_patch.id).await; assert!(client_a .is_event_on_relay(deleted_patch.id) @@ -1444,7 +1457,6 @@ async fn disrespector_mode_has_no_recovery_state_to_apply() { .send_event(deletion.clone()) .await .expect("send deletion request in disrespector mode"); - tokio::time::sleep(Duration::from_millis(500)).await; let reannounce = build_reannouncement(&client, &relay, &repo_id, deletion.created_at); client