upleb.uk

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

summaryrefslogtreecommitdiff
path: root/tests/sync/discovery.rs
diff options
context:
space:
mode:
authorDanConwayDev <DanConwayDev@protonmail.com>2026-02-23 15:41:32 +0000
committerDanConwayDev <DanConwayDev@protonmail.com>2026-02-23 15:41:32 +0000
commitc54ce061d6d278cce8362d5af085808ca60c239b (patch)
treeec967d6195d9f7ec4f061449596611afe3a0950f /tests/sync/discovery.rs
parente0ad39a489b3398f8208713bf728db0cb11475b0 (diff)
parent113928aa84894ea8f65c247d9987527e792b32a9 (diff)
feat: announcement purgatory
Extends purgatory to hold repository announcements until git data arrives, preventing empty repositories from being served to clients. When an announcement is received, a bare repo is created immediately and the announcement is held in purgatory. It is only promoted and served once a git push confirms real content exists. If no push arrives before expiry, the bare repo is deleted and the announcement is silently discarded. Key behaviours: - Soft expiry: announcements are hidden from clients but kept alive while git pushes are in progress, reviving on successful push - Expiry is extended when a matching state event or git push is observed - NIP-09 deletion events remove announcements from purgatory - Purgatory state (announcements, state events, PR events, expired set) is persisted to disk on graceful shutdown and restored on startup, with elapsed downtime subtracted from expiry deadlines - Purgatory announcements drive StateOnly sync in the sync system so state events are fetched from listed relays before promotion - SyncLevel added to RepoSyncIndex to distinguish purgatory repos (StateOnly) from promoted repos (Full L2+L3 sync)
Diffstat (limited to 'tests/sync/discovery.rs')
-rw-r--r--tests/sync/discovery.rs259
1 files changed, 32 insertions, 227 deletions
diff --git a/tests/sync/discovery.rs b/tests/sync/discovery.rs
index 8ed80b5..d45a290 100644
--- a/tests/sync/discovery.rs
+++ b/tests/sync/discovery.rs
@@ -3,10 +3,6 @@
3//! Tests for relay discovery from announcement events. 3//! Tests for relay discovery from announcement events.
4//! When a relay receives an announcement listing another relay, 4//! When a relay receives an announcement listing another relay,
5//! it should discover and connect to that relay to sync events. 5//! it should discover and connect to that relay to sync events.
6//!
7//! # Tests
8//! - Test 2: Direct Layer 3 discovery from Layer 2
9//! - Test 3: Recursive multi-hop Layer 3 discovery
10 6
11use std::time::Duration; 7use std::time::Duration;
12 8
@@ -62,29 +58,26 @@ async fn test_discovers_layer3_via_layer2() {
62 // 3. Create test keys 58 // 3. Create test keys
63 let keys = Keys::generate(); 59 let keys = Keys::generate();
64 60
65 // 4. Create a repository announcement that lists BOTH relays 61 // 4. Set up repository announcement on relay_a with git data
66 let announcement = create_repo_announcement( 62 // (purgatory requires git data before announcements are accepted)
67 &keys, 63 let repo_id = "test-repo-discovery";
68 &[&relay_a.domain(), &relay_b.domain()], 64 let domains = vec![relay_a.domain(), relay_b.domain()];
69 "test-repo-discovery", 65 let domain_refs: Vec<&str> = domains.iter().map(|s| s.as_str()).collect();
70 );
71 let announcement_id = announcement.id;
72 66
67 let (announcement, _git_dir_a) =
68 setup_announcement_on_relay(&relay_a, &keys, &domain_refs, repo_id).await;
69 let announcement_id = announcement.id;
73 println!( 70 println!(
74 "Created announcement {} (kind {})", 71 "Announcement {} set up on relay_a with git data",
75 announcement_id, 72 announcement_id
76 announcement.kind.as_u16()
77 ); 73 );
78 for tag in announcement.tags.iter() {
79 println!(" Tag: {:?}", tag.as_slice());
80 }
81 74
82 // 5. Build the repo coordinate for the 'a' tag in the patch 75 // 5. Build the repo coordinate for the 'a' tag in the patch
83 let repo_coord = format!( 76 let repo_coord = format!(
84 "{}:{}:{}", 77 "{}:{}:{}",
85 Kind::GitRepoAnnouncement.as_u16(), 78 Kind::GitRepoAnnouncement.as_u16(),
86 keys.public_key().to_hex(), 79 keys.public_key().to_hex(),
87 "test-repo-discovery" 80 repo_id
88 ); 81 );
89 82
90 // 6. Create a patch event (Layer 2) that references the announcement 83 // 6. Create a patch event (Layer 2) that references the announcement
@@ -97,22 +90,13 @@ async fn test_discovers_layer3_via_layer2() {
97 let patch_id = patch.id; 90 let patch_id = patch.id;
98 91
99 println!("Created patch {} (kind {})", patch_id, patch.kind.as_u16()); 92 println!("Created patch {} (kind {})", patch_id, patch.kind.as_u16());
100 for tag in patch.tags.iter() {
101 println!(" Tag: {:?}", tag.as_slice());
102 }
103 93
104 // 7. Send announcement and patch to relay_a ONLY 94 // 7. Send patch to relay_a
105 let client_a = TestClient::new(relay_a.url(), keys.clone()) 95 let client_a = TestClient::new(relay_a.url(), keys.clone())
106 .await 96 .await
107 .expect("Failed to connect to relay_a"); 97 .expect("Failed to connect to relay_a");
108 98
109 client_a 99 client_a
110 .send_event(&announcement)
111 .await
112 .expect("Failed to send announcement to relay_a");
113 println!("Announcement sent to relay_a");
114
115 client_a
116 .send_event(&patch) 100 .send_event(&patch)
117 .await 101 .await
118 .expect("Failed to send patch to relay_a"); 102 .expect("Failed to send patch to relay_a");
@@ -120,18 +104,10 @@ async fn test_discovers_layer3_via_layer2() {
120 104
121 client_a.disconnect().await; 105 client_a.disconnect().await;
122 106
123 // 8. Send announcement to relay_b directly (triggers discovery of relay_a) 107 // 8. Set up announcement on relay_b (triggers discovery of relay_a)
124 let client_b = TestClient::new(relay_b.url(), keys.clone()) 108 let (_announcement_b, _git_dir_b) =
125 .await 109 setup_announcement_on_relay(&relay_b, &keys, &domain_refs, repo_id).await;
126 .expect("Failed to connect to relay_b"); 110 println!("Announcement set up on relay_b (should trigger discovery of relay_a)");
127
128 client_b
129 .send_event(&announcement)
130 .await
131 .expect("Failed to send announcement to relay_b");
132 println!("Announcement sent to relay_b (should trigger discovery of relay_a)");
133
134 client_b.disconnect().await;
135 111
136 // 9. Wait for relay_b to discover relay_a and sync the patch 112 // 9. Wait for relay_b to discover relay_a and sync the patch
137 println!("Waiting 3s for relay_b to discover relay_a and sync patch..."); 113 println!("Waiting 3s for relay_b to discover relay_a and sync patch...");
@@ -197,19 +173,20 @@ async fn test_relay_discovery_via_announcements_with_historic_sync() {
197 // 3. Create test keys 173 // 3. Create test keys
198 let keys = Keys::generate(); 174 let keys = Keys::generate();
199 175
200 // 4. Create the event chain on relay_a: 176 // 4. Set up repository on relay_a with git data and a Layer 2 issue
201 177
202 // Layer 1: Repository announcement 178 // Layer 1: Set up announcement with git data
203 let announcement = create_repo_announcement( 179 let domains = vec![relay_a.domain(), relay_b.domain()];
204 &keys, 180 let domain_refs: Vec<&str> = domains.iter().map(|s| s.as_str()).collect();
205 &[&relay_a.domain(), &relay_b.domain()], 181 let repo_id = "test-repo-chain";
206 "test-repo-chain", 182
207 ); 183 let (announcement, _git_dir_a) =
184 setup_announcement_on_relay(&relay_a, &keys, &domain_refs, repo_id).await;
208 let announcement_id = announcement.id; 185 let announcement_id = announcement.id;
209 println!("Created announcement {} (Layer 1)", announcement_id); 186 println!("Announcement {} set up on relay_a with git data (Layer 1)", announcement_id);
210 187
211 // Build repo coordinate for Layer 2 reference 188 // Build repo coordinate for Layer 2 reference
212 let repo_coord = repo_coord(&keys, "test-repo-chain"); 189 let repo_coord = repo_coord(&keys, repo_id);
213 190
214 // Layer 2: Issue referencing the repo 191 // Layer 2: Issue referencing the repo
215 let issue = build_layer2_issue_event(&keys, &repo_coord, "Test issue for chain discovery") 192 let issue = build_layer2_issue_event(&keys, &repo_coord, "Test issue for chain discovery")
@@ -217,35 +194,23 @@ async fn test_relay_discovery_via_announcements_with_historic_sync() {
217 let issue_id = issue.id; 194 let issue_id = issue.id;
218 println!("Created issue {} (Layer 2)", issue_id); 195 println!("Created issue {} (Layer 2)", issue_id);
219 196
220 // 5. Send all events to relay_a 197 // 5. Send issue to relay_a
221 let client_a = TestClient::new(relay_a.url(), keys.clone()) 198 let client_a = TestClient::new(relay_a.url(), keys.clone())
222 .await 199 .await
223 .expect("Failed to connect to relay_a"); 200 .expect("Failed to connect to relay_a");
224 201
225 client_a 202 client_a
226 .send_event(&announcement)
227 .await
228 .expect("Failed to send announcement");
229 client_a
230 .send_event(&issue) 203 .send_event(&issue)
231 .await 204 .await
232 .expect("Failed to send issue"); 205 .expect("Failed to send issue");
233 206
234 println!("Events sent to relay_a"); 207 println!("Issue sent to relay_a");
235 client_a.disconnect().await; 208 client_a.disconnect().await;
236 209
237 // 6. Send only the announcement to relay_b (triggers discovery) 210 // 6. Set up announcement on relay_b (triggers discovery of relay_a)
238 let client_b = TestClient::new(relay_b.url(), keys.clone()) 211 let (_announcement_b, _git_dir_b) =
239 .await 212 setup_announcement_on_relay(&relay_b, &keys, &domain_refs, repo_id).await;
240 .expect("Failed to connect to relay_b"); 213 println!("Announcement set up on relay_b (should trigger discovery of relay_a)");
241
242 client_b
243 .send_event(&announcement)
244 .await
245 .expect("Failed to send announcement to relay_b");
246 println!("Announcement sent to relay_b (should trigger discovery)");
247
248 client_b.disconnect().await;
249 214
250 // 7. Wait for sync 215 // 7. Wait for sync
251 println!("Waiting 3s for Layer 2 sync..."); 216 println!("Waiting 3s for Layer 2 sync...");
@@ -271,163 +236,3 @@ async fn test_relay_discovery_via_announcements_with_historic_sync() {
271 ); 236 );
272} 237}
273 238
274/// Test 3: 3-relay recursive discovery - relay discovers third relay through bootstrap
275///
276/// Scenario:
277/// ```text
278/// relay_a (SUT) relay_b (bootstrap) relay_c (discovered)
279/// │ │ │
280/// │ │ has announcement_x │ has announcement_y
281/// │ │ listing A+B+C │ listing A+C
282/// │ │ │
283/// ├────connect──────────► │
284/// │◄───sync announcement_x───────────────────────
285/// │ │
286/// │ discovers relay_c from announcement_x │
287/// │ │
288/// ├─────────────connect─────────────────────────►
289/// │◄────────────sync announcement_y─────────────┘
290/// ```
291///
292/// This tests that relay_a:
293/// 1. Connects to relay_b (configured as bootstrap)
294/// 2. Receives announcement_x which lists relay_c
295/// 3. Discovers and connects to relay_c
296/// 4. Syncs announcement_y from relay_c
297///
298#[tokio::test]
299async fn test_recursive_relay_discovery_via_announcements_with_historic_sync() {
300 // 1. Start all three relays
301
302 // relay_b - will be the bootstrap relay, has announcement_x
303 let relay_b = TestRelay::start().await;
304 println!(
305 "relay_b (bootstrap) started at {} (domain: {})",
306 relay_b.url(),
307 relay_b.domain()
308 );
309
310 // relay_c - will be discovered via announcement_x, has announcement_y
311 let relay_c = TestRelay::start().await;
312 println!(
313 "relay_c (to be discovered) started at {} (domain: {})",
314 relay_c.url(),
315 relay_c.domain()
316 );
317
318 // relay_a - SUT, starts with relay_b as bootstrap
319 let relay_a = TestRelay::start_with_sync(Some(relay_b.url().to_string())).await;
320 println!(
321 "relay_a (SUT) started at {} (domain: {})",
322 relay_a.url(),
323 relay_a.domain()
324 );
325
326 // 2. Create test keys (one for each announcement)
327 let keys_x = Keys::generate();
328 let keys_y = Keys::generate();
329
330 // 3. Create announcement_x on relay_b (lists all three relays: A+B+C)
331 let announcement_x = create_repo_announcement(
332 &keys_x,
333 &[&relay_a.domain(), &relay_b.domain(), &relay_c.domain()],
334 "repo-x-all-relays",
335 );
336 let announcement_x_id = announcement_x.id;
337 println!("Created announcement_x {} listing A+B+C", announcement_x_id);
338 for tag in announcement_x.tags.iter() {
339 println!(" Tag: {:?}", tag.as_slice());
340 }
341
342 // 4. Create announcement_y on relay_c (lists only A+C, NOT B)
343 let announcement_y = create_repo_announcement(
344 &keys_y,
345 &[&relay_a.domain(), &relay_c.domain()],
346 "repo-y-ac-only",
347 );
348 let announcement_y_id = announcement_y.id;
349 println!(
350 "Created announcement_y {} listing A+C only",
351 announcement_y_id
352 );
353 for tag in announcement_y.tags.iter() {
354 println!(" Tag: {:?}", tag.as_slice());
355 }
356
357 // 5. Send announcement_x to relay_b only
358 let client_b = TestClient::new(relay_b.url(), keys_x.clone())
359 .await
360 .expect("Failed to connect to relay_b");
361
362 client_b
363 .send_event(&announcement_x)
364 .await
365 .expect("Failed to send announcement_x to relay_b");
366 println!("announcement_x sent to relay_b");
367
368 client_b.disconnect().await;
369
370 // 6. Send announcement_y to relay_c only
371 let client_c = TestClient::new(relay_c.url(), keys_y.clone())
372 .await
373 .expect("Failed to connect to relay_c");
374
375 client_c
376 .send_event(&announcement_y)
377 .await
378 .expect("Failed to send announcement_y to relay_c");
379 println!("announcement_y sent to relay_c");
380
381 client_c.disconnect().await;
382
383 // 7. Wait for relay_a to:
384 // - Sync from bootstrap relay_b (gets announcement_x)
385 // - Discover relay_c from announcement_x's relays tag
386 // - Connect to relay_c and sync announcement_y
387 println!("Waiting 5s for recursive relay discovery...");
388 tokio::time::sleep(Duration::from_secs(5)).await;
389
390 // 8. Verify announcement_x was synced to relay_a (from bootstrap relay_b)
391 let filter_x = Filter::new()
392 .kind(Kind::GitRepoAnnouncement)
393 .author(keys_x.public_key());
394
395 let announcement_x_synced =
396 wait_for_event_on_relay(relay_a.url(), filter_x, Duration::from_secs(5)).await;
397
398 println!(
399 "announcement_x {} synced to relay_a: {}",
400 announcement_x_id, announcement_x_synced
401 );
402
403 // 9. Verify announcement_y was synced to relay_a (from discovered relay_c)
404 let filter_y = Filter::new()
405 .kind(Kind::GitRepoAnnouncement)
406 .author(keys_y.public_key());
407
408 let announcement_y_synced =
409 wait_for_event_on_relay(relay_a.url(), filter_y, Duration::from_secs(5)).await;
410
411 println!(
412 "announcement_y {} synced to relay_a: {}",
413 announcement_y_id, announcement_y_synced
414 );
415
416 // 10. Cleanup
417 relay_a.stop().await;
418 relay_b.stop().await;
419 relay_c.stop().await;
420
421 // 11. Assertions
422 assert!(
423 announcement_x_synced,
424 "announcement_x {} should have synced from bootstrap relay_b to relay_a",
425 announcement_x_id
426 );
427
428 assert!(
429 announcement_y_synced,
430 "announcement_y {} should have synced from discovered relay_c to relay_a (recursive discovery)",
431 announcement_y_id
432 );
433}