upleb.uk

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

summaryrefslogtreecommitdiff
path: root/tests/purgatory_persistence.rs
diff options
context:
space:
mode:
authorDanConwayDev <DanConwayDev@protonmail.com>2026-02-23 15:08:37 +0000
committerDanConwayDev <DanConwayDev@protonmail.com>2026-02-23 15:08:37 +0000
commit26f608e5011b9d1ad6036da75b89272835e69695 (patch)
tree8b5dfe29f65abe80e59bddbcd3ee09c0a369dba8 /tests/purgatory_persistence.rs
parent4848c4029fc58f6f310a2babeae1ee82a7e41656 (diff)
persist and restore announcement events across graceful restarts
Extends purgatory persistence to include announcement purgatory entries. On graceful shutdown, non-soft-expired announcements are serialised to purgatory-state.json alongside state/PR/expired events; on startup they are restored, skipping any entry whose bare repo path no longer exists. Updates purgatory-design.md to reflect that purgatory persists through graceful shutdown and documents the new PurgatoryState disk format. Adds create_announcement_event helper to purgatory_helpers and three new integration tests in purgatory_persistence covering the full save/restore cycle, missing-repo skip, and the combined roundtrip with all entry types.
Diffstat (limited to 'tests/purgatory_persistence.rs')
-rw-r--r--tests/purgatory_persistence.rs135
1 files changed, 131 insertions, 4 deletions
diff --git a/tests/purgatory_persistence.rs b/tests/purgatory_persistence.rs
index 5abbf15..05cb44b 100644
--- a/tests/purgatory_persistence.rs
+++ b/tests/purgatory_persistence.rs
@@ -31,9 +31,11 @@
31 31
32mod common; 32mod common;
33 33
34use common::purgatory_helpers::create_announcement_event;
34use ngit_grasp::purgatory::Purgatory; 35use ngit_grasp::purgatory::Purgatory;
35use ngit_grasp::sync::rejected_index::{EventType, RejectedEventsIndex, RejectionReason}; 36use ngit_grasp::sync::rejected_index::{EventType, RejectedEventsIndex, RejectionReason};
36use nostr_sdk::prelude::*; 37use nostr_sdk::prelude::*;
38use std::collections::HashSet;
37use std::time::Duration; 39use std::time::Duration;
38 40
39/// Helper to create a test event 41/// Helper to create a test event
@@ -116,12 +118,31 @@ async fn test_full_purgatory_save_restore_cycle() {
116 // Add a PR placeholder (git-data-first scenario) 118 // Add a PR placeholder (git-data-first scenario)
117 purgatory.add_pr_placeholder("placeholder-id".to_string(), "commit-xyz".to_string()); 119 purgatory.add_pr_placeholder("placeholder-id".to_string(), "commit-xyz".to_string());
118 120
119 // Note: We can't directly test expired events without accessing private fields, 121 // Add an announcement to purgatory (requires a real directory for the repo path)
120 // so we'll focus on testing state and PR events persistence 122 let repo_dir = temp_dir.path().join("repo.git");
123 std::fs::create_dir_all(&repo_dir).unwrap();
124 let ann_keys = Keys::generate();
125 let ann_event = create_announcement_event(
126 &ann_keys,
127 "my-repo",
128 &["http://example.com/my-repo.git"],
129 &["wss://relay.example.com"],
130 )
131 .unwrap();
132 let ann_event_id = ann_event.id;
133 let mut ann_relays = HashSet::new();
134 ann_relays.insert("wss://relay.example.com".to_string());
135 purgatory.add_announcement(
136 ann_event,
137 "my-repo".to_string(),
138 ann_keys.public_key(),
139 repo_dir.clone(),
140 ann_relays,
141 );
121 142
122 // Verify initial counts 143 // Verify initial counts
123 let (announcement_count, state_count, pr_count) = purgatory.count(); 144 let (announcement_count, state_count, pr_count) = purgatory.count();
124 assert_eq!(announcement_count, 0, "Should have 0 announcements"); 145 assert_eq!(announcement_count, 1, "Should have 1 announcement");
125 assert_eq!(state_count, 2, "Should have 2 state events"); 146 assert_eq!(state_count, 2, "Should have 2 state events");
126 assert_eq!( 147 assert_eq!(
127 pr_count, 3, 148 pr_count, 3,
@@ -144,13 +165,22 @@ async fn test_full_purgatory_save_restore_cycle() {
144 165
145 // Verify all data was restored 166 // Verify all data was restored
146 let (announcement_count2, state_count2, pr_count2) = purgatory2.count(); 167 let (announcement_count2, state_count2, pr_count2) = purgatory2.count();
147 assert_eq!(announcement_count2, 0, "Should have 0 announcements after restore"); 168 assert_eq!(announcement_count2, 1, "Should have 1 announcement after restore");
148 assert_eq!(state_count2, 2, "Should have 2 state events after restore"); 169 assert_eq!(state_count2, 2, "Should have 2 state events after restore");
149 assert_eq!( 170 assert_eq!(
150 pr_count2, 3, 171 pr_count2, 3,
151 "Should have 3 PR events after restore (2 events + 1 placeholder)" 172 "Should have 3 PR events after restore (2 events + 1 placeholder)"
152 ); 173 );
153 174
175 // Verify announcement was restored correctly
176 let restored_ann = purgatory2
177 .find_announcement(&ann_keys.public_key(), "my-repo")
178 .expect("Announcement should be restored");
179 assert_eq!(restored_ann.event.id, ann_event_id);
180 assert_eq!(restored_ann.identifier, "my-repo");
181 assert_eq!(restored_ann.repo_path, repo_dir);
182 assert!(!restored_ann.soft_expired);
183
154 // Verify specific state events 184 // Verify specific state events
155 let repo1_states = purgatory2.find_state("repo1"); 185 let repo1_states = purgatory2.find_state("repo1");
156 assert_eq!(repo1_states.len(), 1); 186 assert_eq!(repo1_states.len(), 1);
@@ -748,3 +778,100 @@ async fn test_rejected_cache_entries_expired_during_downtime() {
748 assert_eq!(index2.hot_cache_len(), 0); 778 assert_eq!(index2.hot_cache_len(), 0);
749 assert_eq!(index2.cold_index_len(), 1); 779 assert_eq!(index2.cold_index_len(), 1);
750} 780}
781
782/// Test 18: Announcement events are saved and restored across restarts
783#[tokio::test]
784async fn test_announcement_save_restore_cycle() {
785 let temp_dir = tempfile::tempdir().unwrap();
786 let git_data_path = temp_dir.path().join("git");
787 let state_path = temp_dir.path().join("purgatory.json");
788
789 // Create a real bare repo directory (restore skips entries whose path is missing)
790 let repo_dir = temp_dir.path().join("owner.git");
791 std::fs::create_dir_all(&repo_dir).unwrap();
792
793 let purgatory = Purgatory::new(&git_data_path);
794 let keys = Keys::generate();
795
796 let ann_event = create_announcement_event(
797 &keys,
798 "my-repo",
799 &["http://example.com/my-repo.git"],
800 &["wss://relay.example.com"],
801 )
802 .unwrap();
803 let ann_event_id = ann_event.id;
804
805 let mut relays = HashSet::new();
806 relays.insert("wss://relay.example.com".to_string());
807
808 purgatory.add_announcement(
809 ann_event,
810 "my-repo".to_string(),
811 keys.public_key(),
812 repo_dir.clone(),
813 relays.clone(),
814 );
815
816 let (ann_count, _, _) = purgatory.count();
817 assert_eq!(ann_count, 1);
818
819 // Save to disk
820 purgatory.save_to_disk(&state_path).unwrap();
821 assert!(state_path.exists());
822
823 // Restore into a fresh purgatory
824 let purgatory2 = Purgatory::new(&git_data_path);
825 purgatory2.restore_from_disk(&state_path).unwrap();
826
827 assert!(!state_path.exists(), "State file should be deleted after restore");
828
829 let (ann_count2, _, _) = purgatory2.count();
830 assert_eq!(ann_count2, 1, "Announcement should be restored");
831
832 let restored = purgatory2
833 .find_announcement(&keys.public_key(), "my-repo")
834 .expect("Announcement should be findable after restore");
835
836 assert_eq!(restored.event.id, ann_event_id);
837 assert_eq!(restored.identifier, "my-repo");
838 assert_eq!(restored.owner, keys.public_key());
839 assert_eq!(restored.repo_path, repo_dir);
840 assert_eq!(restored.relays, relays);
841 assert!(!restored.soft_expired);
842}
843
844/// Test 19: Announcement with missing repo path is skipped on restore
845#[tokio::test]
846async fn test_announcement_missing_repo_skipped_on_restore() {
847 let temp_dir = tempfile::tempdir().unwrap();
848 let git_data_path = temp_dir.path().join("git");
849 let state_path = temp_dir.path().join("purgatory.json");
850
851 // Point to a path that does NOT exist on disk
852 let missing_repo = temp_dir.path().join("nonexistent.git");
853
854 let purgatory = Purgatory::new(&git_data_path);
855 let keys = Keys::generate();
856
857 let ann_event = create_announcement_event(&keys, "my-repo", &[], &[]).unwrap();
858
859 purgatory.add_announcement(
860 ann_event,
861 "my-repo".to_string(),
862 keys.public_key(),
863 missing_repo,
864 HashSet::new(),
865 );
866
867 purgatory.save_to_disk(&state_path).unwrap();
868
869 let purgatory2 = Purgatory::new(&git_data_path);
870 purgatory2.restore_from_disk(&state_path).unwrap();
871
872 let (ann_count, _, _) = purgatory2.count();
873 assert_eq!(
874 ann_count, 0,
875 "Announcement with missing repo path must be skipped"
876 );
877}