upleb.uk

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

summaryrefslogtreecommitdiff
path: root/src/purgatory/mod.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/purgatory/mod.rs')
-rw-r--r--src/purgatory/mod.rs341
1 files changed, 290 insertions, 51 deletions
diff --git a/src/purgatory/mod.rs b/src/purgatory/mod.rs
index 9a63bf6..bb6ff54 100644
--- a/src/purgatory/mod.rs
+++ b/src/purgatory/mod.rs
@@ -16,11 +16,12 @@ pub mod persistence;
16pub mod sync; 16pub mod sync;
17mod types; 17mod types;
18 18
19pub use helpers::{can_apply_state, can_satisfy_state, extract_refs_from_state, get_unpushed_refs}; 19pub use helpers::{can_apply_state, can_satisfy_state, diagnose_state_mismatch, extract_refs_from_state, get_unpushed_refs};
20pub use types::{AnnouncementPurgatoryEntry, PrPurgatoryEntry, RefPair, RefUpdate, StatePurgatoryEntry}; 20pub use types::{AnnouncementPurgatoryEntry, EventSource, PrPurgatoryEntry, RefPair, RefUpdate, StatePurgatoryEntry};
21 21
22use dashmap::DashMap; 22use dashmap::DashMap;
23use nostr_sdk::prelude::*; 23use nostr_sdk::prelude::*;
24use nostr_sdk::ToBech32;
24use serde::{Deserialize, Serialize}; 25use serde::{Deserialize, Serialize};
25use std::collections::HashMap; 26use std::collections::HashMap;
26use std::collections::HashSet; 27use std::collections::HashSet;
@@ -64,6 +65,9 @@ struct SerializableStatePurgatoryEntry {
64 created_at_offset_secs: u64, 65 created_at_offset_secs: u64,
65 /// Duration offset from saved_at for expires_at 66 /// Duration offset from saved_at for expires_at
66 expires_at_offset_secs: u64, 67 expires_at_offset_secs: u64,
68 /// Source of this event (direct submission vs sync)
69 #[serde(default)]
70 source: types::EventSource,
67} 71}
68 72
69/// Serializable wrapper for `PrPurgatoryEntry` with time offsets. 73/// Serializable wrapper for `PrPurgatoryEntry` with time offsets.
@@ -81,6 +85,9 @@ struct SerializablePrPurgatoryEntry {
81 created_at_offset_secs: u64, 85 created_at_offset_secs: u64,
82 /// Duration offset from saved_at for expires_at 86 /// Duration offset from saved_at for expires_at
83 expires_at_offset_secs: u64, 87 expires_at_offset_secs: u64,
88 /// Source of this event (direct submission vs sync)
89 #[serde(default)]
90 source: types::EventSource,
84} 91}
85 92
86/// Serializable wrapper for `AnnouncementPurgatoryEntry` with time offsets. 93/// Serializable wrapper for `AnnouncementPurgatoryEntry` with time offsets.
@@ -313,11 +320,38 @@ impl Purgatory {
313 /// For sync-triggered events, the SyncManager calls `enqueue_sync_immediate` separately 320 /// For sync-triggered events, the SyncManager calls `enqueue_sync_immediate` separately
314 /// to override this delay. 321 /// to override this delay.
315 /// 322 ///
323 /// If an event already exists in purgatory with `Sync` source and the new submission
324 /// is direct (`!from_sync`), the source is upgraded to `Direct` without extending expiry.
325 ///
316 /// # Arguments 326 /// # Arguments
317 /// * `event` - The state event (kind 30618) to hold 327 /// * `event` - The state event (kind 30618) to hold
318 /// * `identifier` - The repository identifier from the 'd' tag 328 /// * `identifier` - The repository identifier from the 'd' tag
319 /// * `author` - The event author's public key 329 /// * `author` - The event author's public key
320 pub fn add_state(&self, event: Event, identifier: String, author: PublicKey) { 330 /// * `from_sync` - True if this event came from proactive sync (vs user-submitted)
331 pub fn add_state(&self, event: Event, identifier: String, author: PublicKey, from_sync: bool) {
332 let source = if from_sync {
333 types::EventSource::Sync
334 } else {
335 types::EventSource::Direct
336 };
337
338 // Check if event already exists - if so, potentially upgrade source
339 if let Some(mut entries) = self.state_events.get_mut(&identifier) {
340 if let Some(existing) = entries.iter_mut().find(|e| e.event.id == event.id) {
341 // Upgrade source from Sync to Direct if new submission is direct
342 if existing.source == types::EventSource::Sync && !from_sync {
343 existing.source = types::EventSource::Direct;
344 existing.expires_at = Instant::now() + DEFAULT_EXPIRY;
345 tracing::debug!(
346 event_id = %event.id,
347 identifier = %identifier,
348 "Upgraded purgatory entry source from Sync to Direct, reset expiry"
349 );
350 }
351 return; // Event already exists, don't add duplicate
352 }
353 }
354
321 let now = Instant::now(); 355 let now = Instant::now();
322 let entry = StatePurgatoryEntry { 356 let entry = StatePurgatoryEntry {
323 event, 357 event,
@@ -325,6 +359,7 @@ impl Purgatory {
325 author, 359 author,
326 created_at: now, 360 created_at: now,
327 expires_at: now + DEFAULT_EXPIRY, 361 expires_at: now + DEFAULT_EXPIRY,
362 source,
328 }; 363 };
329 364
330 self.state_events 365 self.state_events
@@ -344,11 +379,35 @@ impl Purgatory {
344 /// Automatically enqueues the referenced repository identifier for background sync 379 /// Automatically enqueues the referenced repository identifier for background sync
345 /// with the default delay (3 minutes), giving time for a git push to arrive. 380 /// with the default delay (3 minutes), giving time for a git push to arrive.
346 /// 381 ///
382 /// If an event already exists in purgatory with `Sync` source and the new submission
383 /// is direct (`!from_sync`), the source is upgraded to `Direct` without extending expiry.
384 ///
347 /// # Arguments 385 /// # Arguments
348 /// * `event` - The PR event (kind 1617/1618) to hold 386 /// * `event` - The PR event (kind 1617/1618) to hold
349 /// * `event_id` - The event ID (hex string) from the 'e' tag 387 /// * `event_id` - The event ID (hex string) from the 'e' tag
350 /// * `commit` - The commit SHA from the 'c' tag 388 /// * `commit` - The commit SHA from the 'c' tag
351 pub fn add_pr(&self, event: Event, event_id: String, commit: String) { 389 /// * `from_sync` - True if this event came from proactive sync (vs user-submitted)
390 pub fn add_pr(&self, event: Event, event_id: String, commit: String, from_sync: bool) {
391 let source = if from_sync {
392 types::EventSource::Sync
393 } else {
394 types::EventSource::Direct
395 };
396
397 // Check if event already exists - if so, potentially upgrade source
398 if let Some(mut existing) = self.pr_events.get_mut(&event_id) {
399 // Upgrade source from Sync to Direct if new submission is direct
400 if existing.source == types::EventSource::Sync && !from_sync {
401 existing.source = types::EventSource::Direct;
402 existing.expires_at = Instant::now() + DEFAULT_EXPIRY;
403 tracing::debug!(
404 event_id = %event_id,
405 "Upgraded PR purgatory entry source from Sync to Direct, reset expiry"
406 );
407 }
408 return; // Event already exists, don't add duplicate
409 }
410
352 // Extract identifier from the event's `a` tag for sync enqueueing 411 // Extract identifier from the event's `a` tag for sync enqueueing
353 let identifier = crate::git::sync::extract_identifier_from_pr_event(&event); 412 let identifier = crate::git::sync::extract_identifier_from_pr_event(&event);
354 413
@@ -358,6 +417,7 @@ impl Purgatory {
358 commit, 417 commit,
359 created_at: now, 418 created_at: now,
360 expires_at: now + DEFAULT_EXPIRY, 419 expires_at: now + DEFAULT_EXPIRY,
420 source,
361 }; 421 };
362 422
363 self.pr_events.insert(event_id, entry); 423 self.pr_events.insert(event_id, entry);
@@ -371,6 +431,8 @@ impl Purgatory {
371 /// Add a PR placeholder (git data arrived before PR event). 431 /// Add a PR placeholder (git data arrived before PR event).
372 /// 432 ///
373 /// Creates a placeholder entry waiting for the corresponding PR event. 433 /// Creates a placeholder entry waiting for the corresponding PR event.
434 /// Placeholders are always marked as `Direct` source since they originate
435 /// from git pushes (direct user action).
374 /// 436 ///
375 /// # Arguments 437 /// # Arguments
376 /// * `event_id` - The expected event ID (from git ref name) 438 /// * `event_id` - The expected event ID (from git ref name)
@@ -382,6 +444,7 @@ impl Purgatory {
382 commit, 444 commit,
383 created_at: now, 445 created_at: now,
384 expires_at: now + DEFAULT_EXPIRY, 446 expires_at: now + DEFAULT_EXPIRY,
447 source: types::EventSource::Direct, // Git pushes are direct user actions
385 }; 448 };
386 449
387 self.pr_events.insert(event_id, entry); 450 self.pr_events.insert(event_id, entry);
@@ -892,6 +955,9 @@ impl Purgatory {
892 /// prevent infinite re-sync loops. Events that expire without finding git data 955 /// prevent infinite re-sync loops. Events that expire without finding git data
893 /// will be filtered out during future negentropy/REQ sync operations. 956 /// will be filtered out during future negentropy/REQ sync operations.
894 /// 957 ///
958 /// Emits structured `[PURGATORY_EXPIRED]` log entries for each expired event
959 /// to support migration scripts and operational monitoring.
960 ///
895 /// # Returns 961 /// # Returns
896 /// Tuple of (num_announcement_removed, num_state_removed, num_pr_removed) 962 /// Tuple of (num_announcement_removed, num_state_removed, num_pr_removed)
897 pub fn cleanup(&self) -> (usize, usize, usize) { 963 pub fn cleanup(&self) -> (usize, usize, usize) {
@@ -976,18 +1042,38 @@ impl Purgatory {
976 let mut state_removed = 0; 1042 let mut state_removed = 0;
977 1043
978 // Remove expired state events and mark them as expired 1044 // Remove expired state events and mark them as expired
979 self.state_events.retain(|_, entries| { 1045 self.state_events.retain(|identifier, entries| {
980 let original_len = entries.len(); 1046 let original_len = entries.len();
981 // Collect event IDs before removing
982 let expired_ids: Vec<EventId> = entries
983 .iter()
984 .filter(|entry| entry.expires_at <= now)
985 .map(|entry| entry.event.id)
986 .collect();
987 1047
988 // Mark as expired to prevent re-sync 1048 // Log and collect expired entries before removing
989 for event_id in expired_ids { 1049 for entry in entries.iter().filter(|e| e.expires_at <= now) {
990 self.mark_expired(event_id); 1050 let npub = entry.author.to_bech32().unwrap_or_else(|_| entry.author.to_hex());
1051 let event_id_short = &entry.event.id.to_hex()[..12];
1052 let source_str = if entry.source.is_direct() { "direct" } else { "sync" };
1053
1054 // Structured log for migration scripts
1055 // Direct submissions log at WARN, synced events at DEBUG
1056 if entry.source.is_direct() {
1057 tracing::warn!(
1058 "[PURGATORY_EXPIRED] repo={} npub={} event_id={}... kind={} source={} reason=\"git data not received within 30 minutes\"",
1059 identifier,
1060 npub,
1061 event_id_short,
1062 entry.event.kind.as_u16(),
1063 source_str
1064 );
1065 } else {
1066 tracing::debug!(
1067 "[PURGATORY_EXPIRED] repo={} npub={} event_id={}... kind={} source={} reason=\"git data not received within 30 minutes\"",
1068 identifier,
1069 npub,
1070 event_id_short,
1071 entry.event.kind.as_u16(),
1072 source_str
1073 );
1074 }
1075
1076 self.mark_expired(entry.event.id);
991 } 1077 }
992 1078
993 // Remove expired entries 1079 // Remove expired entries
@@ -997,21 +1083,103 @@ impl Purgatory {
997 }); 1083 });
998 1084
999 // Remove expired PR events and mark them as expired 1085 // Remove expired PR events and mark them as expired
1000 let expired_prs: Vec<(String, Option<EventId>)> = self 1086 let expired_prs: Vec<_> = self
1001 .pr_events 1087 .pr_events
1002 .iter() 1088 .iter()
1003 .filter(|entry| entry.value().expires_at <= now) 1089 .filter(|entry| entry.value().expires_at <= now)
1004 .map(|entry| { 1090 .map(|entry| {
1005 let event_id = entry.value().event.as_ref().map(|e| e.id); 1091 let pr_entry = entry.value();
1006 (entry.key().clone(), event_id) 1092 let event_id_str = entry.key().clone();
1093 let event_opt = pr_entry.event.clone();
1094 let commit = pr_entry.commit.clone();
1095 let source = pr_entry.source;
1096 (event_id_str, event_opt, commit, source)
1007 }) 1097 })
1008 .collect(); 1098 .collect();
1009 1099
1010 let pr_removed = expired_prs.len(); 1100 let pr_removed = expired_prs.len();
1011 for (event_id_str, event_id_opt) in expired_prs { 1101 for (event_id_str, event_opt, commit, source) in expired_prs {
1012 // Mark actual PR events as expired (not placeholders) 1102 // Log structured entry for PR events (not placeholders)
1013 if let Some(event_id) = event_id_opt { 1103 if let Some(ref event) = event_opt {
1014 self.mark_expired(event_id); 1104 let npub = event
1105 .pubkey
1106 .to_bech32()
1107 .unwrap_or_else(|_| event.pubkey.to_hex());
1108 let event_id_short = &event.id.to_hex()[..12];
1109 let source_str = if source.is_direct() { "direct" } else { "sync" };
1110
1111 // Extract ALL repo identifiers from 'a' tags
1112 // (PR events can reference multiple repos when there are multiple maintainers)
1113 let repos: Vec<String> = event
1114 .tags
1115 .iter()
1116 .filter_map(|tag| {
1117 let tag_vec = tag.clone().to_vec();
1118 if tag_vec.len() >= 2
1119 && tag_vec[0] == "a"
1120 && tag_vec[1].starts_with("30617:")
1121 {
1122 // Format: 30617:<owner_pubkey>:<identifier>
1123 let parts: Vec<&str> = tag_vec[1].split(':').collect();
1124 if parts.len() >= 3 {
1125 Some(parts[2].to_string())
1126 } else {
1127 None
1128 }
1129 } else {
1130 None
1131 }
1132 })
1133 .collect();
1134
1135 // Deduplicate while preserving order
1136 let mut seen = std::collections::HashSet::new();
1137 let unique_repos: Vec<String> = repos
1138 .into_iter()
1139 .filter(|r| seen.insert(r.clone()))
1140 .collect();
1141
1142 let repos_to_log = if unique_repos.is_empty() {
1143 vec!["unknown".to_string()]
1144 } else {
1145 unique_repos
1146 };
1147
1148 // Structured log for migration scripts - log once per repo
1149 // Direct submissions log at WARN, synced events at DEBUG
1150 for repo in &repos_to_log {
1151 if source.is_direct() {
1152 tracing::warn!(
1153 "[PURGATORY_EXPIRED] repo={} npub={} event_id={}... kind={} commit={} source={} reason=\"git data not received within 30 minutes\"",
1154 repo,
1155 npub,
1156 event_id_short,
1157 event.kind.as_u16(),
1158 &commit[..commit.len().min(12)],
1159 source_str
1160 );
1161 } else {
1162 tracing::debug!(
1163 "[PURGATORY_EXPIRED] repo={} npub={} event_id={}... kind={} commit={} source={} reason=\"git data not received within 30 minutes\"",
1164 repo,
1165 npub,
1166 event_id_short,
1167 event.kind.as_u16(),
1168 &commit[..commit.len().min(12)],
1169 source_str
1170 );
1171 }
1172 }
1173
1174 self.mark_expired(event.id);
1175 } else {
1176 // Placeholder (git data arrived first, but PR event never came)
1177 // Placeholders are always Direct source (from git push)
1178 tracing::debug!(
1179 "[PURGATORY_EXPIRED] placeholder event_id={} commit={} source=direct reason=\"PR event not received within 30 minutes\"",
1180 &event_id_str[..event_id_str.len().min(12)],
1181 &commit[..commit.len().min(12)]
1182 );
1015 } 1183 }
1016 self.pr_events.remove(&event_id_str); 1184 self.pr_events.remove(&event_id_str);
1017 } 1185 }
@@ -1191,6 +1359,7 @@ impl Purgatory {
1191 author: e.author, 1359 author: e.author,
1192 created_at_offset_secs: created_offset.as_secs(), 1360 created_at_offset_secs: created_offset.as_secs(),
1193 expires_at_offset_secs: expires_offset.as_secs(), 1361 expires_at_offset_secs: expires_offset.as_secs(),
1362 source: e.source,
1194 } 1363 }
1195 }) 1364 })
1196 .collect(); 1365 .collect();
@@ -1213,6 +1382,7 @@ impl Purgatory {
1213 commit: e.commit.clone(), 1382 commit: e.commit.clone(),
1214 created_at_offset_secs: created_offset.as_secs(), 1383 created_at_offset_secs: created_offset.as_secs(),
1215 expires_at_offset_secs: expires_offset.as_secs(), 1384 expires_at_offset_secs: expires_offset.as_secs(),
1385 source: e.source,
1216 }; 1386 };
1217 pr_events.insert(event_id, serializable); 1387 pr_events.insert(event_id, serializable);
1218 } 1388 }
@@ -1355,6 +1525,7 @@ impl Purgatory {
1355 author: e.author, 1525 author: e.author,
1356 created_at, 1526 created_at,
1357 expires_at, 1527 expires_at,
1528 source: e.source,
1358 } 1529 }
1359 }) 1530 })
1360 .collect(); 1531 .collect();
@@ -1380,6 +1551,7 @@ impl Purgatory {
1380 commit: e.commit, 1551 commit: e.commit,
1381 created_at, 1552 created_at,
1382 expires_at, 1553 expires_at,
1554 source: e.source,
1383 }; 1555 };
1384 1556
1385 self.pr_events.insert(event_id, entry); 1557 self.pr_events.insert(event_id, entry);
@@ -1439,8 +1611,18 @@ mod tests {
1439 .sign_with_keys(&keys) 1611 .sign_with_keys(&keys)
1440 .unwrap(); 1612 .unwrap();
1441 1613
1442 purgatory.add_state(event.clone(), "test-repo".to_string(), keys.public_key()); 1614 purgatory.add_state(
1443 purgatory.add_pr(event, "test-event-id".to_string(), "abc123".to_string()); 1615 event.clone(),
1616 "test-repo".to_string(),
1617 keys.public_key(),
1618 false,
1619 );
1620 purgatory.add_pr(
1621 event,
1622 "test-event-id".to_string(),
1623 "abc123".to_string(),
1624 false,
1625 );
1444 1626
1445 let (announcement_count, state_count, pr_count) = purgatory.count(); 1627 let (announcement_count, state_count, pr_count) = purgatory.count();
1446 assert_eq!(announcement_count, 0); 1628 assert_eq!(announcement_count, 0);
@@ -1492,7 +1674,7 @@ mod tests {
1492 let event = EventBuilder::text_note("state") 1674 let event = EventBuilder::text_note("state")
1493 .sign_with_keys(&keys) 1675 .sign_with_keys(&keys)
1494 .unwrap(); 1676 .unwrap();
1495 purgatory.add_state(event, "test-repo".to_string(), keys.public_key()); 1677 purgatory.add_state(event, "test-repo".to_string(), keys.public_key(), false);
1496 1678
1497 // Now should have pending events 1679 // Now should have pending events
1498 assert!(purgatory.has_pending_events("test-repo")); 1680 assert!(purgatory.has_pending_events("test-repo"));
@@ -1522,7 +1704,12 @@ mod tests {
1522 .sign_with_keys(&keys) 1704 .sign_with_keys(&keys)
1523 .unwrap(); 1705 .unwrap();
1524 1706
1525 purgatory.add_pr(event, "pr-event-id".to_string(), "commit123".to_string()); 1707 purgatory.add_pr(
1708 event,
1709 "pr-event-id".to_string(),
1710 "commit123".to_string(),
1711 false,
1712 );
1526 1713
1527 // Now should have pending events for test-repo 1714 // Now should have pending events for test-repo
1528 assert!(purgatory.has_pending_events("test-repo")); 1715 assert!(purgatory.has_pending_events("test-repo"));
@@ -1587,6 +1774,7 @@ fn test_pr_event_vs_placeholder() {
1587 event.clone(), 1774 event.clone(),
1588 "event-id-1".to_string(), 1775 "event-id-1".to_string(),
1589 "commit-abc".to_string(), 1776 "commit-abc".to_string(),
1777 false,
1590 ); 1778 );
1591 1779
1592 // Add a placeholder (no event) 1780 // Add a placeholder (no event)
@@ -1643,8 +1831,14 @@ fn test_cleanup_removes_expired_entries() {
1643 state_event.clone(), 1831 state_event.clone(),
1644 "test-repo".to_string(), 1832 "test-repo".to_string(),
1645 keys.public_key(), 1833 keys.public_key(),
1834 false,
1835 );
1836 purgatory.add_pr(
1837 pr_event,
1838 "pr-123".to_string(),
1839 "commit-abc".to_string(),
1840 false,
1646 ); 1841 );
1647 purgatory.add_pr(pr_event, "pr-123".to_string(), "commit-abc".to_string());
1648 purgatory.add_pr_placeholder("pr-456".to_string(), "commit-def".to_string()); 1842 purgatory.add_pr_placeholder("pr-456".to_string(), "commit-def".to_string());
1649 1843
1650 // Verify entries are there 1844 // Verify entries are there
@@ -1691,8 +1885,18 @@ fn test_cleanup_preserves_non_expired_entries() {
1691 .unwrap(); 1885 .unwrap();
1692 1886
1693 // Add fresh entries 1887 // Add fresh entries
1694 purgatory.add_state(state_event, "test-repo".to_string(), keys.public_key()); 1888 purgatory.add_state(
1695 purgatory.add_pr(pr_event, "pr-123".to_string(), "commit-abc".to_string()); 1889 state_event,
1890 "test-repo".to_string(),
1891 keys.public_key(),
1892 false,
1893 );
1894 purgatory.add_pr(
1895 pr_event,
1896 "pr-123".to_string(),
1897 "commit-abc".to_string(),
1898 false,
1899 );
1696 1900
1697 // Run cleanup 1901 // Run cleanup
1698 let (_, state_removed, pr_removed) = purgatory.cleanup(); 1902 let (_, state_removed, pr_removed) = purgatory.cleanup();
@@ -1722,8 +1926,8 @@ fn test_cleanup_mixed_expired_and_fresh() {
1722 .sign_with_keys(&keys) 1926 .sign_with_keys(&keys)
1723 .unwrap(); 1927 .unwrap();
1724 1928
1725 purgatory.add_state(event1, "test-repo".to_string(), keys.public_key()); 1929 purgatory.add_state(event1, "test-repo".to_string(), keys.public_key(), false);
1726 purgatory.add_state(event2, "test-repo".to_string(), keys.public_key()); 1930 purgatory.add_state(event2, "test-repo".to_string(), keys.public_key(), false);
1727 1931
1728 // Expire only the first one 1932 // Expire only the first one
1729 if let Some(mut entries) = purgatory.state_events.get_mut("test-repo") { 1933 if let Some(mut entries) = purgatory.state_events.get_mut("test-repo") {
@@ -1740,8 +1944,8 @@ fn test_cleanup_mixed_expired_and_fresh() {
1740 .sign_with_keys(&keys) 1944 .sign_with_keys(&keys)
1741 .unwrap(); 1945 .unwrap();
1742 1946
1743 purgatory.add_pr(pr1, "pr-1".to_string(), "commit-1".to_string()); 1947 purgatory.add_pr(pr1, "pr-1".to_string(), "commit-1".to_string(), false);
1744 purgatory.add_pr(pr2, "pr-2".to_string(), "commit-2".to_string()); 1948 purgatory.add_pr(pr2, "pr-2".to_string(), "commit-2".to_string(), false);
1745 1949
1746 // Expire only first PR 1950 // Expire only first PR
1747 if let Some(mut entry) = purgatory.pr_events.get_mut("pr-1") { 1951 if let Some(mut entry) = purgatory.pr_events.get_mut("pr-1") {
@@ -1773,8 +1977,8 @@ fn test_remove_expired_legacy_method() {
1773 .unwrap(); 1977 .unwrap();
1774 let pr_event = EventBuilder::text_note("pr").sign_with_keys(&keys).unwrap(); 1978 let pr_event = EventBuilder::text_note("pr").sign_with_keys(&keys).unwrap();
1775 1979
1776 purgatory.add_state(state_event, "repo".to_string(), keys.public_key()); 1980 purgatory.add_state(state_event, "repo".to_string(), keys.public_key(), false);
1777 purgatory.add_pr(pr_event, "pr-id".to_string(), "commit".to_string()); 1981 purgatory.add_pr(pr_event, "pr-id".to_string(), "commit".to_string(), false);
1778 1982
1779 // Expire both 1983 // Expire both
1780 if let Some(mut entries) = purgatory.state_events.get_mut("repo") { 1984 if let Some(mut entries) = purgatory.state_events.get_mut("repo") {
@@ -1808,8 +2012,8 @@ fn test_expired_event_tracking() {
1808 let pr_event_id = pr_event.id; 2012 let pr_event_id = pr_event.id;
1809 2013
1810 // Add events to purgatory 2014 // Add events to purgatory
1811 purgatory.add_state(state_event, "repo".to_string(), keys.public_key()); 2015 purgatory.add_state(state_event, "repo".to_string(), keys.public_key(), false);
1812 purgatory.add_pr(pr_event, "pr-id".to_string(), "commit".to_string()); 2016 purgatory.add_pr(pr_event, "pr-id".to_string(), "commit".to_string(), false);
1813 2017
1814 // Events should not be marked as expired yet 2018 // Events should not be marked as expired yet
1815 assert!(!purgatory.is_expired(&state_event_id)); 2019 assert!(!purgatory.is_expired(&state_event_id));
@@ -1861,7 +2065,7 @@ fn test_cleanup_expired_events() {
1861 let event2_id = event2.id; 2065 let event2_id = event2.id;
1862 2066
1863 // Add and immediately expire event1 2067 // Add and immediately expire event1
1864 purgatory.add_state(event1, "repo1".to_string(), keys.public_key()); 2068 purgatory.add_state(event1, "repo1".to_string(), keys.public_key(), false);
1865 if let Some(mut entries) = purgatory.state_events.get_mut("repo1") { 2069 if let Some(mut entries) = purgatory.state_events.get_mut("repo1") {
1866 for entry in entries.iter_mut() { 2070 for entry in entries.iter_mut() {
1867 entry.expires_at = Instant::now() - Duration::from_secs(1); 2071 entry.expires_at = Instant::now() - Duration::from_secs(1);
@@ -1870,7 +2074,7 @@ fn test_cleanup_expired_events() {
1870 purgatory.cleanup(); 2074 purgatory.cleanup();
1871 2075
1872 // Add and expire event2 (will be more recent) 2076 // Add and expire event2 (will be more recent)
1873 purgatory.add_state(event2, "repo2".to_string(), keys.public_key()); 2077 purgatory.add_state(event2, "repo2".to_string(), keys.public_key(), false);
1874 if let Some(mut entries) = purgatory.state_events.get_mut("repo2") { 2078 if let Some(mut entries) = purgatory.state_events.get_mut("repo2") {
1875 for entry in entries.iter_mut() { 2079 for entry in entries.iter_mut() {
1876 entry.expires_at = Instant::now() - Duration::from_secs(1); 2080 entry.expires_at = Instant::now() - Duration::from_secs(1);
@@ -1912,7 +2116,7 @@ fn test_expired_events_prevent_readdition() {
1912 let event_id = event.id; 2116 let event_id = event.id;
1913 2117
1914 // Add event to purgatory 2118 // Add event to purgatory
1915 purgatory.add_state(event.clone(), "repo".to_string(), keys.public_key()); 2119 purgatory.add_state(event.clone(), "repo".to_string(), keys.public_key(), false);
1916 2120
1917 // Expire it 2121 // Expire it
1918 if let Some(mut entries) = purgatory.state_events.get_mut("repo") { 2122 if let Some(mut entries) = purgatory.state_events.get_mut("repo") {
@@ -1932,7 +2136,7 @@ fn test_expired_events_prevent_readdition() {
1932 // This simulates what negentropy/REQ+EOSE should do: 2136 // This simulates what negentropy/REQ+EOSE should do:
1933 // Check if event is in event_ids() before adding 2137 // Check if event is in event_ids() before adding
1934 if !ids.contains(&event_id) { 2138 if !ids.contains(&event_id) {
1935 purgatory.add_state(event, "repo".to_string(), keys.public_key()); 2139 purgatory.add_state(event, "repo".to_string(), keys.public_key(), false);
1936 } 2140 }
1937 2141
1938 // Event should NOT be re-added 2142 // Event should NOT be re-added
@@ -1975,7 +2179,7 @@ fn test_user_can_resubmit_expired_event() {
1975 let event_id = event.id; 2179 let event_id = event.id;
1976 2180
1977 // Add event to purgatory 2181 // Add event to purgatory
1978 purgatory.add_state(event.clone(), "repo".to_string(), keys.public_key()); 2182 purgatory.add_state(event.clone(), "repo".to_string(), keys.public_key(), false);
1979 2183
1980 // Expire it 2184 // Expire it
1981 if let Some(mut entries) = purgatory.state_events.get_mut("repo") { 2185 if let Some(mut entries) = purgatory.state_events.get_mut("repo") {
@@ -2024,8 +2228,18 @@ async fn test_save_and_restore_state_events() {
2024 let event1_id = event1.id; 2228 let event1_id = event1.id;
2025 let event2_id = event2.id; 2229 let event2_id = event2.id;
2026 2230
2027 purgatory.add_state(event1.clone(), "test-repo".to_string(), keys.public_key()); 2231 purgatory.add_state(
2028 purgatory.add_state(event2.clone(), "test-repo".to_string(), keys.public_key()); 2232 event1.clone(),
2233 "test-repo".to_string(),
2234 keys.public_key(),
2235 false,
2236 );
2237 purgatory.add_state(
2238 event2.clone(),
2239 "test-repo".to_string(),
2240 keys.public_key(),
2241 false,
2242 );
2029 2243
2030 // Save to disk 2244 // Save to disk
2031 purgatory.save_to_disk(&state_file).unwrap(); 2245 purgatory.save_to_disk(&state_file).unwrap();
@@ -2087,6 +2301,7 @@ async fn test_save_and_restore_pr_events() {
2087 pr_event.clone(), 2301 pr_event.clone(),
2088 "pr-event-id".to_string(), 2302 "pr-event-id".to_string(),
2089 "commit-abc".to_string(), 2303 "commit-abc".to_string(),
2304 false,
2090 ); 2305 );
2091 2306
2092 // Save to disk 2307 // Save to disk
@@ -2156,7 +2371,7 @@ async fn test_save_and_restore_expired_events() {
2156 let event_id = event.id; 2371 let event_id = event.id;
2157 2372
2158 // Add and expire event 2373 // Add and expire event
2159 purgatory.add_state(event, "repo".to_string(), keys.public_key()); 2374 purgatory.add_state(event, "repo".to_string(), keys.public_key(), false);
2160 if let Some(mut entries) = purgatory.state_events.get_mut("repo") { 2375 if let Some(mut entries) = purgatory.state_events.get_mut("repo") {
2161 for entry in entries.iter_mut() { 2376 for entry in entries.iter_mut() {
2162 entry.expires_at = Instant::now() - Duration::from_secs(1); 2377 entry.expires_at = Instant::now() - Duration::from_secs(1);
@@ -2295,7 +2510,7 @@ async fn test_downtime_calculation() {
2295 .sign_with_keys(&keys) 2510 .sign_with_keys(&keys)
2296 .unwrap(); 2511 .unwrap();
2297 2512
2298 purgatory.add_state(event.clone(), "repo".to_string(), keys.public_key()); 2513 purgatory.add_state(event.clone(), "repo".to_string(), keys.public_key(), false);
2299 2514
2300 // Get original expiry time 2515 // Get original expiry time
2301 let original_entries = purgatory.find_state("repo"); 2516 let original_entries = purgatory.find_state("repo");
@@ -2351,7 +2566,7 @@ async fn test_expiry_times_preserved() {
2351 .sign_with_keys(&keys) 2566 .sign_with_keys(&keys)
2352 .unwrap(); 2567 .unwrap();
2353 2568
2354 purgatory.add_state(event.clone(), "repo".to_string(), keys.public_key()); 2569 purgatory.add_state(event.clone(), "repo".to_string(), keys.public_key(), false);
2355 2570
2356 // Manually set expiry to a specific time in the future 2571 // Manually set expiry to a specific time in the future
2357 let custom_expiry = Instant::now() + Duration::from_secs(600); // 10 minutes 2572 let custom_expiry = Instant::now() + Duration::from_secs(600); // 10 minutes
@@ -2410,16 +2625,19 @@ async fn test_multiple_state_events_same_identifier() {
2410 event1.clone(), 2625 event1.clone(),
2411 "shared-repo".to_string(), 2626 "shared-repo".to_string(),
2412 keys1.public_key(), 2627 keys1.public_key(),
2628 false,
2413 ); 2629 );
2414 purgatory.add_state( 2630 purgatory.add_state(
2415 event2.clone(), 2631 event2.clone(),
2416 "shared-repo".to_string(), 2632 "shared-repo".to_string(),
2417 keys2.public_key(), 2633 keys2.public_key(),
2634 false,
2418 ); 2635 );
2419 purgatory.add_state( 2636 purgatory.add_state(
2420 event3.clone(), 2637 event3.clone(),
2421 "shared-repo".to_string(), 2638 "shared-repo".to_string(),
2422 keys3.public_key(), 2639 keys3.public_key(),
2640 false,
2423 ); 2641 );
2424 2642
2425 // Save to disk 2643 // Save to disk
@@ -2466,6 +2684,7 @@ async fn test_mixed_pr_events_and_placeholders() {
2466 pr_event.clone(), 2684 pr_event.clone(),
2467 "pr-with-event".to_string(), 2685 "pr-with-event".to_string(),
2468 "commit-abc".to_string(), 2686 "commit-abc".to_string(),
2687 false,
2469 ); 2688 );
2470 2689
2471 // Add PR placeholder 2690 // Add PR placeholder
@@ -2511,7 +2730,7 @@ async fn test_file_cleanup_after_successful_restore() {
2511 let event = EventBuilder::text_note("test") 2730 let event = EventBuilder::text_note("test")
2512 .sign_with_keys(&keys) 2731 .sign_with_keys(&keys)
2513 .unwrap(); 2732 .unwrap();
2514 purgatory.add_state(event, "repo".to_string(), keys.public_key()); 2733 purgatory.add_state(event, "repo".to_string(), keys.public_key(), false);
2515 2734
2516 // Save to disk 2735 // Save to disk
2517 purgatory.save_to_disk(&state_file).unwrap(); 2736 purgatory.save_to_disk(&state_file).unwrap();
@@ -2697,8 +2916,18 @@ async fn test_comprehensive_roundtrip() {
2697 .sign_with_keys(&keys2) 2916 .sign_with_keys(&keys2)
2698 .unwrap(); 2917 .unwrap();
2699 2918
2700 purgatory.add_state(state1.clone(), "repo1".to_string(), keys1.public_key()); 2919 purgatory.add_state(
2701 purgatory.add_state(state2.clone(), "repo2".to_string(), keys2.public_key()); 2920 state1.clone(),
2921 "repo1".to_string(),
2922 keys1.public_key(),
2923 false,
2924 );
2925 purgatory.add_state(
2926 state2.clone(),
2927 "repo2".to_string(),
2928 keys2.public_key(),
2929 false,
2930 );
2702 2931
2703 // Add PR event 2932 // Add PR event
2704 let tags = vec![Tag::custom( 2933 let tags = vec![Tag::custom(
@@ -2709,7 +2938,12 @@ async fn test_comprehensive_roundtrip() {
2709 .tags(tags) 2938 .tags(tags)
2710 .sign_with_keys(&keys1) 2939 .sign_with_keys(&keys1)
2711 .unwrap(); 2940 .unwrap();
2712 purgatory.add_pr(pr_event.clone(), "pr-1".to_string(), "commit-1".to_string()); 2941 purgatory.add_pr(
2942 pr_event.clone(),
2943 "pr-1".to_string(),
2944 "commit-1".to_string(),
2945 false,
2946 );
2713 2947
2714 // Add PR placeholder 2948 // Add PR placeholder
2715 purgatory.add_pr_placeholder("pr-2".to_string(), "commit-2".to_string()); 2949 purgatory.add_pr_placeholder("pr-2".to_string(), "commit-2".to_string());
@@ -2719,7 +2953,12 @@ async fn test_comprehensive_roundtrip() {
2719 .sign_with_keys(&keys1) 2953 .sign_with_keys(&keys1)
2720 .unwrap(); 2954 .unwrap();
2721 let expired_id = expired_event.id; 2955 let expired_id = expired_event.id;
2722 purgatory.add_state(expired_event, "repo3".to_string(), keys1.public_key()); 2956 purgatory.add_state(
2957 expired_event,
2958 "repo3".to_string(),
2959 keys1.public_key(),
2960 false,
2961 );
2723 if let Some(mut entries) = purgatory.state_events.get_mut("repo3") { 2962 if let Some(mut entries) = purgatory.state_events.get_mut("repo3") {
2724 for entry in entries.iter_mut() { 2963 for entry in entries.iter_mut() {
2725 entry.expires_at = Instant::now() - Duration::from_secs(1); 2964 entry.expires_at = Instant::now() - Duration::from_secs(1);