upleb.uk

Public git repos — served from a NIP-34 GRASP relay at git.upleb.uk

summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--grasp-audit/src/bin/grasp-audit.rs13
-rw-r--r--grasp-audit/src/fixtures.rs16
-rw-r--r--grasp-audit/src/probe.rs190
-rw-r--r--grasp-audit/src/specs/grasp01/purgatory.rs4
-rw-r--r--src/audit_cleanup.rs24
-rw-r--r--src/git/handlers.rs66
-rw-r--r--src/git/sync.rs2
-rw-r--r--src/http/mod.rs1
-rw-r--r--src/nostr/builder.rs1
-rw-r--r--src/nostr/policy/announcement.rs18
-rw-r--r--src/nostr/policy/deletion.rs45
-rw-r--r--src/nostr/policy/pr_event.rs10
-rw-r--r--src/nostr/policy/state.rs6
-rw-r--r--src/purgatory/mod.rs51
-rw-r--r--src/sync/mod.rs3
-rw-r--r--tests/common/sync_helpers.rs7
-rw-r--r--tests/purgatory_persistence.rs10
-rw-r--r--tests/purgatory_sync.rs15
-rw-r--r--tests/sync/discovery.rs6
-rw-r--r--tests/sync/maintainer_reprocessing.rs25
-rw-r--r--tests/sync/metrics.rs19
21 files changed, 326 insertions, 206 deletions
diff --git a/grasp-audit/src/bin/grasp-audit.rs b/grasp-audit/src/bin/grasp-audit.rs
index 305e5eb..ab835e7 100644
--- a/grasp-audit/src/bin/grasp-audit.rs
+++ b/grasp-audit/src/bin/grasp-audit.rs
@@ -132,7 +132,11 @@ async fn main() -> Result<()> {
132 println!("\n[Run {}]", run); 132 println!("\n[Run {}]", run);
133 } 133 }
134 let report = grasp_audit::probe::run_probe( 134 let report = grasp_audit::probe::run_probe(
135 &relay, keys.clone(), read_only, timeout, overall_secs, 135 &relay,
136 keys.clone(),
137 read_only,
138 timeout,
139 overall_secs,
136 ) 140 )
137 .await; 141 .await;
138 if json { 142 if json {
@@ -144,10 +148,9 @@ async fn main() -> Result<()> {
144 tokio::time::sleep(Duration::from_secs(interval)).await; 148 tokio::time::sleep(Duration::from_secs(interval)).await;
145 } 149 }
146 } else { 150 } else {
147 let report = grasp_audit::probe::run_probe( 151 let report =
148 &relay, keys, read_only, timeout, overall_secs, 152 grasp_audit::probe::run_probe(&relay, keys, read_only, timeout, overall_secs)
149 ) 153 .await;
150 .await;
151 if json { 154 if json {
152 report.print_json(); 155 report.print_json();
153 } else { 156 } else {
diff --git a/grasp-audit/src/fixtures.rs b/grasp-audit/src/fixtures.rs
index 4678790..d09c36b 100644
--- a/grasp-audit/src/fixtures.rs
+++ b/grasp-audit/src/fixtures.rs
@@ -967,7 +967,9 @@ impl<'a> TestContext<'a> {
967 FixtureKind::PREvent2Served => self.build_pr_event_2_served().await, 967 FixtureKind::PREvent2Served => self.build_pr_event_2_served().await,
968 968
969 FixtureKind::PurgatoryValidRepoSent => self.build_purgatory_valid_repo_sent().await, 969 FixtureKind::PurgatoryValidRepoSent => self.build_purgatory_valid_repo_sent().await,
970 FixtureKind::PurgatoryOwnerStateDataPushed => self.build_purgatory_owner_state_data_pushed().await, 970 FixtureKind::PurgatoryOwnerStateDataPushed => {
971 self.build_purgatory_owner_state_data_pushed().await
972 }
971 973
972 FixtureKind::OwnerStateDataPushed => self.build_owner_state_data_pushed().await, 974 FixtureKind::OwnerStateDataPushed => self.build_owner_state_data_pushed().await,
973 975
@@ -1147,7 +1149,10 @@ impl<'a> TestContext<'a> {
1147 Ok(h) => h, 1149 Ok(h) => h,
1148 Err(e) => { 1150 Err(e) => {
1149 cleanup(&clone_path); 1151 cleanup(&clone_path);
1150 return Err(anyhow::anyhow!("Failed to create deterministic commit: {}", e)); 1152 return Err(anyhow::anyhow!(
1153 "Failed to create deterministic commit: {}",
1154 e
1155 ));
1151 } 1156 }
1152 }; 1157 };
1153 1158
@@ -1186,7 +1191,12 @@ impl<'a> TestContext<'a> {
1186 DETERMINISTIC_COMMIT_HASH 1191 DETERMINISTIC_COMMIT_HASH
1187 )); 1192 ));
1188 } 1193 }
1189 Err(e) => return Err(anyhow::anyhow!("PurgatoryOwnerStateDataPushed push error: {}", e)), 1194 Err(e) => {
1195 return Err(anyhow::anyhow!(
1196 "PurgatoryOwnerStateDataPushed push error: {}",
1197 e
1198 ))
1199 }
1190 } 1200 }
1191 1201
1192 // ============================================================ 1202 // ============================================================
diff --git a/grasp-audit/src/probe.rs b/grasp-audit/src/probe.rs
index ecbbcc9..8dad1d4 100644
--- a/grasp-audit/src/probe.rs
+++ b/grasp-audit/src/probe.rs
@@ -167,10 +167,7 @@ fn now_iso8601() -> String {
167 let mo = if mp < 10 { mp + 3 } else { mp - 9 }; // month [1, 12] 167 let mo = if mp < 10 { mp + 3 } else { mp - 9 }; // month [1, 12]
168 let yr = if mo <= 2 { y + 1 } else { y }; 168 let yr = if mo <= 2 { y + 1 } else { y };
169 169
170 format!( 170 format!("{:04}-{:02}-{:02}T{:02}:{:02}:{:02}Z", yr, mo, d, h, m, s)
171 "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}Z",
172 yr, mo, d, h, m, s
173 )
174} 171}
175 172
176// ============================================================ 173// ============================================================
@@ -246,8 +243,7 @@ pub async fn run_probe(
246 error: Some(error_msg), 243 error: Some(error_msg),
247 }); 244 });
248 // Skip all subsequent checks 245 // Skip all subsequent checks
249 let already: std::collections::HashSet<&str> = 246 let already: std::collections::HashSet<&str> = $checks.iter().map(|c| c.name).collect();
250 $checks.iter().map(|c| c.name).collect();
251 for name in ALL_CHECK_NAMES { 247 for name in ALL_CHECK_NAMES {
252 if !already.contains(name) { 248 if !already.contains(name) {
253 $checks.push(skipped(name, "overall timeout")); 249 $checks.push(skipped(name, "overall timeout"));
@@ -303,8 +299,8 @@ pub async fn run_probe(
303 let clone_url = format!("{}/{}/{}.git", http_base, npub, repo_id); 299 let clone_url = format!("{}/{}/{}.git", http_base, npub, repo_id);
304 300
305 // Create temp dir for local repo 301 // Create temp dir for local repo
306 let local_repo_path = std::env::temp_dir() 302 let local_repo_path =
307 .join(format!("grasp-probe-{}", uuid::Uuid::new_v4())); 303 std::env::temp_dir().join(format!("grasp-probe-{}", uuid::Uuid::new_v4()));
308 304
309 // Initialise local repo (offline) 305 // Initialise local repo (offline)
310 let init_result = init_local_repo(&local_repo_path, &clone_url); 306 let init_result = init_local_repo(&local_repo_path, &clone_url);
@@ -368,7 +364,14 @@ pub async fn run_probe(
368 // Step 1: connect_websocket 364 // Step 1: connect_websocket
369 // ============================================================ 365 // ============================================================
370 if Instant::now() >= deadline { 366 if Instant::now() >= deadline {
371 deadline_return!(relay_url, timestamp, total_start, overall_secs, checks, "connect_websocket"); 367 deadline_return!(
368 relay_url,
369 timestamp,
370 total_start,
371 overall_secs,
372 checks,
373 "connect_websocket"
374 );
372 } 375 }
373 let step1_start = Instant::now(); 376 let step1_start = Instant::now();
374 let client_result = tokio::time::timeout( 377 let client_result = tokio::time::timeout(
@@ -426,12 +429,21 @@ pub async fn run_probe(
426 // ============================================================ 429 // ============================================================
427 { 430 {
428 if Instant::now() >= deadline { 431 if Instant::now() >= deadline {
429 deadline_return!(relay_url, timestamp, total_start, overall_secs, checks, "nip11_fetch"); 432 deadline_return!(
433 relay_url,
434 timestamp,
435 total_start,
436 overall_secs,
437 checks,
438 "nip11_fetch"
439 );
430 } 440 }
431 let step2_start = Instant::now(); 441 let step2_start = Instant::now();
432 let http_client = reqwest::Client::new(); 442 let http_client = reqwest::Client::new();
433 let nip11_result = tokio::time::timeout( 443 let nip11_result = tokio::time::timeout(
434 deadline.saturating_duration_since(Instant::now()).min(Duration::from_secs(timeout_secs)), 444 deadline
445 .saturating_duration_since(Instant::now())
446 .min(Duration::from_secs(timeout_secs)),
435 http_client 447 http_client
436 .get(&http_base) 448 .get(&http_base)
437 .header("Accept", "application/nostr+json") 449 .header("Accept", "application/nostr+json")
@@ -443,24 +455,20 @@ pub async fn run_probe(
443 455
444 match nip11_result { 456 match nip11_result {
445 Ok(Ok(resp)) if resp.status().is_success() => { 457 Ok(Ok(resp)) if resp.status().is_success() => {
446 let detail = resp 458 let detail = resp.json::<serde_json::Value>().await.ok().map(|v| {
447 .json::<serde_json::Value>() 459 let name = v.get("name").and_then(|n| n.as_str()).unwrap_or("unknown");
448 .await 460 // software is typically a repo URL; take the last path segment
449 .ok() 461 let software = v
450 .map(|v| { 462 .get("software")
451 let name = v.get("name").and_then(|n| n.as_str()).unwrap_or("unknown"); 463 .and_then(|s| s.as_str())
452 // software is typically a repo URL; take the last path segment 464 .map(|s| s.trim_end_matches('/').rsplit('/').next().unwrap_or(s))
453 let software = v 465 .unwrap_or("unknown");
454 .get("software") 466 let version = v
455 .and_then(|s| s.as_str()) 467 .get("version")
456 .map(|s| s.trim_end_matches('/').rsplit('/').next().unwrap_or(s)) 468 .and_then(|ver| ver.as_str())
457 .unwrap_or("unknown"); 469 .unwrap_or("unknown");
458 let version = v 470 format!("{} ({} v{})", name, software, version)
459 .get("version") 471 });
460 .and_then(|ver| ver.as_str())
461 .unwrap_or("unknown");
462 format!("{} ({} v{})", name, software, version)
463 });
464 checks.push(ProbeCheck { 472 checks.push(ProbeCheck {
465 name: "nip11_fetch", 473 name: "nip11_fetch",
466 passed: true, 474 passed: true,
@@ -509,7 +517,14 @@ pub async fn run_probe(
509 let mut write_succeeded = false; 517 let mut write_succeeded = false;
510 518
511 if Instant::now() >= deadline { 519 if Instant::now() >= deadline {
512 deadline_return!(relay_url, timestamp, total_start, overall_secs, checks, "publish_events"); 520 deadline_return!(
521 relay_url,
522 timestamp,
523 total_start,
524 overall_secs,
525 checks,
526 "publish_events"
527 );
513 } 528 }
514 529
515 if read_only { 530 if read_only {
@@ -558,10 +573,7 @@ pub async fn run_probe(
558 error: Some(e.to_string()), 573 error: Some(e.to_string()),
559 }); 574 });
560 // Skip steps 4 and 5; step 6 will use fallback 575 // Skip steps 4 and 5; step 6 will use fallback
561 checks.push(skipped( 576 checks.push(skipped("git_repo_initialised", "publish_events failed"));
562 "git_repo_initialised",
563 "publish_events failed",
564 ));
565 checks.push(skipped("git_push", "publish_events failed")); 577 checks.push(skipped("git_push", "publish_events failed"));
566 } 578 }
567 } 579 }
@@ -571,7 +583,14 @@ pub async fn run_probe(
571 // ============================================================ 583 // ============================================================
572 if write_succeeded { 584 if write_succeeded {
573 if Instant::now() >= deadline { 585 if Instant::now() >= deadline {
574 deadline_return!(relay_url, timestamp, total_start, overall_secs, checks, "git_repo_initialised"); 586 deadline_return!(
587 relay_url,
588 timestamp,
589 total_start,
590 overall_secs,
591 checks,
592 "git_repo_initialised"
593 );
575 } 594 }
576 let step4_start = Instant::now(); 595 let step4_start = Instant::now();
577 let poll_url = format!("{}/info/refs?service=git-upload-pack", clone_url); 596 let poll_url = format!("{}/info/refs?service=git-upload-pack", clone_url);
@@ -624,7 +643,14 @@ pub async fn run_probe(
624 // ============================================================ 643 // ============================================================
625 if write_succeeded { 644 if write_succeeded {
626 if Instant::now() >= deadline { 645 if Instant::now() >= deadline {
627 deadline_return!(relay_url, timestamp, total_start, overall_secs, checks, "git_push"); 646 deadline_return!(
647 relay_url,
648 timestamp,
649 total_start,
650 overall_secs,
651 checks,
652 "git_push"
653 );
628 } 654 }
629 let step5_start = Instant::now(); 655 let step5_start = Instant::now();
630 let push_result = try_push(&local_repo_path); 656 let push_result = try_push(&local_repo_path);
@@ -690,8 +716,14 @@ pub async fn run_probe(
690 continue; 716 continue;
691 } 717 }
692 let mut parts = content.splitn(2, ' '); 718 let mut parts = content.splitn(2, ' ');
693 let hash = match parts.next() { Some(h) if h.len() == 40 => h, _ => continue }; 719 let hash = match parts.next() {
694 let refname = match parts.next() { Some(r) => r.trim(), None => continue }; 720 Some(h) if h.len() == 40 => h,
721 _ => continue,
722 };
723 let refname = match parts.next() {
724 Some(r) => r.trim(),
725 None => continue,
726 };
695 // Skip refs/nostr/* — only branches (refs/heads/*) and tags (refs/tags/*) 727 // Skip refs/nostr/* — only branches (refs/heads/*) and tags (refs/tags/*)
696 if refname.starts_with("refs/nostr/") { 728 if refname.starts_with("refs/nostr/") {
697 continue; 729 continue;
@@ -705,14 +737,23 @@ pub async fn run_probe(
705 // ---- Write path ---- 737 // ---- Write path ----
706 // Step 6a: git_fetch_refs — just verify the endpoint returns 200 738 // Step 6a: git_fetch_refs — just verify the endpoint returns 200
707 if Instant::now() >= deadline { 739 if Instant::now() >= deadline {
708 deadline_return!(relay_url, timestamp, total_start, overall_secs, checks, "git_fetch_refs"); 740 deadline_return!(
741 relay_url,
742 timestamp,
743 total_start,
744 overall_secs,
745 checks,
746 "git_fetch_refs"
747 );
709 } 748 }
710 let refs_url = format!("{}/info/refs?service=git-upload-pack", clone_url); 749 let refs_url = format!("{}/info/refs?service=git-upload-pack", clone_url);
711 let http_client = reqwest::Client::new(); 750 let http_client = reqwest::Client::new();
712 751
713 let step6_start = Instant::now(); 752 let step6_start = Instant::now();
714 let refs_result = tokio::time::timeout( 753 let refs_result = tokio::time::timeout(
715 deadline.saturating_duration_since(Instant::now()).min(Duration::from_secs(timeout_secs)), 754 deadline
755 .saturating_duration_since(Instant::now())
756 .min(Duration::from_secs(timeout_secs)),
716 http_client.get(&refs_url).send(), 757 http_client.get(&refs_url).send(),
717 ) 758 )
718 .await; 759 .await;
@@ -833,14 +874,23 @@ pub async fn run_probe(
833 874
834 // In read-only mode: first check that at least one announcement exists 875 // In read-only mode: first check that at least one announcement exists
835 if Instant::now() >= deadline { 876 if Instant::now() >= deadline {
836 deadline_return!(relay_url, timestamp, total_start, overall_secs, checks, "serves_latest_announcement"); 877 deadline_return!(
878 relay_url,
879 timestamp,
880 total_start,
881 overall_secs,
882 checks,
883 "serves_latest_announcement"
884 );
837 } 885 }
838 let filter = Filter::new().kind(Kind::GitRepoAnnouncement).limit(1); 886 let filter = Filter::new().kind(Kind::GitRepoAnnouncement).limit(1);
839 let existing = client 887 let existing = client
840 .client() 888 .client()
841 .fetch_events( 889 .fetch_events(
842 filter, 890 filter,
843 deadline.saturating_duration_since(Instant::now()).min(Duration::from_secs(5)), 891 deadline
892 .saturating_duration_since(Instant::now())
893 .min(Duration::from_secs(5)),
844 ) 894 )
845 .await 895 .await
846 .unwrap_or_default(); 896 .unwrap_or_default();
@@ -909,18 +959,25 @@ pub async fn run_probe(
909 .find(|t| t.kind() == TagKind::custom("clone")) 959 .find(|t| t.kind() == TagKind::custom("clone"))
910 .and_then(|t| t.content()) 960 .and_then(|t| t.content())
911 .map(|s| s.to_string()) 961 .map(|s| s.to_string())
912 .unwrap_or_else(|| { 962 .unwrap_or_else(|| format!("{}/{}/{}.git", http_base, ann_npub, ann_id));
913 format!("{}/{}/{}.git", http_base, ann_npub, ann_id)
914 });
915 963
916 if Instant::now() >= deadline { 964 if Instant::now() >= deadline {
917 deadline_return!(relay_url, timestamp, total_start, overall_secs, checks, "git_fetch_refs"); 965 deadline_return!(
966 relay_url,
967 timestamp,
968 total_start,
969 overall_secs,
970 checks,
971 "git_fetch_refs"
972 );
918 } 973 }
919 let step6_start = Instant::now(); 974 let step6_start = Instant::now();
920 let refs_url = format!("{}/info/refs?service=git-upload-pack", fetch_url); 975 let refs_url = format!("{}/info/refs?service=git-upload-pack", fetch_url);
921 let http_client = reqwest::Client::new(); 976 let http_client = reqwest::Client::new();
922 let refs_result = tokio::time::timeout( 977 let refs_result = tokio::time::timeout(
923 deadline.saturating_duration_since(Instant::now()).min(Duration::from_secs(timeout_secs)), 978 deadline
979 .saturating_duration_since(Instant::now())
980 .min(Duration::from_secs(timeout_secs)),
924 http_client.get(&refs_url).send(), 981 http_client.get(&refs_url).send(),
925 ) 982 )
926 .await; 983 .await;
@@ -935,7 +992,7 @@ pub async fn run_probe(
935 passed: true, 992 passed: true,
936 skipped: false, 993 skipped: false,
937 duration_ms: step6_ms, 994 duration_ms: step6_ms,
938 detail: None, 995 detail: None,
939 error: None, 996 error: None,
940 }); 997 });
941 Some(body) 998 Some(body)
@@ -946,7 +1003,7 @@ pub async fn run_probe(
946 passed: false, 1003 passed: false,
947 skipped: false, 1004 skipped: false,
948 duration_ms: step6_ms, 1005 duration_ms: step6_ms,
949 detail: None, 1006 detail: None,
950 error: Some(format!("HTTP {}", resp.status())), 1007 error: Some(format!("HTTP {}", resp.status())),
951 }); 1008 });
952 None 1009 None
@@ -957,7 +1014,7 @@ pub async fn run_probe(
957 passed: false, 1014 passed: false,
958 skipped: false, 1015 skipped: false,
959 duration_ms: step6_ms, 1016 duration_ms: step6_ms,
960 detail: None, 1017 detail: None,
961 error: Some(e.to_string()), 1018 error: Some(e.to_string()),
962 }); 1019 });
963 None 1020 None
@@ -968,7 +1025,7 @@ pub async fn run_probe(
968 passed: false, 1025 passed: false,
969 skipped: false, 1026 skipped: false,
970 duration_ms: step6_ms, 1027 duration_ms: step6_ms,
971 detail: None, 1028 detail: None,
972 error: Some("timeout".to_string()), 1029 error: Some("timeout".to_string()),
973 }); 1030 });
974 None 1031 None
@@ -981,10 +1038,7 @@ pub async fn run_probe(
981 // including recursive maintainer chains), then compare against git refs. 1038 // including recursive maintainer chains), then compare against git refs.
982 match refs_body_fallback { 1039 match refs_body_fallback {
983 None => { 1040 None => {
984 checks.push(skipped( 1041 checks.push(skipped("git_refs_match_state", "git_fetch_refs failed"));
985 "git_refs_match_state",
986 "git_fetch_refs failed",
987 ));
988 } 1042 }
989 Some(body) => { 1043 Some(body) => {
990 let fetched_refs = parse_refs(&body); 1044 let fetched_refs = parse_refs(&body);
@@ -992,19 +1046,19 @@ pub async fn run_probe(
992 // Fetch all state events for this repo_id from the relay. 1046 // Fetch all state events for this repo_id from the relay.
993 // The relay only serves authorized state events (owner + full 1047 // The relay only serves authorized state events (owner + full
994 // recursive maintainer chain already resolved by the relay). 1048 // recursive maintainer chain already resolved by the relay).
995 let state_filter = Filter::new() 1049 let state_filter = Filter::new().kind(Kind::RepoState).custom_tag(
996 .kind(Kind::RepoState) 1050 nostr_sdk::prelude::SingleLetterTag::lowercase(
997 .custom_tag( 1051 nostr_sdk::prelude::Alphabet::D,
998 nostr_sdk::prelude::SingleLetterTag::lowercase( 1052 ),
999 nostr_sdk::prelude::Alphabet::D, 1053 ann_id.clone(),
1000 ), 1054 );
1001 ann_id.clone(),
1002 );
1003 let state_events = client 1055 let state_events = client
1004 .client() 1056 .client()
1005 .fetch_events( 1057 .fetch_events(
1006 state_filter, 1058 state_filter,
1007 deadline.saturating_duration_since(Instant::now()).min(Duration::from_secs(5)), 1059 deadline
1060 .saturating_duration_since(Instant::now())
1061 .min(Duration::from_secs(5)),
1008 ) 1062 )
1009 .await 1063 .await
1010 .unwrap_or_default(); 1064 .unwrap_or_default();
@@ -1046,7 +1100,8 @@ pub async fn run_probe(
1046 Some(h) => h.to_string(), 1100 Some(h) => h.to_string(),
1047 None => continue, 1101 None => continue,
1048 }; 1102 };
1049 let prev_ts = latest_ts.get(kind_str.as_ref()).copied().unwrap_or(0); 1103 let prev_ts =
1104 latest_ts.get(kind_str.as_ref()).copied().unwrap_or(0);
1050 if ts >= prev_ts { 1105 if ts >= prev_ts {
1051 expected.insert(kind_str.to_string(), hash); 1106 expected.insert(kind_str.to_string(), hash);
1052 latest_ts.insert(kind_str.to_string(), ts); 1107 latest_ts.insert(kind_str.to_string(), ts);
@@ -1103,10 +1158,7 @@ pub async fn run_probe(
1103 detail: None, 1158 detail: None,
1104 error: Some("no repositories found on relay".to_string()), 1159 error: Some("no repositories found on relay".to_string()),
1105 }); 1160 });
1106 checks.push(skipped( 1161 checks.push(skipped("git_refs_match_state", "no announcement found"));
1107 "git_refs_match_state",
1108 "no announcement found",
1109 ));
1110 } 1162 }
1111 } 1163 }
1112 } 1164 }
diff --git a/grasp-audit/src/specs/grasp01/purgatory.rs b/grasp-audit/src/specs/grasp01/purgatory.rs
index 0686da8..fdc1e32 100644
--- a/grasp-audit/src/specs/grasp01/purgatory.rs
+++ b/grasp-audit/src/specs/grasp01/purgatory.rs
@@ -54,9 +54,7 @@ impl PurgatoryTests {
54 54
55 // Deletion event tests (NIP-09) 55 // Deletion event tests (NIP-09)
56 results.add(Self::test_deletion_by_event_id_removes_purgatory_state_event(client).await); 56 results.add(Self::test_deletion_by_event_id_removes_purgatory_state_event(client).await);
57 results.add( 57 results.add(Self::test_deletion_by_coordinate_removes_purgatory_state_event(client).await);
58 Self::test_deletion_by_coordinate_removes_purgatory_state_event(client).await,
59 );
60 58
61 // PR purgatory tests 59 // PR purgatory tests
62 results.add(Self::test_pr_event_accepted_into_purgatory_and_isnt_served(client).await); 60 results.add(Self::test_pr_event_accepted_into_purgatory_and_isnt_served(client).await);
diff --git a/src/audit_cleanup.rs b/src/audit_cleanup.rs
index b976b1f..de78b1b 100644
--- a/src/audit_cleanup.rs
+++ b/src/audit_cleanup.rs
@@ -15,7 +15,7 @@
15//! 15//!
16//! Runs every `AUDIT_CLEANUP_INTERVAL_SECS` seconds. 16//! Runs every `AUDIT_CLEANUP_INTERVAL_SECS` seconds.
17 17
18use std::path::PathBuf; 18use std::path::{Path, PathBuf};
19use std::time::Duration; 19use std::time::Duration;
20 20
21use nostr_sdk::prelude::*; 21use nostr_sdk::prelude::*;
@@ -46,8 +46,12 @@ pub async fn run_audit_cleanup_loop(database: SharedDatabase, git_data_path: Pat
46} 46}
47 47
48/// Perform a single cleanup pass. 48/// Perform a single cleanup pass.
49async fn run_audit_cleanup_once(database: &SharedDatabase, git_data_path: &PathBuf) { 49async fn run_audit_cleanup_once(database: &SharedDatabase, git_data_path: &Path) {
50 let cutoff = Timestamp::from(Timestamp::now().as_secs().saturating_sub(AUDIT_CLEANUP_AGE_SECS)); 50 let cutoff = Timestamp::from(
51 Timestamp::now()
52 .as_secs()
53 .saturating_sub(AUDIT_CLEANUP_AGE_SECS),
54 );
51 55
52 // --- Step 1: Find repo announcements to delete git repos for --- 56 // --- Step 1: Find repo announcements to delete git repos for ---
53 let repo_filter = Filter::new() 57 let repo_filter = Filter::new()
@@ -73,10 +77,7 @@ async fn run_audit_cleanup_once(database: &SharedDatabase, git_data_path: &PathB
73 if repo_path.exists() { 77 if repo_path.exists() {
74 match std::fs::remove_dir_all(&repo_path) { 78 match std::fs::remove_dir_all(&repo_path) {
75 Ok(()) => { 79 Ok(()) => {
76 debug!( 80 debug!("audit_cleanup: deleted git repo {}", repo_path.display());
77 "audit_cleanup: deleted git repo {}",
78 repo_path.display()
79 );
80 repos_deleted += 1; 81 repos_deleted += 1;
81 82
82 // Remove the parent npub directory if it is now empty 83 // Remove the parent npub directory if it is now empty
@@ -131,9 +132,7 @@ async fn run_audit_cleanup_once(database: &SharedDatabase, git_data_path: &PathB
131 } 132 }
132 133
133 // --- Step 2: Delete all audit events from the database --- 134 // --- Step 2: Delete all audit events from the database ---
134 let all_audit_filter = Filter::new() 135 let all_audit_filter = Filter::new().hashtag(AUDIT_TEST_EVENT_TAG).until(cutoff);
135 .hashtag(AUDIT_TEST_EVENT_TAG)
136 .until(cutoff);
137 136
138 match database.delete(all_audit_filter).await { 137 match database.delete(all_audit_filter).await {
139 Ok(()) => { 138 Ok(()) => {
@@ -143,7 +142,10 @@ async fn run_audit_cleanup_once(database: &SharedDatabase, git_data_path: &PathB
143 ); 142 );
144 } 143 }
145 Err(e) => { 144 Err(e) => {
146 error!("audit_cleanup: failed to delete audit events from database: {}", e); 145 error!(
146 "audit_cleanup: failed to delete audit events from database: {}",
147 e
148 );
147 } 149 }
148 } 150 }
149} 151}
diff --git a/src/git/handlers.rs b/src/git/handlers.rs
index 5ff3a7f..b615251 100644
--- a/src/git/handlers.rs
+++ b/src/git/handlers.rs
@@ -154,13 +154,10 @@ pub async fn handle_upload_pack(
154 154
155 // Write request to git's stdin 155 // Write request to git's stdin
156 if let Some(mut stdin) = git.take_stdin() { 156 if let Some(mut stdin) = git.take_stdin() {
157 stdin 157 stdin.write_all(&request_body).await.map_err(|e| {
158 .write_all(&request_body) 158 error!("Failed to write to git upload-pack stdin: {}", e);
159 .await 159 GitError::IoError(e)
160 .map_err(|e| { 160 })?;
161 error!("Failed to write to git upload-pack stdin: {}", e);
162 GitError::IoError(e)
163 })?;
164 // Close stdin to signal end of input 161 // Close stdin to signal end of input
165 drop(stdin); 162 drop(stdin);
166 } 163 }
@@ -171,24 +168,18 @@ pub async fn handle_upload_pack(
171 168
172 if let Some(stdout) = git.take_stdout() { 169 if let Some(stdout) = git.take_stdout() {
173 let mut stdout = stdout; 170 let mut stdout = stdout;
174 stdout 171 stdout.read_to_end(&mut output).await.map_err(|e| {
175 .read_to_end(&mut output) 172 error!("Failed to read git upload-pack stdout: {}", e);
176 .await 173 GitError::IoError(e)
177 .map_err(|e| { 174 })?;
178 error!("Failed to read git upload-pack stdout: {}", e);
179 GitError::IoError(e)
180 })?;
181 } 175 }
182 176
183 if let Some(stderr) = git.take_stderr() { 177 if let Some(stderr) = git.take_stderr() {
184 let mut stderr = stderr; 178 let mut stderr = stderr;
185 stderr 179 stderr.read_to_end(&mut stderr_output).await.map_err(|e| {
186 .read_to_end(&mut stderr_output) 180 error!("Failed to read git upload-pack stderr: {}", e);
187 .await 181 GitError::IoError(e)
188 .map_err(|e| { 182 })?;
189 error!("Failed to read git upload-pack stderr: {}", e);
190 GitError::IoError(e)
191 })?;
192 } 183 }
193 184
194 // Wait for process 185 // Wait for process
@@ -317,13 +308,10 @@ pub async fn handle_receive_pack(
317 308
318 // Write request to git's stdin 309 // Write request to git's stdin
319 if let Some(mut stdin) = git.take_stdin() { 310 if let Some(mut stdin) = git.take_stdin() {
320 stdin 311 stdin.write_all(&request_body).await.map_err(|e| {
321 .write_all(&request_body) 312 error!("Failed to write to git receive-pack stdin: {}", e);
322 .await 313 GitError::IoError(e)
323 .map_err(|e| { 314 })?;
324 error!("Failed to write to git receive-pack stdin: {}", e);
325 GitError::IoError(e)
326 })?;
327 drop(stdin); 315 drop(stdin);
328 } 316 }
329 317
@@ -333,24 +321,18 @@ pub async fn handle_receive_pack(
333 321
334 if let Some(stdout) = git.take_stdout() { 322 if let Some(stdout) = git.take_stdout() {
335 let mut stdout = stdout; 323 let mut stdout = stdout;
336 stdout 324 stdout.read_to_end(&mut output).await.map_err(|e| {
337 .read_to_end(&mut output) 325 error!("Failed to read git receive-pack stdout: {}", e);
338 .await 326 GitError::IoError(e)
339 .map_err(|e| { 327 })?;
340 error!("Failed to read git receive-pack stdout: {}", e);
341 GitError::IoError(e)
342 })?;
343 } 328 }
344 329
345 if let Some(stderr) = git.take_stderr() { 330 if let Some(stderr) = git.take_stderr() {
346 let mut stderr = stderr; 331 let mut stderr = stderr;
347 stderr 332 stderr.read_to_end(&mut stderr_output).await.map_err(|e| {
348 .read_to_end(&mut stderr_output) 333 error!("Failed to read git receive-pack stderr: {}", e);
349 .await 334 GitError::IoError(e)
350 .map_err(|e| { 335 })?;
351 error!("Failed to read git receive-pack stderr: {}", e);
352 GitError::IoError(e)
353 })?;
354 } 336 }
355 337
356 // Wait for process 338 // Wait for process
diff --git a/src/git/sync.rs b/src/git/sync.rs
index 9a02ad4..05dcbda 100644
--- a/src/git/sync.rs
+++ b/src/git/sync.rs
@@ -814,6 +814,7 @@ pub fn extract_identifier_from_pr_event(event: &Event) -> Option<String> {
814/// 814///
815/// # Returns 815/// # Returns
816/// A `ProcessResult` describing what was processed 816/// A `ProcessResult` describing what was processed
817#[allow(clippy::too_many_arguments)]
817pub async fn process_newly_available_git_data( 818pub async fn process_newly_available_git_data(
818 source_repo_path: &Path, 819 source_repo_path: &Path,
819 new_oids: &HashSet<String>, 820 new_oids: &HashSet<String>,
@@ -1339,6 +1340,7 @@ async fn process_purgatory_pr_events(
1339/// When `write_policy` and `rejected_events_index` are provided (git push path), 1340/// When `write_policy` and `rejected_events_index` are provided (git push path),
1340/// any maintainer announcements sitting in the hot cache are re-processed immediately 1341/// any maintainer announcements sitting in the hot cache are re-processed immediately
1341/// after the owner announcement is promoted, so they don't wait for the next sync cycle. 1342/// after the owner announcement is promoted, so they don't wait for the next sync cycle.
1343#[allow(clippy::too_many_arguments)]
1342async fn process_purgatory_announcements( 1344async fn process_purgatory_announcements(
1343 identifier: &str, 1345 identifier: &str,
1344 source_repo_path: &Path, 1346 source_repo_path: &Path,
diff --git a/src/http/mod.rs b/src/http/mod.rs
index 76ffef3..c397365 100644
--- a/src/http/mod.rs
+++ b/src/http/mod.rs
@@ -105,6 +105,7 @@ struct HttpService {
105} 105}
106 106
107impl HttpService { 107impl HttpService {
108 #[allow(clippy::too_many_arguments)]
108 fn new( 109 fn new(
109 relay: LocalRelay, 110 relay: LocalRelay,
110 config: Config, 111 config: Config,
diff --git a/src/nostr/builder.rs b/src/nostr/builder.rs
index a0088e1..03132bf 100644
--- a/src/nostr/builder.rs
+++ b/src/nostr/builder.rs
@@ -18,7 +18,6 @@ use crate::nostr::policy::{
18 ReferenceResult, RelatedEventPolicy, StatePolicy, StateResult, 18 ReferenceResult, RelatedEventPolicy, StatePolicy, StateResult,
19}; 19};
20 20
21
22/// Type alias for the shared database used by the relay 21/// Type alias for the shared database used by the relay
23pub type SharedDatabase = Arc<dyn NostrDatabase>; 22pub type SharedDatabase = Arc<dyn NostrDatabase>;
24 23
diff --git a/src/nostr/policy/announcement.rs b/src/nostr/policy/announcement.rs
index b366f0b..aba5181 100644
--- a/src/nostr/policy/announcement.rs
+++ b/src/nostr/policy/announcement.rs
@@ -70,7 +70,10 @@ impl AnnouncementPolicy {
70 .is_some_and(|entry| event.created_at > entry.event.created_at); 70 .is_some_and(|entry| event.created_at > entry.event.created_at);
71 71
72 if should_evict { 72 if should_evict {
73 self.remove_purgatory_announcement(&event.pubkey, &announcement.identifier); 73 self.remove_purgatory_announcement(
74 &event.pubkey,
75 &announcement.identifier,
76 );
74 } 77 }
75 78
76 match self 79 match self
@@ -145,10 +148,9 @@ impl AnnouncementPolicy {
145 ); 148 );
146 AnnouncementResult::AcceptPurgatory 149 AnnouncementResult::AcceptPurgatory
147 } 150 }
148 Err(e) => AnnouncementResult::Reject(format!( 151 Err(e) => {
149 "Failed to parse announcement: {}", 152 AnnouncementResult::Reject(format!("Failed to parse announcement: {}", e))
150 e 153 }
151 )),
152 } 154 }
153 } 155 }
154 // AcceptPurgatory shouldn't come from validate_announcement, but handle it 156 // AcceptPurgatory shouldn't come from validate_announcement, but handle it
@@ -161,11 +163,7 @@ impl AnnouncementPolicy {
161 /// Called when a replacement announcement arrives for a (pubkey, identifier) pair 163 /// Called when a replacement announcement arrives for a (pubkey, identifier) pair
162 /// that is currently in purgatory. Updates the purgatory entry and extends the 164 /// that is currently in purgatory. Updates the purgatory entry and extends the
163 /// expiry so the new announcement has a fresh waiting window. 165 /// expiry so the new announcement has a fresh waiting window.
164 fn replace_purgatory_announcement( 166 fn replace_purgatory_announcement(&self, event: &Event, announcement: &RepositoryAnnouncement) {
165 &self,
166 event: &Event,
167 announcement: &RepositoryAnnouncement,
168 ) {
169 let repo_path = self.ctx.git_data_path.join(announcement.repo_path()); 167 let repo_path = self.ctx.git_data_path.join(announcement.repo_path());
170 let relays: HashSet<String> = announcement.relays.iter().cloned().collect(); 168 let relays: HashSet<String> = announcement.relays.iter().cloned().collect();
171 169
diff --git a/src/nostr/policy/deletion.rs b/src/nostr/policy/deletion.rs
index 6457c90..c5a52d4 100644
--- a/src/nostr/policy/deletion.rs
+++ b/src/nostr/policy/deletion.rs
@@ -155,7 +155,9 @@ impl DeletionPolicy {
155 author = %author.to_hex(), 155 author = %author.to_hex(),
156 "Deletion request: removing purgatory state event by event ID" 156 "Deletion request: removing purgatory state event by event ID"
157 ); 157 );
158 self.ctx.purgatory.remove_state_event(&identifier, &entry.event.id); 158 self.ctx
159 .purgatory
160 .remove_state_event(&identifier, &entry.event.id);
159 return; // event IDs are unique 161 return; // event IDs are unique
160 } 162 }
161 } 163 }
@@ -223,7 +225,9 @@ impl DeletionPolicy {
223 if entry.author == *author 225 if entry.author == *author
224 && entry.event.created_at.as_secs() <= deletion_created_at 226 && entry.event.created_at.as_secs() <= deletion_created_at
225 { 227 {
226 self.ctx.purgatory.remove_state_event(identifier, &entry.event.id); 228 self.ctx
229 .purgatory
230 .remove_state_event(identifier, &entry.event.id);
227 removed += 1; 231 removed += 1;
228 } 232 }
229 } 233 }
@@ -306,7 +310,10 @@ mod tests {
306 EventBuilder::new(Kind::GitRepoAnnouncement, "") 310 EventBuilder::new(Kind::GitRepoAnnouncement, "")
307 .tags(vec![ 311 .tags(vec![
308 Tag::identifier(identifier), 312 Tag::identifier(identifier),
309 Tag::custom(TagKind::custom("clone"), vec!["https://example.com/repo.git"]), 313 Tag::custom(
314 TagKind::custom("clone"),
315 vec!["https://example.com/repo.git"],
316 ),
310 ]) 317 ])
311 .sign_with_keys(keys) 318 .sign_with_keys(keys)
312 .unwrap() 319 .unwrap()
@@ -331,7 +338,9 @@ mod tests {
331 let announcement = make_announcement_event(&keys, identifier); 338 let announcement = make_announcement_event(&keys, identifier);
332 add_to_purgatory(&ctx, &announcement, identifier); 339 add_to_purgatory(&ctx, &announcement, identifier);
333 340
334 assert!(ctx.purgatory.has_purgatory_announcement(&keys.public_key(), identifier)); 341 assert!(ctx
342 .purgatory
343 .has_purgatory_announcement(&keys.public_key(), identifier));
335 344
336 // Build kind 5 deletion event referencing the announcement by event ID 345 // Build kind 5 deletion event referencing the announcement by event ID
337 let deletion = EventBuilder::new(Kind::EventDeletion, "") 346 let deletion = EventBuilder::new(Kind::EventDeletion, "")
@@ -347,7 +356,8 @@ mod tests {
347 356
348 assert!(matches!(result, WritePolicyResult::Accept)); 357 assert!(matches!(result, WritePolicyResult::Accept));
349 assert!( 358 assert!(
350 !ctx.purgatory.has_purgatory_announcement(&keys.public_key(), identifier), 359 !ctx.purgatory
360 .has_purgatory_announcement(&keys.public_key(), identifier),
351 "Purgatory entry should have been removed" 361 "Purgatory entry should have been removed"
352 ); 362 );
353 } 363 }
@@ -361,7 +371,9 @@ mod tests {
361 let announcement = make_announcement_event(&keys, identifier); 371 let announcement = make_announcement_event(&keys, identifier);
362 add_to_purgatory(&ctx, &announcement, identifier); 372 add_to_purgatory(&ctx, &announcement, identifier);
363 373
364 assert!(ctx.purgatory.has_purgatory_announcement(&keys.public_key(), identifier)); 374 assert!(ctx
375 .purgatory
376 .has_purgatory_announcement(&keys.public_key(), identifier));
365 377
366 // Build kind 5 deletion event referencing the announcement by coordinate 378 // Build kind 5 deletion event referencing the announcement by coordinate
367 let coord = format!("30617:{}:{}", keys.public_key().to_hex(), identifier); 379 let coord = format!("30617:{}:{}", keys.public_key().to_hex(), identifier);
@@ -378,7 +390,8 @@ mod tests {
378 390
379 assert!(matches!(result, WritePolicyResult::Accept)); 391 assert!(matches!(result, WritePolicyResult::Accept));
380 assert!( 392 assert!(
381 !ctx.purgatory.has_purgatory_announcement(&keys.public_key(), identifier), 393 !ctx.purgatory
394 .has_purgatory_announcement(&keys.public_key(), identifier),
382 "Purgatory entry should have been removed" 395 "Purgatory entry should have been removed"
383 ); 396 );
384 } 397 }
@@ -407,7 +420,8 @@ mod tests {
407 420
408 assert!(matches!(result, WritePolicyResult::Accept)); 421 assert!(matches!(result, WritePolicyResult::Accept));
409 assert!( 422 assert!(
410 ctx.purgatory.has_purgatory_announcement(&owner_keys.public_key(), identifier), 423 ctx.purgatory
424 .has_purgatory_announcement(&owner_keys.public_key(), identifier),
411 "Purgatory entry should NOT have been removed by wrong author" 425 "Purgatory entry should NOT have been removed by wrong author"
412 ); 426 );
413 } 427 }
@@ -438,7 +452,8 @@ mod tests {
438 452
439 assert!(matches!(result, WritePolicyResult::Accept)); 453 assert!(matches!(result, WritePolicyResult::Accept));
440 assert!( 454 assert!(
441 ctx.purgatory.has_purgatory_announcement(&owner_keys.public_key(), identifier), 455 ctx.purgatory
456 .has_purgatory_announcement(&owner_keys.public_key(), identifier),
442 "Purgatory entry should NOT have been removed by wrong author" 457 "Purgatory entry should NOT have been removed by wrong author"
443 ); 458 );
444 } 459 }
@@ -450,11 +465,10 @@ mod tests {
450 465
451 // No purgatory entry exists — deletion should still be accepted 466 // No purgatory entry exists — deletion should still be accepted
452 let deletion = EventBuilder::new(Kind::EventDeletion, "") 467 let deletion = EventBuilder::new(Kind::EventDeletion, "")
453 .tags(vec![ 468 .tags(vec![Tag::custom(
454 Tag::custom(TagKind::custom("a"), vec![ 469 TagKind::custom("a"),
455 format!("30617:{}:nonexistent", keys.public_key().to_hex()) 470 vec![format!("30617:{}:nonexistent", keys.public_key().to_hex())],
456 ]), 471 )])
457 ])
458 .sign_with_keys(&keys) 472 .sign_with_keys(&keys)
459 .unwrap(); 473 .unwrap();
460 474
@@ -491,7 +505,8 @@ mod tests {
491 505
492 assert!(matches!(result, WritePolicyResult::Accept)); 506 assert!(matches!(result, WritePolicyResult::Accept));
493 assert!( 507 assert!(
494 ctx.purgatory.has_purgatory_announcement(&keys.public_key(), identifier), 508 ctx.purgatory
509 .has_purgatory_announcement(&keys.public_key(), identifier),
495 "Purgatory entry should NOT be removed: entry is newer than deletion request" 510 "Purgatory entry should NOT be removed: entry is newer than deletion request"
496 ); 511 );
497 } 512 }
diff --git a/src/nostr/policy/pr_event.rs b/src/nostr/policy/pr_event.rs
index 52747a4..e4a64b8 100644
--- a/src/nostr/policy/pr_event.rs
+++ b/src/nostr/policy/pr_event.rs
@@ -7,7 +7,9 @@ use nostr_relay_builder::prelude::Event;
7 7
8use super::PolicyContext; 8use super::PolicyContext;
9use crate::git; 9use crate::git;
10use crate::git::authorization::{collect_authorized_maintainers, fetch_repository_data_excluding_purgatory}; 10use crate::git::authorization::{
11 collect_authorized_maintainers, fetch_repository_data_excluding_purgatory,
12};
11 13
12/// Policy for validating PR and PR Update events 14/// Policy for validating PR and PR Update events
13#[derive(Clone)] 15#[derive(Clone)]
@@ -131,7 +133,8 @@ impl PrEventPolicy {
131 // only be accepted for announcements that have been promoted (validated). 133 // only be accepted for announcements that have been promoted (validated).
132 // If the announcement is still in purgatory, the PR event should also go 134 // If the announcement is still in purgatory, the PR event should also go
133 // to purgatory and wait for the announcement to be promoted. 135 // to purgatory and wait for the announcement to be promoted.
134 let db_repo_data = fetch_repository_data_excluding_purgatory(&self.ctx.database, &identifier).await?; 136 let db_repo_data =
137 fetch_repository_data_excluding_purgatory(&self.ctx.database, &identifier).await?;
135 138
136 // Extract owner pubkey from source repo path 139 // Extract owner pubkey from source repo path
137 let owner_pubkey = crate::git::sync::extract_owner_from_repo_path( 140 let owner_pubkey = crate::git::sync::extract_owner_from_repo_path(
@@ -211,7 +214,8 @@ impl PrEventPolicy {
211 // only be accepted for announcements that have been promoted (validated). 214 // only be accepted for announcements that have been promoted (validated).
212 // If the announcement is still in purgatory, the PR event should also go 215 // If the announcement is still in purgatory, the PR event should also go
213 // to purgatory and wait for the announcement to be promoted. 216 // to purgatory and wait for the announcement to be promoted.
214 let db_repo_data = fetch_repository_data_excluding_purgatory(&self.ctx.database, identifier).await?; 217 let db_repo_data =
218 fetch_repository_data_excluding_purgatory(&self.ctx.database, identifier).await?;
215 219
216 // 3. Extract list of maintainers from "a 30617:<maintainer>:<identifier>" tags 220 // 3. Extract list of maintainers from "a 30617:<maintainer>:<identifier>" tags
217 let mut maintainer_pubkeys = std::collections::HashSet::new(); 221 let mut maintainer_pubkeys = std::collections::HashSet::new();
diff --git a/src/nostr/policy/state.rs b/src/nostr/policy/state.rs
index df743ae..80fe84c 100644
--- a/src/nostr/policy/state.rs
+++ b/src/nostr/policy/state.rs
@@ -158,7 +158,11 @@ impl StatePolicy {
158 // authorized it. 158 // authorized it.
159 for owner_hex in &authorized_owners { 159 for owner_hex in &authorized_owners {
160 if let Ok(owner_pk) = nostr_sdk::PublicKey::from_hex(owner_hex) { 160 if let Ok(owner_pk) = nostr_sdk::PublicKey::from_hex(owner_hex) {
161 if self.ctx.purgatory.has_purgatory_announcement(&owner_pk, &state.identifier) { 161 if self
162 .ctx
163 .purgatory
164 .has_purgatory_announcement(&owner_pk, &state.identifier)
165 {
162 self.ctx.purgatory.extend_announcement_expiry( 166 self.ctx.purgatory.extend_announcement_expiry(
163 &owner_pk, 167 &owner_pk,
164 &state.identifier, 168 &state.identifier,
diff --git a/src/purgatory/mod.rs b/src/purgatory/mod.rs
index bb6ff54..9b370d2 100644
--- a/src/purgatory/mod.rs
+++ b/src/purgatory/mod.rs
@@ -16,8 +16,14 @@ pub mod persistence;
16pub mod sync; 16pub mod sync;
17mod types; 17mod types;
18 18
19pub use helpers::{can_apply_state, can_satisfy_state, diagnose_state_mismatch, extract_refs_from_state, get_unpushed_refs}; 19pub use helpers::{
20pub use types::{AnnouncementPurgatoryEntry, EventSource, PrPurgatoryEntry, RefPair, RefUpdate, StatePurgatoryEntry}; 20 can_apply_state, can_satisfy_state, diagnose_state_mismatch, extract_refs_from_state,
21 get_unpushed_refs,
22};
23pub use types::{
24 AnnouncementPurgatoryEntry, EventSource, PrPurgatoryEntry, RefPair, RefUpdate,
25 StatePurgatoryEntry,
26};
21 27
22use dashmap::DashMap; 28use dashmap::DashMap;
23use nostr_sdk::prelude::*; 29use nostr_sdk::prelude::*;
@@ -672,9 +678,15 @@ impl Purgatory {
672 /// 678 ///
673 /// # Returns 679 /// # Returns
674 /// The announcement entry if found, None otherwise 680 /// The announcement entry if found, None otherwise
675 pub fn find_announcement(&self, owner: &PublicKey, identifier: &str) -> Option<AnnouncementPurgatoryEntry> { 681 pub fn find_announcement(
682 &self,
683 owner: &PublicKey,
684 identifier: &str,
685 ) -> Option<AnnouncementPurgatoryEntry> {
676 let key = (*owner, identifier.to_string()); 686 let key = (*owner, identifier.to_string());
677 self.announcement_purgatory.get(&key).map(|entry| entry.clone()) 687 self.announcement_purgatory
688 .get(&key)
689 .map(|entry| entry.clone())
678 } 690 }
679 691
680 /// Get all announcements in purgatory for a given identifier. 692 /// Get all announcements in purgatory for a given identifier.
@@ -687,7 +699,10 @@ impl Purgatory {
687 /// 699 ///
688 /// # Returns 700 /// # Returns
689 /// Vector of announcement entries for this identifier 701 /// Vector of announcement entries for this identifier
690 pub fn get_announcements_by_identifier(&self, identifier: &str) -> Vec<AnnouncementPurgatoryEntry> { 702 pub fn get_announcements_by_identifier(
703 &self,
704 identifier: &str,
705 ) -> Vec<AnnouncementPurgatoryEntry> {
691 self.announcement_purgatory 706 self.announcement_purgatory
692 .iter() 707 .iter()
693 .filter(|entry| entry.key().1 == identifier) 708 .filter(|entry| entry.key().1 == identifier)
@@ -755,7 +770,12 @@ impl Purgatory {
755 /// * `owner` - The owner pubkey 770 /// * `owner` - The owner pubkey
756 /// * `identifier` - The repository identifier 771 /// * `identifier` - The repository identifier
757 /// * `duration` - Minimum duration to guarantee from now 772 /// * `duration` - Minimum duration to guarantee from now
758 pub fn extend_announcement_expiry(&self, owner: &PublicKey, identifier: &str, duration: Duration) { 773 pub fn extend_announcement_expiry(
774 &self,
775 owner: &PublicKey,
776 identifier: &str,
777 duration: Duration,
778 ) {
759 let key = (*owner, identifier.to_string()); 779 let key = (*owner, identifier.to_string());
760 780
761 // Collect revival info before taking a mutable borrow 781 // Collect revival info before taking a mutable borrow
@@ -977,16 +997,24 @@ impl Purgatory {
977 .map(|entry| { 997 .map(|entry| {
978 let key = entry.key(); 998 let key = entry.key();
979 let v = entry.value(); 999 let v = entry.value();
980 (key.0.clone(), key.1.clone(), v.repo_path.clone(), v.event.id, v.soft_expired) 1000 (
1001 key.0,
1002 key.1.clone(),
1003 v.repo_path.clone(),
1004 v.event.id,
1005 v.soft_expired,
1006 )
981 }) 1007 })
982 .collect(); 1008 .collect();
983 1009
984 let mut announcement_removed = 0; 1010 let mut announcement_removed = 0;
985 for (owner, identifier, repo_path, event_id, already_soft_expired) in expired_announcements { 1011 for (owner, identifier, repo_path, event_id, already_soft_expired) in expired_announcements
1012 {
986 if already_soft_expired { 1013 if already_soft_expired {
987 // Phase 2: fully remove 1014 // Phase 2: fully remove
988 self.mark_expired(event_id); 1015 self.mark_expired(event_id);
989 self.announcement_purgatory.remove(&(owner.clone(), identifier.clone())); 1016 self.announcement_purgatory
1017 .remove(&(owner, identifier.clone()));
990 announcement_removed += 1; 1018 announcement_removed += 1;
991 tracing::info!( 1019 tracing::info!(
992 owner = %owner, 1020 owner = %owner,
@@ -1026,7 +1054,10 @@ impl Purgatory {
1026 1054
1027 if repo_gone { 1055 if repo_gone {
1028 // Mark soft_expired and extend expiry 1056 // Mark soft_expired and extend expiry
1029 if let Some(mut entry) = self.announcement_purgatory.get_mut(&(owner.clone(), identifier.clone())) { 1057 if let Some(mut entry) = self
1058 .announcement_purgatory
1059 .get_mut(&(owner, identifier.clone()))
1060 {
1030 entry.soft_expired = true; 1061 entry.soft_expired = true;
1031 entry.expires_at = now + SOFT_EXPIRY_EXTENDED; 1062 entry.expires_at = now + SOFT_EXPIRY_EXTENDED;
1032 } 1063 }
diff --git a/src/sync/mod.rs b/src/sync/mod.rs
index cd62380..36142e3 100644
--- a/src/sync/mod.rs
+++ b/src/sync/mod.rs
@@ -2406,7 +2406,8 @@ impl SyncManager {
2406 } 2406 }
2407 2407
2408 // Register any new entries in repo_sync_index as StateOnly 2408 // Register any new entries in repo_sync_index as StateOnly
2409 let mut new_relay_urls: std::collections::HashSet<String> = std::collections::HashSet::new(); 2409 let mut new_relay_urls: std::collections::HashSet<String> =
2410 std::collections::HashSet::new();
2410 { 2411 {
2411 let mut index = self.repo_sync_index.write().await; 2412 let mut index = self.repo_sync_index.write().await;
2412 for (repo_id, relays) in &announcements { 2413 for (repo_id, relays) in &announcements {
diff --git a/tests/common/sync_helpers.rs b/tests/common/sync_helpers.rs
index af51e78..611b1a5 100644
--- a/tests/common/sync_helpers.rs
+++ b/tests/common/sync_helpers.rs
@@ -1240,8 +1240,11 @@ pub async fn push_unique_git_data_to_relay(
1240 git(path, &["config", "commit.gpgsign", "false"]); 1240 git(path, &["config", "commit.gpgsign", "false"]);
1241 1241
1242 // Write a unique file so each maintainer gets a distinct commit hash 1242 // Write a unique file so each maintainer gets a distinct commit hash
1243 std::fs::write(path.join("state_test.txt"), "State test content for purgatory sync") 1243 std::fs::write(
1244 .expect("write state_test.txt"); 1244 path.join("state_test.txt"),
1245 "State test content for purgatory sync",
1246 )
1247 .expect("write state_test.txt");
1245 std::fs::write(path.join(".unique"), unique_seed).expect("write .unique"); 1248 std::fs::write(path.join(".unique"), unique_seed).expect("write .unique");
1246 git(path, &["add", "."]); 1249 git(path, &["add", "."]);
1247 git(path, &["commit", "-m", "State test commit"]); 1250 git(path, &["commit", "-m", "State test commit"]);
diff --git a/tests/purgatory_persistence.rs b/tests/purgatory_persistence.rs
index 655b0d9..0c1de28 100644
--- a/tests/purgatory_persistence.rs
+++ b/tests/purgatory_persistence.rs
@@ -169,7 +169,10 @@ async fn test_full_purgatory_save_restore_cycle() {
169 169
170 // Verify all data was restored 170 // Verify all data was restored
171 let (announcement_count2, state_count2, pr_count2) = purgatory2.count(); 171 let (announcement_count2, state_count2, pr_count2) = purgatory2.count();
172 assert_eq!(announcement_count2, 1, "Should have 1 announcement after restore"); 172 assert_eq!(
173 announcement_count2, 1,
174 "Should have 1 announcement after restore"
175 );
173 assert_eq!(state_count2, 2, "Should have 2 state events after restore"); 176 assert_eq!(state_count2, 2, "Should have 2 state events after restore");
174 assert_eq!( 177 assert_eq!(
175 pr_count2, 3, 178 pr_count2, 3,
@@ -853,7 +856,10 @@ async fn test_announcement_save_restore_cycle() {
853 let purgatory2 = Purgatory::new(&git_data_path); 856 let purgatory2 = Purgatory::new(&git_data_path);
854 purgatory2.restore_from_disk(&state_path).unwrap(); 857 purgatory2.restore_from_disk(&state_path).unwrap();
855 858
856 assert!(!state_path.exists(), "State file should be deleted after restore"); 859 assert!(
860 !state_path.exists(),
861 "State file should be deleted after restore"
862 );
857 863
858 let (ann_count2, _, _) = purgatory2.count(); 864 let (ann_count2, _, _) = purgatory2.count();
859 assert_eq!(ann_count2, 1, "Announcement should be restored"); 865 assert_eq!(ann_count2, 1, "Announcement should be restored");
diff --git a/tests/purgatory_sync.rs b/tests/purgatory_sync.rs
index eefd6bc..ced39ff 100644
--- a/tests/purgatory_sync.rs
+++ b/tests/purgatory_sync.rs
@@ -711,15 +711,10 @@ async fn test_concurrent_state_and_pr_sync() {
711 ); 711 );
712 712
713 // Check refs/nostr/<event-id> points to pr_commit 713 // Check refs/nostr/<event-id> points to pr_commit
714 let pr_ref_correct = check_ref_at_commit( 714 let pr_ref_correct =
715 &syncing_domain, 715 check_ref_at_commit(&syncing_domain, &npub, identifier, &pr_ref_name, &pr_commit)
716 &npub, 716 .await
717 identifier, 717 .expect("Failed to check PR ref");
718 &pr_ref_name,
719 &pr_commit,
720 )
721 .await
722 .expect("Failed to check PR ref");
723 718
724 assert!( 719 assert!(
725 pr_ref_correct, 720 pr_ref_correct,
@@ -993,7 +988,7 @@ async fn test_pr_event_clone_tag_sync_with_partial_oid_aggregation_from_multiple
993 let syncing_relay = TestRelay::start_on_port_with_options( 988 let syncing_relay = TestRelay::start_on_port_with_options(
994 syncing_port, 989 syncing_port,
995 Some(source_grasp.url().to_string()), // Bootstrap from source_grasp 990 Some(source_grasp.url().to_string()), // Bootstrap from source_grasp
996 true, // Disable negentropy - MockRelay doesn't support NIP-77 991 true, // Disable negentropy - MockRelay doesn't support NIP-77
997 ) 992 )
998 .await; 993 .await;
999 994
diff --git a/tests/sync/discovery.rs b/tests/sync/discovery.rs
index d45a290..ee675e6 100644
--- a/tests/sync/discovery.rs
+++ b/tests/sync/discovery.rs
@@ -183,7 +183,10 @@ async fn test_relay_discovery_via_announcements_with_historic_sync() {
183 let (announcement, _git_dir_a) = 183 let (announcement, _git_dir_a) =
184 setup_announcement_on_relay(&relay_a, &keys, &domain_refs, repo_id).await; 184 setup_announcement_on_relay(&relay_a, &keys, &domain_refs, repo_id).await;
185 let announcement_id = announcement.id; 185 let announcement_id = announcement.id;
186 println!("Announcement {} set up on relay_a with git data (Layer 1)", announcement_id); 186 println!(
187 "Announcement {} set up on relay_a with git data (Layer 1)",
188 announcement_id
189 );
187 190
188 // Build repo coordinate for Layer 2 reference 191 // Build repo coordinate for Layer 2 reference
189 let repo_coord = repo_coord(&keys, repo_id); 192 let repo_coord = repo_coord(&keys, repo_id);
@@ -235,4 +238,3 @@ async fn test_relay_discovery_via_announcements_with_historic_sync() {
235 issue_id 238 issue_id
236 ); 239 );
237} 240}
238
diff --git a/tests/sync/maintainer_reprocessing.rs b/tests/sync/maintainer_reprocessing.rs
index ff1eb43..9154ee5 100644
--- a/tests/sync/maintainer_reprocessing.rs
+++ b/tests/sync/maintainer_reprocessing.rs
@@ -70,17 +70,15 @@ async fn test_maintainer_announcement_reprocessed_immediately() {
70 identifier 70 identifier
71 )], 71 )],
72 ), 72 ),
73 Tag::custom( 73 Tag::custom(TagKind::custom("relays"), vec![relay_a.url().to_string()]),
74 TagKind::custom("relays"),
75 vec![relay_a.url().to_string()],
76 ),
77 ]) 74 ])
78 .sign_with_keys(&maintainer_keys) 75 .sign_with_keys(&maintainer_keys)
79 .unwrap(); 76 .unwrap();
80 send_to_relay(&relay_a, &maintainer_announcement).await.unwrap(); 77 send_to_relay(&relay_a, &maintainer_announcement)
78 .await
79 .unwrap();
81 let _git_dir_maintainer = 80 let _git_dir_maintainer =
82 push_git_data_to_relay(&relay_a, &maintainer_keys, identifier, &[&relay_a.domain()]) 81 push_git_data_to_relay(&relay_a, &maintainer_keys, identifier, &[&relay_a.domain()]).await;
83 .await;
84 println!("✓ Maintainer announcement + git data pushed to relay_a"); 82 println!("✓ Maintainer announcement + git data pushed to relay_a");
85 83
86 // Step 2: Start relay_b with relay_a as bootstrap so its SyncManager connects immediately. 84 // Step 2: Start relay_b with relay_a as bootstrap so its SyncManager connects immediately.
@@ -134,7 +132,9 @@ async fn test_maintainer_announcement_reprocessed_immediately() {
134 // re-processing of the maintainer announcement via our new code path. 132 // re-processing of the maintainer announcement via our new code path.
135 let _git_dir_owner = 133 let _git_dir_owner =
136 push_git_data_to_relay(&relay_b, &owner_keys, identifier, &[&relay_b.domain()]).await; 134 push_git_data_to_relay(&relay_b, &owner_keys, identifier, &[&relay_b.domain()]).await;
137 println!("✓ Owner git data pushed to relay_b (owner announcement promoted, hot cache re-processed)"); 135 println!(
136 "✓ Owner git data pushed to relay_b (owner announcement promoted, hot cache re-processed)"
137 );
138 138
139 // Step 5: Wait briefly for async processing to complete. 139 // Step 5: Wait briefly for async processing to complete.
140 tokio::time::sleep(Duration::from_secs(1)).await; 140 tokio::time::sleep(Duration::from_secs(1)).await;
@@ -363,9 +363,12 @@ async fn test_multiple_maintainers_all_reprocessed() {
363 .kind(Kind::GitRepoAnnouncement) 363 .kind(Kind::GitRepoAnnouncement)
364 .author(keys.public_key()) 364 .author(keys.public_key())
365 .identifier(identifier); 365 .identifier(identifier);
366 let found = 366 let found = wait_for_event_on_relay(relay_a.url(), filter, Duration::from_secs(10)).await;
367 wait_for_event_on_relay(relay_a.url(), filter, Duration::from_secs(10)).await; 367 assert!(
368 assert!(found, "{} announcement should be in relay_a before starting relay_b", name); 368 found,
369 "{} announcement should be in relay_a before starting relay_b",
370 name
371 );
369 } 372 }
370 println!("✓ All three maintainer announcements confirmed in relay_a's DB"); 373 println!("✓ All three maintainer announcements confirmed in relay_a's DB");
371 374
diff --git a/tests/sync/metrics.rs b/tests/sync/metrics.rs
index e973bbb..996c8b7 100644
--- a/tests/sync/metrics.rs
+++ b/tests/sync/metrics.rs
@@ -417,9 +417,12 @@ async fn test_live_sync_event_count() {
417 println!("Announcement set up on source relay with git data"); 417 println!("Announcement set up on source relay with git data");
418 418
419 // Start syncing relay with pre-allocated port 419 // Start syncing relay with pre-allocated port
420 let syncing_relay = 420 let syncing_relay = TestRelay::start_on_port_with_options(
421 TestRelay::start_on_port_with_options(sync_port, Some(source_relay.url().to_string()), false) 421 sync_port,
422 .await; 422 Some(source_relay.url().to_string()),
423 false,
424 )
425 .await;
423 println!("Syncing relay started at {}", syncing_relay.url()); 426 println!("Syncing relay started at {}", syncing_relay.url());
424 427
425 // Wait for sync connection to be fully established with EOSE received 428 // Wait for sync connection to be fully established with EOSE received
@@ -458,8 +461,14 @@ async fn test_live_sync_event_count() {
458 let client = TestClient::new(source_relay.url(), keys.clone()) 461 let client = TestClient::new(source_relay.url(), keys.clone())
459 .await 462 .await
460 .expect("Failed to connect to source"); 463 .expect("Failed to connect to source");
461 client.send_event(&patch1).await.expect("Failed to send patch 1"); 464 client
462 client.send_event(&patch2).await.expect("Failed to send patch 2"); 465 .send_event(&patch1)
466 .await
467 .expect("Failed to send patch 1");
468 client
469 .send_event(&patch2)
470 .await
471 .expect("Failed to send patch 2");
463 client.disconnect().await; 472 client.disconnect().await;
464 println!("Two patches sent to source relay (live mode)"); 473 println!("Two patches sent to source relay (live mode)");
465 474