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-18 17:12:27 +0000
committerDanConwayDev <DanConwayDev@protonmail.com>2026-02-18 17:12:27 +0000
commit07c8c00274298e90654207d8baceb1089514ccae (patch)
tree931e1c5da2f7c605c50d6644012eed32d5c62b4e /tests/purgatory_sync.rs
parentd76003b629a4a03dba23a8a1c41da6e4ac4c30cf (diff)
test: rewrite PR sync tests to reflect purgatory-first announcement flow
The tests now correctly reflect the actual purgatory behavior: 1. Announcement goes to purgatory (StateOnly) - not immediately accepted 2. State event goes to purgatory 3. Git push promotes announcement to Full and releases state event 4. PR event is sent AFTER announcement promotion (accepted since repo is Full) 5. PR commit push releases PR event from purgatory This matches the design: announcements require git data validation before being promoted to the database, which means PR events can only be accepted for repos with promoted announcements. Also routes relay stdout to /tmp/relay-{port}.log for easier debugging.
Diffstat (limited to 'tests/purgatory_sync.rs')
-rw-r--r--tests/purgatory_sync.rs209
1 files changed, 134 insertions, 75 deletions
diff --git a/tests/purgatory_sync.rs b/tests/purgatory_sync.rs
index 72f3d81..304865d 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