upleb.uk

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

summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--docs/archive/2026-01-relay-ngit-dev-migration/production-sync-testing.md (renamed from docs/how-to/production-sync-testing.md)0
-rw-r--r--src/config.rs69
-rw-r--r--src/nostr/builder.rs34
-rw-r--r--src/nostr/events.rs16
-rw-r--r--src/nostr/policy/state.rs9
-rw-r--r--src/purgatory/mod.rs94
-rw-r--r--src/sync/mod.rs1
-rw-r--r--tests/purgatory_persistence.rs45
8 files changed, 179 insertions, 89 deletions
diff --git a/docs/how-to/production-sync-testing.md b/docs/archive/2026-01-relay-ngit-dev-migration/production-sync-testing.md
index 3a273a7..3a273a7 100644
--- a/docs/how-to/production-sync-testing.md
+++ b/docs/archive/2026-01-relay-ngit-dev-migration/production-sync-testing.md
diff --git a/src/config.rs b/src/config.rs
index df7a7ef..dd7b1e3 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -109,22 +109,25 @@ impl WhitelistEntry {
109} 109}
110 110
111/// GRASP-05 Archive mode configuration 111/// GRASP-05 Archive mode configuration
112#[derive(Debug, Clone, Serialize, Deserialize)] 112#[derive(Debug, Clone, Serialize, Deserialize, Default)]
113pub struct ArchiveConfig { 113pub struct ArchiveConfig {
114 /// Accept all repository announcements (no filtering) 114 /// Accept all repository announcements (no filtering)
115 /// 115 ///
116 /// WARNING: Setting this to true allows anyone to mirror any repository 116 /// WARNING: Setting this to true allows anyone to mirror any repository
117 /// to this relay, potentially causing storage/bandwidth exhaustion. 117 /// to this relay, potentially causing storage/bandwidth exhaustion.
118 #[serde(default)]
118 pub archive_all: bool, 119 pub archive_all: bool,
119 120
120 /// Whitelist entries for selective archiving 121 /// Whitelist entries for selective archiving
121 /// 122 ///
122 /// If empty and archive_all is false, GRASP-05 is disabled (GRASP-01 strict mode). 123 /// If empty and archive_all is false, GRASP-05 is disabled (GRASP-01 strict mode).
124 #[serde(default)]
123 pub whitelist: Vec<WhitelistEntry>, 125 pub whitelist: Vec<WhitelistEntry>,
124 126
125 /// GRASP server domains to archive (archive all repositories from these domains) 127 /// GRASP server domains to archive (archive all repositories from these domains)
126 /// 128 ///
127 /// If non-empty, archives all repositories from the specified GRASP server domains. 129 /// If non-empty, archives all repositories from the specified GRASP server domains.
130 #[serde(default)]
128 pub grasp_services: Vec<String>, 131 pub grasp_services: Vec<String>,
129 132
130 /// Read-only archive mode: relay is a read-only sync of archived repositories 133 /// Read-only archive mode: relay is a read-only sync of archived repositories
@@ -132,6 +135,7 @@ pub struct ArchiveConfig {
132 /// When true, the relay ONLY accepts announcements matching the archive whitelist/all. 135 /// When true, the relay ONLY accepts announcements matching the archive whitelist/all.
133 /// Announcements listing the relay but not in the whitelist are rejected. 136 /// Announcements listing the relay but not in the whitelist are rejected.
134 /// When false, the relay operates in GRASP-01 mode for unwhitelisted repos. 137 /// When false, the relay operates in GRASP-01 mode for unwhitelisted repos.
138 #[serde(default)]
135 pub read_only: bool, 139 pub read_only: bool,
136} 140}
137 141
@@ -146,6 +150,7 @@ impl ArchiveConfig {
146 /// Returns true if: 150 /// Returns true if:
147 /// - archive_all is true, OR 151 /// - archive_all is true, OR
148 /// - announcement matches any whitelist entry 152 /// - announcement matches any whitelist entry
153 ///
149 /// Note: grasp_services matching is handled via matches_grasp_services() 154 /// Note: grasp_services matching is handled via matches_grasp_services()
150 pub fn matches(&self, npub: &str, identifier: &str) -> bool { 155 pub fn matches(&self, npub: &str, identifier: &str) -> bool {
151 if self.archive_all { 156 if self.archive_all {
@@ -171,23 +176,13 @@ impl ArchiveConfig {
171 } 176 }
172} 177}
173 178
174impl Default for ArchiveConfig {
175 fn default() -> Self {
176 Self {
177 archive_all: false,
178 whitelist: Vec::new(),
179 grasp_services: Vec::new(),
180 read_only: false,
181 }
182 }
183}
184
185/// Repository whitelist configuration 179/// Repository whitelist configuration
186#[derive(Debug, Clone, Serialize, Deserialize)] 180#[derive(Debug, Clone, Serialize, Deserialize, Default)]
187pub struct RepositoryConfig { 181pub struct RepositoryConfig {
188 /// Whitelist entries for selective repository acceptance 182 /// Whitelist entries for selective repository acceptance
189 /// 183 ///
190 /// If empty, all repositories listing the service are accepted (GRASP-01 mode). 184 /// If empty, all repositories listing the service are accepted (GRASP-01 mode).
185 #[serde(default)]
191 pub whitelist: Vec<WhitelistEntry>, 186 pub whitelist: Vec<WhitelistEntry>,
192} 187}
193 188
@@ -207,21 +202,14 @@ impl RepositoryConfig {
207 } 202 }
208} 203}
209 204
210impl Default for RepositoryConfig {
211 fn default() -> Self {
212 Self {
213 whitelist: Vec::new(),
214 }
215 }
216}
217
218/// Repository blacklist configuration 205/// Repository blacklist configuration
219#[derive(Debug, Clone, Serialize, Deserialize)] 206#[derive(Debug, Clone, Serialize, Deserialize, Default)]
220pub struct BlacklistConfig { 207pub struct BlacklistConfig {
221 /// Blacklist entries for blocking specific repositories 208 /// Blacklist entries for blocking specific repositories
222 /// 209 ///
223 /// If empty, no repositories are blacklisted. 210 /// If empty, no repositories are blacklisted.
224 /// Blacklist takes precedence over both archive and repository whitelists. 211 /// Blacklist takes precedence over both archive and repository whitelists.
212 #[serde(default)]
225 pub blacklist: Vec<WhitelistEntry>, 213 pub blacklist: Vec<WhitelistEntry>,
226} 214}
227 215
@@ -256,21 +244,14 @@ impl BlacklistConfig {
256 } 244 }
257} 245}
258 246
259impl Default for BlacklistConfig {
260 fn default() -> Self {
261 Self {
262 blacklist: Vec::new(),
263 }
264 }
265}
266
267/// Event blacklist configuration for blocking events by author npub 247/// Event blacklist configuration for blocking events by author npub
268#[derive(Debug, Clone, Serialize, Deserialize)] 248#[derive(Debug, Clone, Serialize, Deserialize, Default)]
269pub struct EventBlacklistConfig { 249pub struct EventBlacklistConfig {
270 /// Blacklisted npubs - events from these authors are rejected 250 /// Blacklisted npubs - events from these authors are rejected
271 /// 251 ///
272 /// If empty, no events are blacklisted by author. 252 /// If empty, no events are blacklisted by author.
273 /// Applies to ALL event types, preventing events from reaching both the relay and purgatory. 253 /// Applies to ALL event types, preventing events from reaching both the relay and purgatory.
254 #[serde(default)]
274 pub blacklisted_npubs: Vec<String>, 255 pub blacklisted_npubs: Vec<String>,
275} 256}
276 257
@@ -292,14 +273,6 @@ impl EventBlacklistConfig {
292 } 273 }
293} 274}
294 275
295impl Default for EventBlacklistConfig {
296 fn default() -> Self {
297 Self {
298 blacklisted_npubs: Vec::new(),
299 }
300 }
301}
302
303/// Database backend type for the relay 276/// Database backend type for the relay
304#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default, ValueEnum)] 277#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default, ValueEnum)]
305#[serde(rename_all = "lowercase")] 278#[serde(rename_all = "lowercase")]
@@ -1108,14 +1081,14 @@ mod tests {
1108 fn test_archive_read_only_defaults() { 1081 fn test_archive_read_only_defaults() {
1109 // Default: false when no archive mode 1082 // Default: false when no archive mode
1110 let config = Config::for_testing(); 1083 let config = Config::for_testing();
1111 assert_eq!(config.archive_config().read_only, false); 1084 assert!(!config.archive_config().read_only);
1112 1085
1113 // Default: true when archive_all is set 1086 // Default: true when archive_all is set
1114 let config = Config { 1087 let config = Config {
1115 archive_all: true, 1088 archive_all: true,
1116 ..Config::for_testing() 1089 ..Config::for_testing()
1117 }; 1090 };
1118 assert_eq!(config.archive_config().read_only, true); 1091 assert!(config.archive_config().read_only);
1119 1092
1120 // Default: true when archive_whitelist is set 1093 // Default: true when archive_whitelist is set
1121 let keys = Keys::generate(); 1094 let keys = Keys::generate();
@@ -1124,7 +1097,7 @@ mod tests {
1124 archive_whitelist: test_npub, 1097 archive_whitelist: test_npub,
1125 ..Config::for_testing() 1098 ..Config::for_testing()
1126 }; 1099 };
1127 assert_eq!(config.archive_config().read_only, true); 1100 assert!(config.archive_config().read_only);
1128 } 1101 }
1129 1102
1130 #[test] 1103 #[test]
@@ -1135,7 +1108,7 @@ mod tests {
1135 archive_read_only: Some(true), 1108 archive_read_only: Some(true),
1136 ..Config::for_testing() 1109 ..Config::for_testing()
1137 }; 1110 };
1138 assert_eq!(config.archive_config().read_only, true); 1111 assert!(config.archive_config().read_only);
1139 1112
1140 // Explicit false with archive_all (unusual but allowed) 1113 // Explicit false with archive_all (unusual but allowed)
1141 let config = Config { 1114 let config = Config {
@@ -1143,14 +1116,14 @@ mod tests {
1143 archive_read_only: Some(false), 1116 archive_read_only: Some(false),
1144 ..Config::for_testing() 1117 ..Config::for_testing()
1145 }; 1118 };
1146 assert_eq!(config.archive_config().read_only, false); 1119 assert!(!config.archive_config().read_only);
1147 1120
1148 // Explicit false without archive mode 1121 // Explicit false without archive mode
1149 let config = Config { 1122 let config = Config {
1150 archive_read_only: Some(false), 1123 archive_read_only: Some(false),
1151 ..Config::for_testing() 1124 ..Config::for_testing()
1152 }; 1125 };
1153 assert_eq!(config.archive_config().read_only, false); 1126 assert!(!config.archive_config().read_only);
1154 } 1127 }
1155 1128
1156 #[test] 1129 #[test]
@@ -1553,7 +1526,7 @@ mod tests {
1553 }; 1526 };
1554 let archive_config = config.archive_config(); 1527 let archive_config = config.archive_config();
1555 assert!(archive_config.enabled()); 1528 assert!(archive_config.enabled());
1556 assert_eq!(archive_config.read_only, true); // Default to true 1529 assert!(archive_config.read_only); // Default to true
1557 } 1530 }
1558 1531
1559 #[test] 1532 #[test]
@@ -1563,7 +1536,7 @@ mod tests {
1563 archive_grasp_services: "git.example.com".to_string(), 1536 archive_grasp_services: "git.example.com".to_string(),
1564 ..Config::for_testing() 1537 ..Config::for_testing()
1565 }; 1538 };
1566 assert_eq!(config.archive_config().read_only, true); 1539 assert!(config.archive_config().read_only);
1567 } 1540 }
1568 1541
1569 #[test] 1542 #[test]
@@ -1574,7 +1547,7 @@ mod tests {
1574 archive_read_only: Some(false), 1547 archive_read_only: Some(false),
1575 ..Config::for_testing() 1548 ..Config::for_testing()
1576 }; 1549 };
1577 assert_eq!(config.archive_config().read_only, false); 1550 assert!(!config.archive_config().read_only);
1578 } 1551 }
1579 1552
1580 #[test] 1553 #[test]
diff --git a/src/nostr/builder.rs b/src/nostr/builder.rs
index 9211972..3baa2ff 100644
--- a/src/nostr/builder.rs
+++ b/src/nostr/builder.rs
@@ -185,7 +185,10 @@ impl Nip34WritePolicy {
185 WritePolicyResult::Accept 185 WritePolicyResult::Accept
186 } 186 }
187 Err(e) => { 187 Err(e) => {
188 let npub = event.pubkey.to_bech32().unwrap_or_else(|_| event.pubkey.to_hex()); 188 let npub = event
189 .pubkey
190 .to_bech32()
191 .unwrap_or_else(|_| event.pubkey.to_hex());
189 let event_id_short = &event.id.to_hex()[..12]; 192 let event_id_short = &event.id.to_hex()[..12];
190 // Try to extract repo identifier from 'd' tag even if parsing failed 193 // Try to extract repo identifier from 'd' tag even if parsing failed
191 let repo = Self::extract_identifier_from_event(event); 194 let repo = Self::extract_identifier_from_event(event);
@@ -221,7 +224,10 @@ impl Nip34WritePolicy {
221 WritePolicyResult::Accept 224 WritePolicyResult::Accept
222 } 225 }
223 Err(e) => { 226 Err(e) => {
224 let npub = event.pubkey.to_bech32().unwrap_or_else(|_| event.pubkey.to_hex()); 227 let npub = event
228 .pubkey
229 .to_bech32()
230 .unwrap_or_else(|_| event.pubkey.to_hex());
225 let event_id_short = &event.id.to_hex()[..12]; 231 let event_id_short = &event.id.to_hex()[..12];
226 // Try to extract repo identifier from 'd' tag even if parsing failed 232 // Try to extract repo identifier from 'd' tag even if parsing failed
227 let repo = Self::extract_identifier_from_event(event); 233 let repo = Self::extract_identifier_from_event(event);
@@ -265,7 +271,10 @@ impl Nip34WritePolicy {
265 { 271 {
266 Ok(poilicy_result) => poilicy_result, 272 Ok(poilicy_result) => poilicy_result,
267 Err(e) => { 273 Err(e) => {
268 let npub = event.pubkey.to_bech32().unwrap_or_else(|_| event.pubkey.to_hex()); 274 let npub = event
275 .pubkey
276 .to_bech32()
277 .unwrap_or_else(|_| event.pubkey.to_hex());
269 let event_id_short = &event.id.to_hex()[..12]; 278 let event_id_short = &event.id.to_hex()[..12];
270 // Try to extract repo identifier from 'd' tag even if parsing failed 279 // Try to extract repo identifier from 'd' tag even if parsing failed
271 let repo = Self::extract_identifier_from_event(event); 280 let repo = Self::extract_identifier_from_event(event);
@@ -287,7 +296,10 @@ impl Nip34WritePolicy {
287 } 296 }
288 } 297 }
289 StateResult::Reject(reason) => { 298 StateResult::Reject(reason) => {
290 let npub = event.pubkey.to_bech32().unwrap_or_else(|_| event.pubkey.to_hex()); 299 let npub = event
300 .pubkey
301 .to_bech32()
302 .unwrap_or_else(|_| event.pubkey.to_hex());
291 let event_id_short = &event.id.to_hex()[..12]; 303 let event_id_short = &event.id.to_hex()[..12];
292 // Try to extract repo identifier from 'd' tag even if parsing failed 304 // Try to extract repo identifier from 'd' tag even if parsing failed
293 let repo = Self::extract_identifier_from_event(event); 305 let repo = Self::extract_identifier_from_event(event);
@@ -397,9 +409,12 @@ impl Nip34WritePolicy {
397 ); 409 );
398 410
399 // Add to purgatory 411 // Add to purgatory
400 self.ctx 412 self.ctx.purgatory.add_pr(
401 .purgatory 413 event.clone(),
402 .add_pr(event.clone(), event.id.to_hex(), commit.clone(), is_synced); 414 event.id.to_hex(),
415 commit.clone(),
416 is_synced,
417 );
403 418
404 WritePolicyResult::Reject { 419 WritePolicyResult::Reject {
405 status: true, // Client sees OK 420 status: true, // Client sees OK
@@ -417,7 +432,10 @@ impl Nip34WritePolicy {
417 } 432 }
418 Err(e) => { 433 Err(e) => {
419 // Error checking git data - reject event 434 // Error checking git data - reject event
420 let npub = event.pubkey.to_bech32().unwrap_or_else(|_| event.pubkey.to_hex()); 435 let npub = event
436 .pubkey
437 .to_bech32()
438 .unwrap_or_else(|_| event.pubkey.to_hex());
421 let event_id_short = &event.id.to_hex()[..12]; 439 let event_id_short = &event.id.to_hex()[..12];
422 // Extract ALL repo identifiers from 'a' tags for PR events 440 // Extract ALL repo identifiers from 'a' tags for PR events
423 // (PR events can reference multiple repos when there are multiple maintainers) 441 // (PR events can reference multiple repos when there are multiple maintainers)
diff --git a/src/nostr/events.rs b/src/nostr/events.rs
index 718633e..a441742 100644
--- a/src/nostr/events.rs
+++ b/src/nostr/events.rs
@@ -419,14 +419,14 @@ pub fn validate_announcement(
419 // GRASP-01: Normal mode - accept if announcement lists our service AND matches repository whitelist (if enabled) 419 // GRASP-01: Normal mode - accept if announcement lists our service AND matches repository whitelist (if enabled)
420 if lists_service && !archive_config.read_only { 420 if lists_service && !archive_config.read_only {
421 // Check repository whitelist if enabled 421 // Check repository whitelist if enabled
422 if repository_config.enabled() { 422 if repository_config.enabled()
423 if !repository_config.matches(&npub, &announcement.identifier) { 423 && !repository_config.matches(&npub, &announcement.identifier)
424 return AnnouncementResult::Reject(format!( 424 {
425 "Announcement lists service but does not match repository whitelist. \ 425 return AnnouncementResult::Reject(format!(
426 Repository {}/{} not in whitelist", 426 "Announcement lists service but does not match repository whitelist. \
427 npub, announcement.identifier 427 Repository {}/{} not in whitelist",
428 )); 428 npub, announcement.identifier
429 } 429 ));
430 } 430 }
431 return AnnouncementResult::Accept; 431 return AnnouncementResult::Accept;
432 } 432 }
diff --git a/src/nostr/policy/state.rs b/src/nostr/policy/state.rs
index 52f0483..3411077 100644
--- a/src/nostr/policy/state.rs
+++ b/src/nostr/policy/state.rs
@@ -205,9 +205,12 @@ impl StatePolicy {
205 205
206 // If no git data - add to purgatory 206 // If no git data - add to purgatory
207 // (add_state automatically enqueues for background sync) 207 // (add_state automatically enqueues for background sync)
208 self.ctx 208 self.ctx.purgatory.add_state(
209 .purgatory 209 event.clone(),
210 .add_state(event.clone(), state.identifier.clone(), event.pubkey, is_synced); 210 state.identifier.clone(),
211 event.pubkey,
212 is_synced,
213 );
211 214
212 tracing::info!( 215 tracing::info!(
213 "state event added to purgatory: eventid: {}, identifier: {}", 216 "state event added to purgatory: eventid: {}, identifier: {}",
diff --git a/src/purgatory/mod.rs b/src/purgatory/mod.rs
index d442ad8..8094450 100644
--- a/src/purgatory/mod.rs
+++ b/src/purgatory/mod.rs
@@ -740,7 +740,10 @@ impl Purgatory {
740 for (event_id_str, event_opt, commit, source) in expired_prs { 740 for (event_id_str, event_opt, commit, source) in expired_prs {
741 // Log structured entry for PR events (not placeholders) 741 // Log structured entry for PR events (not placeholders)
742 if let Some(ref event) = event_opt { 742 if let Some(ref event) = event_opt {
743 let npub = event.pubkey.to_bech32().unwrap_or_else(|_| event.pubkey.to_hex()); 743 let npub = event
744 .pubkey
745 .to_bech32()
746 .unwrap_or_else(|_| event.pubkey.to_hex());
744 let event_id_short = &event.id.to_hex()[..12]; 747 let event_id_short = &event.id.to_hex()[..12];
745 let source_str = if source.is_direct() { "direct" } else { "sync" }; 748 let source_str = if source.is_direct() { "direct" } else { "sync" };
746 749
@@ -751,7 +754,10 @@ impl Purgatory {
751 .iter() 754 .iter()
752 .filter_map(|tag| { 755 .filter_map(|tag| {
753 let tag_vec = tag.clone().to_vec(); 756 let tag_vec = tag.clone().to_vec();
754 if tag_vec.len() >= 2 && tag_vec[0] == "a" && tag_vec[1].starts_with("30617:") { 757 if tag_vec.len() >= 2
758 && tag_vec[0] == "a"
759 && tag_vec[1].starts_with("30617:")
760 {
755 // Format: 30617:<owner_pubkey>:<identifier> 761 // Format: 30617:<owner_pubkey>:<identifier>
756 let parts: Vec<&str> = tag_vec[1].split(':').collect(); 762 let parts: Vec<&str> = tag_vec[1].split(':').collect();
757 if parts.len() >= 3 { 763 if parts.len() >= 3 {
@@ -1171,8 +1177,18 @@ mod tests {
1171 .sign_with_keys(&keys) 1177 .sign_with_keys(&keys)
1172 .unwrap(); 1178 .unwrap();
1173 1179
1174 purgatory.add_state(event.clone(), "test-repo".to_string(), keys.public_key(), false); 1180 purgatory.add_state(
1175 purgatory.add_pr(event, "test-event-id".to_string(), "abc123".to_string(), false); 1181 event.clone(),
1182 "test-repo".to_string(),
1183 keys.public_key(),
1184 false,
1185 );
1186 purgatory.add_pr(
1187 event,
1188 "test-event-id".to_string(),
1189 "abc123".to_string(),
1190 false,
1191 );
1176 1192
1177 let (state_count, pr_count) = purgatory.count(); 1193 let (state_count, pr_count) = purgatory.count();
1178 assert_eq!(state_count, 1); 1194 assert_eq!(state_count, 1);
@@ -1253,7 +1269,12 @@ mod tests {
1253 .sign_with_keys(&keys) 1269 .sign_with_keys(&keys)
1254 .unwrap(); 1270 .unwrap();
1255 1271
1256 purgatory.add_pr(event, "pr-event-id".to_string(), "commit123".to_string(), false); 1272 purgatory.add_pr(
1273 event,
1274 "pr-event-id".to_string(),
1275 "commit123".to_string(),
1276 false,
1277 );
1257 1278
1258 // Now should have pending events for test-repo 1279 // Now should have pending events for test-repo
1259 assert!(purgatory.has_pending_events("test-repo")); 1280 assert!(purgatory.has_pending_events("test-repo"));
@@ -1377,7 +1398,12 @@ fn test_cleanup_removes_expired_entries() {
1377 keys.public_key(), 1398 keys.public_key(),
1378 false, 1399 false,
1379 ); 1400 );
1380 purgatory.add_pr(pr_event, "pr-123".to_string(), "commit-abc".to_string(), false); 1401 purgatory.add_pr(
1402 pr_event,
1403 "pr-123".to_string(),
1404 "commit-abc".to_string(),
1405 false,
1406 );
1381 purgatory.add_pr_placeholder("pr-456".to_string(), "commit-def".to_string()); 1407 purgatory.add_pr_placeholder("pr-456".to_string(), "commit-def".to_string());
1382 1408
1383 // Verify entries are there 1409 // Verify entries are there
@@ -1424,8 +1450,18 @@ fn test_cleanup_preserves_non_expired_entries() {
1424 .unwrap(); 1450 .unwrap();
1425 1451
1426 // Add fresh entries 1452 // Add fresh entries
1427 purgatory.add_state(state_event, "test-repo".to_string(), keys.public_key(), false); 1453 purgatory.add_state(
1428 purgatory.add_pr(pr_event, "pr-123".to_string(), "commit-abc".to_string(), false); 1454 state_event,
1455 "test-repo".to_string(),
1456 keys.public_key(),
1457 false,
1458 );
1459 purgatory.add_pr(
1460 pr_event,
1461 "pr-123".to_string(),
1462 "commit-abc".to_string(),
1463 false,
1464 );
1429 1465
1430 // Run cleanup 1466 // Run cleanup
1431 let (state_removed, pr_removed) = purgatory.cleanup(); 1467 let (state_removed, pr_removed) = purgatory.cleanup();
@@ -1757,8 +1793,18 @@ async fn test_save_and_restore_state_events() {
1757 let event1_id = event1.id; 1793 let event1_id = event1.id;
1758 let event2_id = event2.id; 1794 let event2_id = event2.id;
1759 1795
1760 purgatory.add_state(event1.clone(), "test-repo".to_string(), keys.public_key(), false); 1796 purgatory.add_state(
1761 purgatory.add_state(event2.clone(), "test-repo".to_string(), keys.public_key(), false); 1797 event1.clone(),
1798 "test-repo".to_string(),
1799 keys.public_key(),
1800 false,
1801 );
1802 purgatory.add_state(
1803 event2.clone(),
1804 "test-repo".to_string(),
1805 keys.public_key(),
1806 false,
1807 );
1762 1808
1763 // Save to disk 1809 // Save to disk
1764 purgatory.save_to_disk(&state_file).unwrap(); 1810 purgatory.save_to_disk(&state_file).unwrap();
@@ -2283,8 +2329,18 @@ async fn test_comprehensive_roundtrip() {
2283 .sign_with_keys(&keys2) 2329 .sign_with_keys(&keys2)
2284 .unwrap(); 2330 .unwrap();
2285 2331
2286 purgatory.add_state(state1.clone(), "repo1".to_string(), keys1.public_key(), false); 2332 purgatory.add_state(
2287 purgatory.add_state(state2.clone(), "repo2".to_string(), keys2.public_key(), false); 2333 state1.clone(),
2334 "repo1".to_string(),
2335 keys1.public_key(),
2336 false,
2337 );
2338 purgatory.add_state(
2339 state2.clone(),
2340 "repo2".to_string(),
2341 keys2.public_key(),
2342 false,
2343 );
2288 2344
2289 // Add PR event 2345 // Add PR event
2290 let tags = vec![Tag::custom( 2346 let tags = vec![Tag::custom(
@@ -2295,7 +2351,12 @@ async fn test_comprehensive_roundtrip() {
2295 .tags(tags) 2351 .tags(tags)
2296 .sign_with_keys(&keys1) 2352 .sign_with_keys(&keys1)
2297 .unwrap(); 2353 .unwrap();
2298 purgatory.add_pr(pr_event.clone(), "pr-1".to_string(), "commit-1".to_string(), false); 2354 purgatory.add_pr(
2355 pr_event.clone(),
2356 "pr-1".to_string(),
2357 "commit-1".to_string(),
2358 false,
2359 );
2299 2360
2300 // Add PR placeholder 2361 // Add PR placeholder
2301 purgatory.add_pr_placeholder("pr-2".to_string(), "commit-2".to_string()); 2362 purgatory.add_pr_placeholder("pr-2".to_string(), "commit-2".to_string());
@@ -2305,7 +2366,12 @@ async fn test_comprehensive_roundtrip() {
2305 .sign_with_keys(&keys1) 2366 .sign_with_keys(&keys1)
2306 .unwrap(); 2367 .unwrap();
2307 let expired_id = expired_event.id; 2368 let expired_id = expired_event.id;
2308 purgatory.add_state(expired_event, "repo3".to_string(), keys1.public_key(), false); 2369 purgatory.add_state(
2370 expired_event,
2371 "repo3".to_string(),
2372 keys1.public_key(),
2373 false,
2374 );
2309 if let Some(mut entries) = purgatory.state_events.get_mut("repo3") { 2375 if let Some(mut entries) = purgatory.state_events.get_mut("repo3") {
2310 for entry in entries.iter_mut() { 2376 for entry in entries.iter_mut() {
2311 entry.expires_at = Instant::now() - Duration::from_secs(1); 2377 entry.expires_at = Instant::now() - Duration::from_secs(1);
diff --git a/src/sync/mod.rs b/src/sync/mod.rs
index a0dfa59..d6634ff 100644
--- a/src/sync/mod.rs
+++ b/src/sync/mod.rs
@@ -584,6 +584,7 @@ impl SyncManager {
584 /// * `config` - Configuration for sync settings 584 /// * `config` - Configuration for sync settings
585 /// * `data_path` - Path to git data directory (for persistence) 585 /// * `data_path` - Path to git data directory (for persistence)
586 /// * `sync_metrics` - Optional pre-registered SyncMetrics (passed from Metrics if metrics are enabled) 586 /// * `sync_metrics` - Optional pre-registered SyncMetrics (passed from Metrics if metrics are enabled)
587 #[allow(clippy::too_many_arguments)]
587 pub fn new( 588 pub fn new(
588 bootstrap_relay_url: Option<String>, 589 bootstrap_relay_url: Option<String>,
589 service_domain: String, 590 service_domain: String,
diff --git a/tests/purgatory_persistence.rs b/tests/purgatory_persistence.rs
index fe37c33..4dc5e94 100644
--- a/tests/purgatory_persistence.rs
+++ b/tests/purgatory_persistence.rs
@@ -94,11 +94,13 @@ async fn test_full_purgatory_save_restore_cycle() {
94 state_event1.clone(), 94 state_event1.clone(),
95 "repo1".to_string(), 95 "repo1".to_string(),
96 keys1.public_key(), 96 keys1.public_key(),
97 false,
97 ); 98 );
98 purgatory.add_state( 99 purgatory.add_state(
99 state_event2.clone(), 100 state_event2.clone(),
100 "repo2".to_string(), 101 "repo2".to_string(),
101 keys2.public_key(), 102 keys2.public_key(),
103 false,
102 ); 104 );
103 105
104 // Add PR events to purgatory 106 // Add PR events to purgatory
@@ -106,11 +108,13 @@ async fn test_full_purgatory_save_restore_cycle() {
106 pr_event1.clone(), 108 pr_event1.clone(),
107 pr_event1.id.to_hex(), 109 pr_event1.id.to_hex(),
108 "commit-abc".to_string(), 110 "commit-abc".to_string(),
111 false,
109 ); 112 );
110 purgatory.add_pr( 113 purgatory.add_pr(
111 pr_event2.clone(), 114 pr_event2.clone(),
112 pr_event2.id.to_hex(), 115 pr_event2.id.to_hex(),
113 "commit-def".to_string(), 116 "commit-def".to_string(),
117 false,
114 ); 118 );
115 119
116 // Add a PR placeholder (git-data-first scenario) 120 // Add a PR placeholder (git-data-first scenario)
@@ -262,7 +266,12 @@ async fn test_purgatory_downtime_adjustment() {
262 266
263 let state_event = create_state_event_with_refs(&keys, "repo1", &[("main", "abc123")]).unwrap(); 267 let state_event = create_state_event_with_refs(&keys, "repo1", &[("main", "abc123")]).unwrap();
264 268
265 purgatory.add_state(state_event.clone(), "repo1".to_string(), keys.public_key()); 269 purgatory.add_state(
270 state_event.clone(),
271 "repo1".to_string(),
272 keys.public_key(),
273 false,
274 );
266 275
267 // Save to disk 276 // Save to disk
268 purgatory.save_to_disk(&state_path).unwrap(); 277 purgatory.save_to_disk(&state_path).unwrap();
@@ -340,7 +349,7 @@ async fn test_purgatory_file_cleanup_after_restore() {
340 349
341 let state_event = create_state_event_with_refs(&keys, "repo1", &[("main", "abc123")]).unwrap(); 350 let state_event = create_state_event_with_refs(&keys, "repo1", &[("main", "abc123")]).unwrap();
342 351
343 purgatory.add_state(state_event, "repo1".to_string(), keys.public_key()); 352 purgatory.add_state(state_event, "repo1".to_string(), keys.public_key(), false);
344 353
345 // Save to disk 354 // Save to disk
346 purgatory.save_to_disk(&state_path).unwrap(); 355 purgatory.save_to_disk(&state_path).unwrap();
@@ -408,7 +417,7 @@ async fn test_purgatory_restore_missing_file() {
408 // Should be able to add events normally 417 // Should be able to add events normally
409 let keys = Keys::generate(); 418 let keys = Keys::generate();
410 let event = create_test_event(&keys, "test").await; 419 let event = create_test_event(&keys, "test").await;
411 purgatory.add_state(event, "repo1".to_string(), keys.public_key()); 420 purgatory.add_state(event, "repo1".to_string(), keys.public_key(), false);
412 421
413 let (state_count, _) = purgatory.count(); 422 let (state_count, _) = purgatory.count();
414 assert_eq!(state_count, 1); 423 assert_eq!(state_count, 1);
@@ -547,8 +556,18 @@ async fn test_purgatory_multiple_state_events_same_identifier() {
547 let event1 = create_state_event_with_refs(&keys1, "repo1", &[("main", "abc123")]).unwrap(); 556 let event1 = create_state_event_with_refs(&keys1, "repo1", &[("main", "abc123")]).unwrap();
548 let event2 = create_state_event_with_refs(&keys2, "repo1", &[("main", "def456")]).unwrap(); 557 let event2 = create_state_event_with_refs(&keys2, "repo1", &[("main", "def456")]).unwrap();
549 558
550 purgatory.add_state(event1.clone(), "repo1".to_string(), keys1.public_key()); 559 purgatory.add_state(
551 purgatory.add_state(event2.clone(), "repo1".to_string(), keys2.public_key()); 560 event1.clone(),
561 "repo1".to_string(),
562 keys1.public_key(),
563 false,
564 );
565 purgatory.add_state(
566 event2.clone(),
567 "repo1".to_string(),
568 keys2.public_key(),
569 false,
570 );
552 571
553 // Save and restore 572 // Save and restore
554 purgatory.save_to_disk(&state_path).unwrap(); 573 purgatory.save_to_disk(&state_path).unwrap();
@@ -577,7 +596,12 @@ async fn test_purgatory_continues_working_after_restore() {
577 596
578 let event1 = create_state_event_with_refs(&keys, "repo1", &[("main", "abc123")]).unwrap(); 597 let event1 = create_state_event_with_refs(&keys, "repo1", &[("main", "abc123")]).unwrap();
579 598
580 purgatory.add_state(event1.clone(), "repo1".to_string(), keys.public_key()); 599 purgatory.add_state(
600 event1.clone(),
601 "repo1".to_string(),
602 keys.public_key(),
603 false,
604 );
581 605
582 // Save and restore 606 // Save and restore
583 purgatory.save_to_disk(&state_path).unwrap(); 607 purgatory.save_to_disk(&state_path).unwrap();
@@ -588,7 +612,12 @@ async fn test_purgatory_continues_working_after_restore() {
588 // Add new events after restore 612 // Add new events after restore
589 let event2 = create_state_event_with_refs(&keys, "repo2", &[("main", "xyz789")]).unwrap(); 613 let event2 = create_state_event_with_refs(&keys, "repo2", &[("main", "xyz789")]).unwrap();
590 614
591 purgatory2.add_state(event2.clone(), "repo2".to_string(), keys.public_key()); 615 purgatory2.add_state(
616 event2.clone(),
617 "repo2".to_string(),
618 keys.public_key(),
619 false,
620 );
592 621
593 // Verify both old and new events work 622 // Verify both old and new events work
594 let (state_count, _) = purgatory2.count(); 623 let (state_count, _) = purgatory2.count();
@@ -669,7 +698,7 @@ async fn test_purgatory_entries_expired_during_downtime() {
669 698
670 let event = create_state_event_with_refs(&keys, "repo1", &[("main", "abc123")]).unwrap(); 699 let event = create_state_event_with_refs(&keys, "repo1", &[("main", "abc123")]).unwrap();
671 700
672 purgatory.add_state(event.clone(), "repo1".to_string(), keys.public_key()); 701 purgatory.add_state(event.clone(), "repo1".to_string(), keys.public_key(), false);
673 702
674 // Save to disk 703 // Save to disk
675 purgatory.save_to_disk(&state_path).unwrap(); 704 purgatory.save_to_disk(&state_path).unwrap();