diff options
| -rw-r--r-- | grasp-audit/src/bin/grasp-audit.rs | 13 | ||||
| -rw-r--r-- | grasp-audit/src/fixtures.rs | 16 | ||||
| -rw-r--r-- | grasp-audit/src/probe.rs | 190 | ||||
| -rw-r--r-- | grasp-audit/src/specs/grasp01/purgatory.rs | 4 | ||||
| -rw-r--r-- | src/audit_cleanup.rs | 24 | ||||
| -rw-r--r-- | src/git/handlers.rs | 66 | ||||
| -rw-r--r-- | src/git/sync.rs | 2 | ||||
| -rw-r--r-- | src/http/mod.rs | 1 | ||||
| -rw-r--r-- | src/nostr/builder.rs | 1 | ||||
| -rw-r--r-- | src/nostr/policy/announcement.rs | 18 | ||||
| -rw-r--r-- | src/nostr/policy/deletion.rs | 45 | ||||
| -rw-r--r-- | src/nostr/policy/pr_event.rs | 10 | ||||
| -rw-r--r-- | src/nostr/policy/state.rs | 6 | ||||
| -rw-r--r-- | src/purgatory/mod.rs | 51 | ||||
| -rw-r--r-- | src/sync/mod.rs | 3 | ||||
| -rw-r--r-- | tests/common/sync_helpers.rs | 7 | ||||
| -rw-r--r-- | tests/purgatory_persistence.rs | 10 | ||||
| -rw-r--r-- | tests/purgatory_sync.rs | 15 | ||||
| -rw-r--r-- | tests/sync/discovery.rs | 6 | ||||
| -rw-r--r-- | tests/sync/maintainer_reprocessing.rs | 25 | ||||
| -rw-r--r-- | tests/sync/metrics.rs | 19 |
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 | ||
| 18 | use std::path::PathBuf; | 18 | use std::path::{Path, PathBuf}; |
| 19 | use std::time::Duration; | 19 | use std::time::Duration; |
| 20 | 20 | ||
| 21 | use nostr_sdk::prelude::*; | 21 | use 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. |
| 49 | async fn run_audit_cleanup_once(database: &SharedDatabase, git_data_path: &PathBuf) { | 49 | async 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)] | ||
| 817 | pub async fn process_newly_available_git_data( | 818 | pub 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)] | ||
| 1342 | async fn process_purgatory_announcements( | 1344 | async 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 | ||
| 107 | impl HttpService { | 107 | impl 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 |
| 23 | pub type SharedDatabase = Arc<dyn NostrDatabase>; | 22 | pub 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 | ||
| 8 | use super::PolicyContext; | 8 | use super::PolicyContext; |
| 9 | use crate::git; | 9 | use crate::git; |
| 10 | use crate::git::authorization::{collect_authorized_maintainers, fetch_repository_data_excluding_purgatory}; | 10 | use 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; | |||
| 16 | pub mod sync; | 16 | pub mod sync; |
| 17 | mod types; | 17 | mod types; |
| 18 | 18 | ||
| 19 | pub use helpers::{can_apply_state, can_satisfy_state, diagnose_state_mismatch, extract_refs_from_state, get_unpushed_refs}; | 19 | pub use helpers::{ |
| 20 | pub 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 | }; | ||
| 23 | pub use types::{ | ||
| 24 | AnnouncementPurgatoryEntry, EventSource, PrPurgatoryEntry, RefPair, RefUpdate, | ||
| 25 | StatePurgatoryEntry, | ||
| 26 | }; | ||
| 21 | 27 | ||
| 22 | use dashmap::DashMap; | 28 | use dashmap::DashMap; |
| 23 | use nostr_sdk::prelude::*; | 29 | use 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 | ||