upleb.uk

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

summaryrefslogtreecommitdiff
path: root/tests/purgatory_sync.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/purgatory_sync.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/purgatory_sync.rs')
-rw-r--r--tests/purgatory_sync.rs365
1 files changed, 151 insertions, 214 deletions
diff --git a/tests/purgatory_sync.rs b/tests/purgatory_sync.rs
index 72f3d81..eefd6bc 100644
--- a/tests/purgatory_sync.rs
+++ b/tests/purgatory_sync.rs
@@ -282,15 +282,20 @@ async fn test_state_event_syncs_from_remote() {
282/// Test that a PR event entering purgatory triggers remote commit fetch 282/// Test that a PR event entering purgatory triggers remote commit fetch
283/// and is released once the commit is available. 283/// and is released once the commit is available.
284/// 284///
285/// Scenario: 285/// Flow on source relay:
286/// 1. Start source relay with repository announcement 286/// 1. Send announcement → purgatory (StateOnly - no git data yet)
287/// 2. Create PR event (goes to purgatory - no git data yet) 287/// 2. Send state event → purgatory (refs point to non-existent commits)
288/// 3. Push commit to refs/nostr/<event-id> (authorized by PR event in purgatory) 288/// 3. Push git data → promotes announcement to Full + releases state event
289/// 4. PR event gets released from purgatory on source relay 289/// 4. Send PR event → purgatory (announcement now Full, so PR events accepted)
290/// 5. Start syncing relay 290/// 5. Push PR commit → releases PR event
291/// 6. Syncing relay syncs PR event (goes to purgatory - no local git data) 291///
292/// 7. Syncing relay fetches commit from source's clone URL 292/// Flow on syncing relay:
293/// 8. Verify PR event is released and refs/nostr/<event-id> created on syncing relay 293/// 6. Start syncing relay
294/// 7. Syncs announcement → purgatory (StateOnly)
295/// 8. Syncs state event → purgatory
296/// 9. Fetches git data → promotes announcement (Full) + releases state event
297/// 10. Syncs PR event → purgatory (announcement now Full)
298/// 11. Fetches PR commit → releases PR event
294#[tokio::test] 299#[tokio::test]
295async fn test_pr_event_syncs_from_remote() { 300async fn test_pr_event_syncs_from_remote() {
296 // 1. Start source relay 301 // 1. Start source relay
@@ -313,8 +318,7 @@ async fn test_pr_event_syncs_from_remote() {
313 .to_bech32() 318 .to_bech32()
314 .expect("Failed to get npub"); 319 .expect("Failed to get npub");
315 320
316 // 3. Create and send announcement listing BOTH relays 321 // 3. Create announcement listing BOTH relays
317 // This ensures the syncing relay will accept the PR event when it syncs
318 let announcement = create_repo_announcement( 322 let announcement = create_repo_announcement(
319 &owner_keys, 323 &owner_keys,
320 &[&source_relay.domain(), &syncing_domain], 324 &[&source_relay.domain(), &syncing_domain],
@@ -331,7 +335,7 @@ async fn test_pr_event_syncs_from_remote() {
331 // Wait for connection 335 // Wait for connection
332 tokio::time::sleep(Duration::from_millis(500)).await; 336 tokio::time::sleep(Duration::from_millis(500)).await;
333 337
334 // Send announcement to source relay (creates bare repo) 338 // Step 1: Send announcement to source relay → purgatory (StateOnly)
335 source_client 339 source_client
336 .send_event(&announcement) 340 .send_event(&announcement)
337 .await 341 .await
@@ -339,8 +343,52 @@ async fn test_pr_event_syncs_from_remote() {
339 343
340 tokio::time::sleep(Duration::from_millis(200)).await; 344 tokio::time::sleep(Duration::from_millis(200)).await;
341 345
342 // 4. Create and send PR event BEFORE pushing 346 // Step 2: Create and send state event → purgatory (no git data yet)
343 // The PR event goes to purgatory on source relay, which authorizes the push 347 let clone_urls = [
348 format!(
349 "http://{}/{}/{}.git",
350 source_relay.domain(),
351 npub,
352 identifier
353 ),
354 format!("http://{}/{}/{}.git", syncing_domain, npub, identifier),
355 ];
356 let relay_urls = [
357 source_relay.url().to_string(),
358 format!("ws://{}", syncing_domain),
359 ];
360
361 let state_event = create_state_event(
362 &owner_keys,
363 identifier,
364 &[("main", &commit_hash)],
365 &[],
366 &[&clone_urls[0], &clone_urls[1]],
367 &[&relay_urls[0], &relay_urls[1]],
368 )
369 .expect("Failed to create state event");
370
371 let state_event_id = state_event.id;
372
373 source_client
374 .send_event(&state_event)
375 .await
376 .expect("Failed to send state event to source");
377
378 tokio::time::sleep(Duration::from_millis(200)).await;
379
380 // Step 3: Push git data to source relay
381 // This promotes the announcement from StateOnly to Full AND releases state event
382 push_to_relay(temp_dir.path(), &source_relay.domain(), &npub, identifier)
383 .expect("Push to source should succeed");
384
385 // Wait for state event to be released from purgatory on source relay
386 wait_for_event_served(source_relay.url(), &state_event_id, Duration::from_secs(5))
387 .await
388 .expect("State event should be served on source relay after push");
389
390 // Step 4: Create and send PR event → purgatory
391 // NOW the announcement is promoted (Full), so PR events are accepted
344 let repo_coord = build_repo_coord(&owner_keys, identifier); 392 let repo_coord = build_repo_coord(&owner_keys, identifier);
345 393
346 let pr_event = create_pr_event( 394 let pr_event = create_pr_event(
@@ -367,11 +415,10 @@ async fn test_pr_event_syncs_from_remote() {
367 .await 415 .await
368 .expect("Failed to send PR event to source"); 416 .expect("Failed to send PR event to source");
369 417
370 // Small delay to ensure PR event is processed into purgatory
371 tokio::time::sleep(Duration::from_millis(200)).await; 418 tokio::time::sleep(Duration::from_millis(200)).await;
372 419
373 // 5. Push commit to refs/nostr/<event-id> on source relay 420 // Step 5: Push PR commit to refs/nostr/<event-id> on source relay
374 // The PR event in purgatory authorizes this push 421 // This releases the PR event from purgatory
375 let ref_name = format!("refs/nostr/{}", pr_event_id.to_hex()); 422 let ref_name = format!("refs/nostr/{}", pr_event_id.to_hex());
376 push_ref_to_relay( 423 push_ref_to_relay(
377 temp_dir.path(), 424 temp_dir.path(),
@@ -383,12 +430,12 @@ async fn test_pr_event_syncs_from_remote() {
383 ) 430 )
384 .expect("Push to refs/nostr/<event-id> should succeed"); 431 .expect("Push to refs/nostr/<event-id> should succeed");
385 432
386 // After push, PR event should be released from purgatory on source relay 433 // Wait for PR event to be released from purgatory on source relay
387 wait_for_event_served(source_relay.url(), &pr_event_id, Duration::from_secs(5)) 434 wait_for_event_served(source_relay.url(), &pr_event_id, Duration::from_secs(5))
388 .await 435 .await
389 .expect("PR event should be served on source relay after push"); 436 .expect("PR event should be served on source relay after push");
390 437
391 // 6. Start syncing relay (syncs from source) 438 // Step 6: Start syncing relay (syncs from source)
392 let syncing_relay = TestRelay::start_on_port_with_options( 439 let syncing_relay = TestRelay::start_on_port_with_options(
393 syncing_port, 440 syncing_port,
394 Some(source_relay.url().to_string()), 441 Some(source_relay.url().to_string()),
@@ -401,14 +448,13 @@ async fn test_pr_event_syncs_from_remote() {
401 .await 448 .await
402 .expect("Sync connection should establish"); 449 .expect("Sync connection should establish");
403 450
404 // 7. Wait for PR event to be released on syncing relay 451 // Steps 7-11: Syncing relay syncs events
405 // The sync should: 452 // The sync should:
406 // a) Fetch the announcement and PR event from source relay 453 // a) Sync announcement → purgatory (StateOnly)
407 // b) Accept announcement (creates bare repo structure) 454 // b) Sync state event → purgatory
408 // c) Put PR event in purgatory (commit missing on syncing relay) 455 // c) Fetch git data → promotes announcement (Full) + releases state event
409 // d) Fetch commit from source relay's clone URL 456 // d) Sync PR event → purgatory (announcement now Full)
410 // e) Release the PR event from purgatory 457 // e) Fetch PR commit → releases PR event
411 // f) Create refs/nostr/<event-id> pointing to the commit
412 let found = wait_for_event_served( 458 let found = wait_for_event_served(
413 syncing_relay.url(), 459 syncing_relay.url(),
414 &pr_event_id, 460 &pr_event_id,
@@ -422,7 +468,7 @@ async fn test_pr_event_syncs_from_remote() {
422 found.err() 468 found.err()
423 ); 469 );
424 470
425 // 8. Verify refs/nostr/<event-id> was created on syncing relay 471 // Verify refs/nostr/<event-id> was created on syncing relay
426 let ref_correct = 472 let ref_correct =
427 check_ref_at_commit(&syncing_domain, &npub, identifier, &ref_name, &commit_hash) 473 check_ref_at_commit(&syncing_domain, &npub, identifier, &ref_name, &commit_hash)
428 .await 474 .await
@@ -443,14 +489,20 @@ async fn test_pr_event_syncs_from_remote() {
443/// Test that concurrent state and PR events for the same repository 489/// Test that concurrent state and PR events for the same repository
444/// both sync correctly. 490/// both sync correctly.
445/// 491///
446/// Scenario: 492/// Flow on source relay:
447/// 1. Start source relay with repo containing two commits (main branch + PR commit) 493/// 1. Send announcement → purgatory (StateOnly - no git data yet)
448/// 2. Create and push both commits to source relay 494/// 2. Send state event → purgatory (refs point to non-existent commits)
449/// 3. Send both state event and PR event to source relay 495/// 3. Push git data → promotes announcement to Full + releases state event
450/// 4. Start syncing relay 496/// 4. THEN send PR event → purgatory (announcement now Full, so PR events accepted)
451/// 5. Wait for sync to fetch git data and release both events 497/// 5. Push PR commit → releases PR event
452/// 6. Verify both state event and PR event are served 498///
453/// 7. Verify refs are correct for both (main branch and refs/nostr/<event-id>) 499/// Flow on syncing relay:
500/// 6. Start syncing relay
501/// 7. Syncs announcement → purgatory (StateOnly)
502/// 8. Syncs state event → purgatory
503/// 9. Fetches git data → promotes announcement (Full) + releases state event
504/// 10. Syncs PR event → purgatory (announcement now Full)
505/// 11. Fetches PR commit → releases PR event
454#[tokio::test] 506#[tokio::test]
455async fn test_concurrent_state_and_pr_sync() { 507async fn test_concurrent_state_and_pr_sync() {
456 // 1. Start source relay 508 // 1. Start source relay
@@ -464,15 +516,13 @@ async fn test_concurrent_state_and_pr_sync() {
464 let syncing_domain = format!("127.0.0.1:{}", syncing_port); 516 let syncing_domain = format!("127.0.0.1:{}", syncing_port);
465 517
466 // 2. Create test repository with two commits 518 // 2. Create test repository with two commits
467 // First commit establishes the repo, second commit is used for both state and PR events 519 // First commit establishes the repo (for state event), second commit is for PR
468 let temp_dir = tempfile::tempdir().expect("Failed to create temp dir"); 520 let temp_dir = tempfile::tempdir().expect("Failed to create temp dir");
469 let _first_commit = create_test_repo_with_commit(temp_dir.path(), CommitVariant::StateTest) 521 let _state_commit = create_test_repo_with_commit(temp_dir.path(), CommitVariant::StateTest)
470 .expect("Failed to create test repo"); 522 .expect("Failed to create test repo");
471 523
472 // Add second commit - this becomes HEAD of main and is referenced by both events 524 // Add second commit - this is used for the PR event
473 // In a real scenario, the state event would reference the current branch state, 525 let pr_commit =
474 // and the PR would propose changes (which happen to be the same commit here for simplicity)
475 let head_commit =
476 add_commit_to_repo(temp_dir.path(), CommitVariant::PrTest).expect("Failed to add commit"); 526 add_commit_to_repo(temp_dir.path(), CommitVariant::PrTest).expect("Failed to add commit");
477 527
478 let npub = owner_keys 528 let npub = owner_keys
@@ -480,7 +530,7 @@ async fn test_concurrent_state_and_pr_sync() {
480 .to_bech32() 530 .to_bech32()
481 .expect("Failed to get npub"); 531 .expect("Failed to get npub");
482 532
483 // 3. Create and send announcement listing BOTH relays 533 // 3. Create announcement listing BOTH relays
484 let announcement = create_repo_announcement( 534 let announcement = create_repo_announcement(
485 &owner_keys, 535 &owner_keys,
486 &[&source_relay.domain(), &syncing_domain], 536 &[&source_relay.domain(), &syncing_domain],
@@ -497,7 +547,7 @@ async fn test_concurrent_state_and_pr_sync() {
497 // Wait for connection 547 // Wait for connection
498 tokio::time::sleep(Duration::from_millis(500)).await; 548 tokio::time::sleep(Duration::from_millis(500)).await;
499 549
500 // Send announcement to source relay (creates bare repo) 550 // Step 1: Send announcement to source relay → purgatory (StateOnly)
501 source_client 551 source_client
502 .send_event(&announcement) 552 .send_event(&announcement)
503 .await 553 .await
@@ -505,8 +555,7 @@ async fn test_concurrent_state_and_pr_sync() {
505 555
506 tokio::time::sleep(Duration::from_millis(200)).await; 556 tokio::time::sleep(Duration::from_millis(200)).await;
507 557
508 // 4. Create state event referencing the HEAD commit (pr_commit) 558 // Step 2: Create and send state event → purgatory (no git data yet)
509 // After add_commit_to_repo, main points to pr_commit (which includes state_commit in history)
510 let clone_urls = [ 559 let clone_urls = [
511 format!( 560 format!(
512 "http://{}/{}/{}.git", 561 "http://{}/{}/{}.git",
@@ -521,11 +570,13 @@ async fn test_concurrent_state_and_pr_sync() {
521 format!("ws://{}", syncing_domain), 570 format!("ws://{}", syncing_domain),
522 ]; 571 ];
523 572
524 // State event references main at head_commit (the current HEAD) 573 // State event references main at pr_commit (HEAD after add_commit_to_repo).
574 // push_to_relay uses `git push --all` which pushes main -> pr_commit (HEAD),
575 // so the state event must reference pr_commit for push validation to succeed.
525 let state_event = create_state_event( 576 let state_event = create_state_event(
526 &owner_keys, 577 &owner_keys,
527 identifier, 578 identifier,
528 &[("main", &head_commit)], 579 &[("main", &pr_commit)],
529 &[], 580 &[],
530 &[&clone_urls[0], &clone_urls[1]], 581 &[&clone_urls[0], &clone_urls[1]],
531 &[&relay_urls[0], &relay_urls[1]], 582 &[&relay_urls[0], &relay_urls[1]],
@@ -534,20 +585,31 @@ async fn test_concurrent_state_and_pr_sync() {
534 585
535 let state_event_id = state_event.id; 586 let state_event_id = state_event.id;
536 587
537 // Send state event to source relay (goes to purgatory - no git data yet)
538 source_client 588 source_client
539 .send_event(&state_event) 589 .send_event(&state_event)
540 .await 590 .await
541 .expect("Failed to send state event to source"); 591 .expect("Failed to send state event to source");
542 592
543 // 5. Create PR event referencing the same commit (head_commit) 593 tokio::time::sleep(Duration::from_millis(200)).await;
544 // This simulates a PR that proposes the changes in head_commit 594
595 // Step 3: Push git data to source relay
596 // This promotes the announcement from StateOnly to Full AND releases state event
597 push_to_relay(temp_dir.path(), &source_relay.domain(), &npub, identifier)
598 .expect("Push to source should succeed");
599
600 // Wait for state event to be released from purgatory on source relay
601 wait_for_event_served(source_relay.url(), &state_event_id, Duration::from_secs(5))
602 .await
603 .expect("State event should be served on source relay after push");
604
605 // Step 4: Create and send PR event → purgatory
606 // NOW the announcement is promoted (Full), so PR events are accepted
545 let repo_coord = build_repo_coord(&owner_keys, identifier); 607 let repo_coord = build_repo_coord(&owner_keys, identifier);
546 608
547 let pr_event = create_pr_event( 609 let pr_event = create_pr_event(
548 &pr_author_keys, 610 &pr_author_keys,
549 &repo_coord, 611 &repo_coord,
550 &head_commit, 612 &pr_commit,
551 "Test PR for concurrent sync", 613 "Test PR for concurrent sync",
552 ) 614 )
553 .expect("Failed to create PR event"); 615 .expect("Failed to create PR event");
@@ -570,33 +632,25 @@ async fn test_concurrent_state_and_pr_sync() {
570 632
571 tokio::time::sleep(Duration::from_millis(200)).await; 633 tokio::time::sleep(Duration::from_millis(200)).await;
572 634
573 // 6. Push git data to source relay 635 // Step 5: Push PR commit to refs/nostr/<event-id> on source relay
574 // Push all branches (main contains both commits due to linear history) 636 // This releases the PR event from purgatory
575 push_to_relay(temp_dir.path(), &source_relay.domain(), &npub, identifier)
576 .expect("Push to source should succeed");
577
578 // Also push the PR ref
579 let pr_ref_name = format!("refs/nostr/{}", pr_event_id.to_hex()); 637 let pr_ref_name = format!("refs/nostr/{}", pr_event_id.to_hex());
580 push_ref_to_relay( 638 push_ref_to_relay(
581 temp_dir.path(), 639 temp_dir.path(),
582 &source_relay.domain(), 640 &source_relay.domain(),
583 &npub, 641 &npub,
584 identifier, 642 identifier,
585 &head_commit, 643 &pr_commit,
586 &pr_ref_name, 644 &pr_ref_name,
587 ) 645 )
588 .expect("Push PR ref to source should succeed"); 646 .expect("Push PR ref to source should succeed");
589 647
590 // After push, both events should be released from purgatory on source relay 648 // Wait for PR event to be released from purgatory on source relay
591 wait_for_event_served(source_relay.url(), &state_event_id, Duration::from_secs(5))
592 .await
593 .expect("State event should be served on source relay after push");
594
595 wait_for_event_served(source_relay.url(), &pr_event_id, Duration::from_secs(5)) 649 wait_for_event_served(source_relay.url(), &pr_event_id, Duration::from_secs(5))
596 .await 650 .await
597 .expect("PR event should be served on source relay after push"); 651 .expect("PR event should be served on source relay after push");
598 652
599 // 7. Start syncing relay (syncs from source) 653 // Step 6: Start syncing relay (syncs from source)
600 let syncing_relay = TestRelay::start_on_port_with_options( 654 let syncing_relay = TestRelay::start_on_port_with_options(
601 syncing_port, 655 syncing_port,
602 Some(source_relay.url().to_string()), 656 Some(source_relay.url().to_string()),
@@ -609,8 +663,13 @@ async fn test_concurrent_state_and_pr_sync() {
609 .await 663 .await
610 .expect("Sync connection should establish"); 664 .expect("Sync connection should establish");
611 665
612 // 8. Wait for BOTH events to be released on syncing relay 666 // Steps 7-11: Syncing relay syncs events
613 // The sync should fetch git data and release both events 667 // The sync should:
668 // a) Sync announcement → purgatory (StateOnly)
669 // b) Sync state event → purgatory
670 // c) Fetch git data → promotes announcement (Full) + releases state event
671 // d) Sync PR event → purgatory (announcement now Full)
672 // e) Fetch PR commit → releases PR event
614 let state_found = wait_for_event_served( 673 let state_found = wait_for_event_served(
615 syncing_relay.url(), 674 syncing_relay.url(),
616 &state_event_id, 675 &state_event_id,
@@ -629,18 +688,18 @@ async fn test_concurrent_state_and_pr_sync() {
629 688
630 assert!( 689 assert!(
631 pr_found.is_ok(), 690 pr_found.is_ok(),
632 "PR event should be served after sync fetches git data: {:?}", 691 "PR event should be served after sync fetches commit: {:?}",
633 pr_found.err() 692 pr_found.err()
634 ); 693 );
635 694
636 // 9. Verify refs are correct on syncing relay 695 // Verify refs are correct on syncing relay
637 // Check main branch points to head_commit (the HEAD) 696 // Check main branch points to pr_commit (HEAD after both commits)
638 let main_ref_correct = check_ref_at_commit( 697 let main_ref_correct = check_ref_at_commit(
639 &syncing_domain, 698 &syncing_domain,
640 &npub, 699 &npub,
641 identifier, 700 identifier,
642 "refs/heads/main", 701 "refs/heads/main",
643 &head_commit, 702 &pr_commit, // After push, main points to pr_commit (HEAD)
644 ) 703 )
645 .await 704 .await
646 .expect("Failed to check main ref"); 705 .expect("Failed to check main ref");
@@ -648,24 +707,24 @@ async fn test_concurrent_state_and_pr_sync() {
648 assert!( 707 assert!(
649 main_ref_correct, 708 main_ref_correct,
650 "main branch should point to HEAD commit ({})", 709 "main branch should point to HEAD commit ({})",
651 head_commit 710 pr_commit
652 ); 711 );
653 712
654 // Check refs/nostr/<event-id> points to the same commit 713 // Check refs/nostr/<event-id> points to pr_commit
655 let pr_ref_correct = check_ref_at_commit( 714 let pr_ref_correct = check_ref_at_commit(
656 &syncing_domain, 715 &syncing_domain,
657 &npub, 716 &npub,
658 identifier, 717 identifier,
659 &pr_ref_name, 718 &pr_ref_name,
660 &head_commit, 719 &pr_commit,
661 ) 720 )
662 .await 721 .await
663 .expect("Failed to check PR ref"); 722 .expect("Failed to check PR ref");
664 723
665 assert!( 724 assert!(
666 pr_ref_correct, 725 pr_ref_correct,
667 "refs/nostr/<event-id> should point to commit ({})", 726 "refs/nostr/<event-id> should point to PR commit ({})",
668 head_commit 727 pr_commit
669 ); 728 );
670 729
671 // Cleanup 730 // Cleanup
@@ -921,162 +980,43 @@ async fn test_pr_event_clone_tag_sync_with_partial_oid_aggregation_from_multiple
921 .expect("PR event should be served on mock_relay immediately"); 980 .expect("PR event should be served on mock_relay immediately");
922 981
923 // ======================================================================== 982 // ========================================================================
924 // Step 5: Start syncing_relay WITHOUT bootstrap and publish announcement directly 983 // Step 5: Start syncing_relay with source_grasp as bootstrap
925 // ======================================================================== 984 // ========================================================================
926 985
927 // Start syncing_relay with sync enabled but NO bootstrap relay 986 // Start syncing_relay with source_grasp as bootstrap relay.
928 // This tests relay discovery from announcement's `relays` tag 987 // Negentropy is disabled because MockRelay doesn't support NIP-77, and the
929 // Note: We disable negentropy because MockRelay doesn't support NIP-77, 988 // sync system doesn't properly fall back to REQ+EOSE when negentropy fails.
930 // and the sync system doesn't properly fall back to REQ+EOSE when negentropy fails. 989 //
990 // We do NOT publish the announcement directly to syncing_relay. Instead,
991 // syncing_relay discovers it via the bootstrap connection to source_grasp,
992 // which has the promoted announcement in its database.
931 let syncing_relay = TestRelay::start_on_port_with_options( 993 let syncing_relay = TestRelay::start_on_port_with_options(
932 syncing_port, 994 syncing_port,
933 None, // NO bootstrap - relay discovery via announcement tags 995 Some(source_grasp.url().to_string()), // Bootstrap from source_grasp
934 true, // Disable negentropy - MockRelay doesn't support NIP-77 996 true, // Disable negentropy - MockRelay doesn't support NIP-77
935 ) 997 )
936 .await; 998 .await;
937 999
938 // Publish announcement DIRECTLY to syncing_relay
939 // This triggers relay discovery from the announcement's `relays` tag
940 let syncing_client = Client::new(owner_keys.clone());
941 syncing_client
942 .add_relay(syncing_relay.url())
943 .await
944 .expect("Failed to add syncing_relay");
945 syncing_client.connect().await;
946 tokio::time::sleep(Duration::from_millis(500)).await;
947
948 syncing_client
949 .send_event(&announcement)
950 .await
951 .expect("Failed to send announcement to syncing_relay");
952 tokio::time::sleep(Duration::from_millis(200)).await;
953
954 // Wait for relay discovery and sync connections to establish
955 // syncing_relay should discover source_grasp and mock_relay from announcement's relays tag
956 println!("=== Waiting for sync connections ===");
957 println!("syncing_relay URL: {}", syncing_relay.url());
958 println!("source_grasp URL: {}", source_grasp.url());
959 println!("mock_relay URL: {}", mock_relay.url());
960 println!("git_server URL: {}", git_server.url());
961
962 wait_for_sync_connection(syncing_relay.url(), 2, Duration::from_secs(10))
963 .await
964 .expect(
965 "Sync connections should establish to discovered relays (source_grasp + mock_relay)",
966 );
967 println!("Sync connections established!");
968
969 // Debug: Check metrics to see what relays are connected
970 let metrics_url = syncing_relay
971 .url()
972 .replace("ws://", "http://")
973 .replace("/", "")
974 + "/metrics";
975 println!("Checking metrics at: {}", metrics_url);
976 if let Ok(response) = reqwest::get(&metrics_url).await {
977 if let Ok(metrics) = response.text().await {
978 // Print sync-related metrics
979 for line in metrics.lines() {
980 if line.contains("sync") && !line.starts_with('#') {
981 println!(" {}", line);
982 }
983 }
984 }
985 }
986
987 // Give some time for sync to happen
988 println!("Waiting 10s for events to sync...");
989 tokio::time::sleep(Duration::from_secs(10)).await;
990
991 // Check metrics again after waiting
992 println!("=== Checking metrics after sync wait ===");
993 if let Ok(response) = reqwest::get(&metrics_url).await {
994 if let Ok(metrics) = response.text().await {
995 for line in metrics.lines() {
996 if line.contains("sync") && !line.starts_with('#') {
997 println!(" {}", line);
998 }
999 }
1000 }
1001 }
1002
1003 // Debug: Check if PR event is still on mock_relay
1004 println!("=== Debug: Checking PR event on mock_relay ===");
1005 let pr_on_mock =
1006 wait_for_event_served(mock_relay.url(), &pr_event_id, Duration::from_secs(2)).await;
1007 println!("PR event on mock_relay: {:?}", pr_on_mock.is_ok());
1008 if let Ok(ref pr) = pr_on_mock {
1009 println!("PR event tags:");
1010 for tag in pr.tags.iter() {
1011 println!(" {:?}", tag.as_slice());
1012 }
1013 }
1014
1015 // Debug: Check repo coordinate
1016 let repo_coord = build_repo_coord(&owner_keys, identifier);
1017 println!("Expected repo coordinate: {}", repo_coord);
1018
1019 // Debug: Test if mock_relay responds to tag-based filter (Layer 2 style)
1020 println!("=== Debug: Testing mock_relay tag filter response ===");
1021 let test_client = Client::new(Keys::generate());
1022 test_client
1023 .add_relay(mock_relay.url())
1024 .await
1025 .expect("Failed to add mock_relay");
1026 test_client.connect().await;
1027 tokio::time::sleep(Duration::from_millis(500)).await;
1028
1029 // Build a Layer 2 style filter (by 'a' tag)
1030 let tag_filter =
1031 Filter::new().custom_tag(SingleLetterTag::lowercase(Alphabet::A), repo_coord.as_str());
1032 println!("Tag filter: {:?}", tag_filter);
1033
1034 let tag_results = test_client
1035 .fetch_events(tag_filter, Duration::from_secs(5))
1036 .await;
1037 match tag_results {
1038 Ok(events) => {
1039 println!("Tag filter returned {} events", events.len());
1040 for event in events.iter() {
1041 println!(" Event ID: {}, Kind: {}", event.id, event.kind.as_u16());
1042 }
1043 }
1044 Err(e) => {
1045 println!("Tag filter query failed: {:?}", e);
1046 }
1047 }
1048 test_client.disconnect().await;
1049
1050 // The syncing relay will: 1000 // The syncing relay will:
1051 // 1. Receive announcement directly (creates bare repo) 1001 // 1. Sync promoted announcement from source_grasp via bootstrap connection → purgatory (no local git data)
1052 // 2. Discover source_grasp and mock_relay from announcement's `relays` tag 1002 // 2. EOSE triggers StateOnly subscription → syncs state event from source_grasp → purgatory sync
1053 // 3. Connect to discovered relays 1003 // 3. Purgatory sync fetches commit_a from source_grasp clone URL → announcement + state promoted
1054 // 4. Sync state event from source_grasp → purgatory (no commit_a locally) 1004 // 4. SelfSubscriber sees promoted announcement → upgrades to Full → connects to mock_relay
1055 // 5. Sync PR event from mock_relay → purgatory (no commit_b locally) 1005 // 5. Syncs PR event from mock_relay → purgatory (no commit_b locally)
1056 // 6. Purgatory sync triggers 1006 // 6. Purgatory sync fetches commit_b from git_server via PR clone tag
1057 // 7. Fetches commit_a from source_grasp clone URL (from announcement clone tag) 1007 // 7. PR event promoted → served
1058 // 8. Fetches commit_b from git_server (from PR event's clone tag)
1059 // 9. Both events released when all OIDs available
1060 1008
1061 // ======================================================================== 1009 // ========================================================================
1062 // Step 6: Verify Results 1010 // Step 6: Verify Results
1063 // ======================================================================== 1011 // ========================================================================
1064 1012
1065 println!("=== Step 6: Verify Results ===");
1066 println!("State event ID: {}", state_event_id);
1067 println!("PR event ID: {}", pr_event_id);
1068 println!("commit_a: {}", commit_a);
1069 println!("commit_b: {}", commit_b);
1070
1071 // Wait for state event to be served on syncing_relay 1013 // Wait for state event to be served on syncing_relay
1072 println!("Waiting for state event on syncing_relay...");
1073 let state_found = wait_for_event_served( 1014 let state_found = wait_for_event_served(
1074 syncing_relay.url(), 1015 syncing_relay.url(),
1075 &state_event_id, 1016 &state_event_id,
1076 Duration::from_secs(30), 1017 Duration::from_secs(30),
1077 ) 1018 )
1078 .await; 1019 .await;
1079 println!("State event result: {:?}", state_found);
1080 assert!( 1020 assert!(
1081 state_found.is_ok(), 1021 state_found.is_ok(),
1082 "State event should be served on syncing_relay: {:?}", 1022 "State event should be served on syncing_relay: {:?}",
@@ -1084,10 +1024,8 @@ async fn test_pr_event_clone_tag_sync_with_partial_oid_aggregation_from_multiple
1084 ); 1024 );
1085 1025
1086 // Wait for PR event to be served on syncing_relay 1026 // Wait for PR event to be served on syncing_relay
1087 println!("Waiting for PR event on syncing_relay...");
1088 let pr_found = 1027 let pr_found =
1089 wait_for_event_served(syncing_relay.url(), &pr_event_id, Duration::from_secs(30)).await; 1028 wait_for_event_served(syncing_relay.url(), &pr_event_id, Duration::from_secs(30)).await;
1090 println!("PR event result: {:?}", pr_found);
1091 assert!( 1029 assert!(
1092 pr_found.is_ok(), 1030 pr_found.is_ok(),
1093 "PR event should be served on syncing_relay (fetched commit_b from git_server via PR clone tag): {:?}", 1031 "PR event should be served on syncing_relay (fetched commit_b from git_server via PR clone tag): {:?}",
@@ -1128,7 +1066,6 @@ async fn test_pr_event_clone_tag_sync_with_partial_oid_aggregation_from_multiple
1128 source_client.disconnect().await; 1066 source_client.disconnect().await;
1129 mock_client.disconnect().await; 1067 mock_client.disconnect().await;
1130 pr_client.disconnect().await; 1068 pr_client.disconnect().await;
1131 syncing_client.disconnect().await;
1132 git_server.stop().await; 1069 git_server.stop().await;
1133 mock_relay.stop().await; 1070 mock_relay.stop().await;
1134 syncing_relay.stop().await; 1071 syncing_relay.stop().await;