diff options
Diffstat (limited to 'src/purgatory/mod.rs')
| -rw-r--r-- | src/purgatory/mod.rs | 341 |
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; | |||
| 16 | pub mod sync; | 16 | pub mod sync; |
| 17 | mod types; | 17 | mod types; |
| 18 | 18 | ||
| 19 | pub use helpers::{can_apply_state, can_satisfy_state, extract_refs_from_state, get_unpushed_refs}; | 19 | pub use helpers::{can_apply_state, can_satisfy_state, diagnose_state_mismatch, extract_refs_from_state, get_unpushed_refs}; |
| 20 | pub use types::{AnnouncementPurgatoryEntry, PrPurgatoryEntry, RefPair, RefUpdate, StatePurgatoryEntry}; | 20 | pub use types::{AnnouncementPurgatoryEntry, EventSource, PrPurgatoryEntry, RefPair, RefUpdate, StatePurgatoryEntry}; |
| 21 | 21 | ||
| 22 | use dashmap::DashMap; | 22 | use dashmap::DashMap; |
| 23 | use nostr_sdk::prelude::*; | 23 | use nostr_sdk::prelude::*; |
| 24 | use nostr_sdk::ToBech32; | ||
| 24 | use serde::{Deserialize, Serialize}; | 25 | use serde::{Deserialize, Serialize}; |
| 25 | use std::collections::HashMap; | 26 | use std::collections::HashMap; |
| 26 | use std::collections::HashSet; | 27 | use 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); |