diff options
| -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.rs | 69 | ||||
| -rw-r--r-- | src/nostr/builder.rs | 34 | ||||
| -rw-r--r-- | src/nostr/events.rs | 16 | ||||
| -rw-r--r-- | src/nostr/policy/state.rs | 9 | ||||
| -rw-r--r-- | src/purgatory/mod.rs | 94 | ||||
| -rw-r--r-- | src/sync/mod.rs | 1 | ||||
| -rw-r--r-- | tests/purgatory_persistence.rs | 45 |
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)] |
| 113 | pub struct ArchiveConfig { | 113 | pub 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 | ||
| 174 | impl 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)] |
| 187 | pub struct RepositoryConfig { | 181 | pub 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 | ||
| 210 | impl 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)] |
| 220 | pub struct BlacklistConfig { | 207 | pub 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 | ||
| 259 | impl 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)] |
| 269 | pub struct EventBlacklistConfig { | 249 | pub 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 | ||
| 295 | impl 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(); |