upleb.uk

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

summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDanConwayDev <DanConwayDev@protonmail.com>2025-12-05 16:37:09 +0000
committerDanConwayDev <DanConwayDev@protonmail.com>2025-12-05 16:37:09 +0000
commitb4da09689ee0bd6ac327a6ed7ffb01e2175e2596 (patch)
treecb662fb7da60568a959c75526c501ba76c0f4043
parentb7aa7b72c189290b45fb388ec1826862bc8dda49 (diff)
remove stupid tests and methods
-rw-r--r--src/sync/connection.rs41
-rw-r--r--src/sync/subscription.rs71
-rw-r--r--tests/proactive_sync_catchup.rs169
-rw-r--r--tests/proactive_sync_dynamic.rs287
4 files changed, 173 insertions, 395 deletions
diff --git a/src/sync/connection.rs b/src/sync/connection.rs
index e921185..61a33f8 100644
--- a/src/sync/connection.rs
+++ b/src/sync/connection.rs
@@ -70,10 +70,8 @@ impl SyncConnection {
70 tracing::info!("Sync connection established to {}", url); 70 tracing::info!("Sync connection established to {}", url);
71 71
72 // Create subscription manager for this connection 72 // Create subscription manager for this connection
73 let subscription_manager = SubscriptionManager::new( 73 let subscription_manager =
74 filter_service.clone(), 74 SubscriptionManager::new(filter_service.clone(), remote_domain.to_string());
75 remote_domain.to_string(),
76 );
77 75
78 Ok(Self { 76 Ok(Self {
79 url: url.to_string(), 77 url: url.to_string(),
@@ -208,10 +206,8 @@ impl SyncConnection {
208 /// - kind 30617/30618: New announcement → add Layer 2 subscription 206 /// - kind 30617/30618: New announcement → add Layer 2 subscription
209 /// - kind 1617/1618/1619/1621/1622: New PR/Issue → add Layer 3 subscription 207 /// - kind 1617/1618/1619/1621/1622: New PR/Issue → add Layer 3 subscription
210 async fn handle_dynamic_subscription(&mut self, event: &Event) { 208 async fn handle_dynamic_subscription(&mut self, event: &Event) {
211 let kind = event.kind.as_u16();
212
213 // Check if this is an announcement kind (triggers Layer 2 subscription) 209 // Check if this is an announcement kind (triggers Layer 2 subscription)
214 if SubscriptionManager::is_announcement_kind(kind) { 210 if matches!(event.kind, Kind::GitRepoAnnouncement | Kind::RepoState) {
215 if let Some(new_filters) = self.subscription_manager.add_announcement(event) { 211 if let Some(new_filters) = self.subscription_manager.add_announcement(event) {
216 tracing::info!( 212 tracing::info!(
217 "New announcement {} on {}, adding {} Layer 2 filter(s) (total filters: {})", 213 "New announcement {} on {}, adding {} Layer 2 filter(s) (total filters: {})",
@@ -224,8 +220,11 @@ impl SyncConnection {
224 } 220 }
225 } 221 }
226 222
227 // Check if this is a PR/Issue kind (triggers Layer 3 subscription) 223 // Check if this is a Patch/PR/Issue kind (triggers Layer 3 subscription)
228 if SubscriptionManager::is_pr_issue_kind(kind) { 224 if matches!(
225 event.kind,
226 Kind::GitPatch | Kind::GitIssue | Kind::Custom(1618)
227 ) {
229 if let Some(new_filters) = self.subscription_manager.add_event(event) { 228 if let Some(new_filters) = self.subscription_manager.add_event(event) {
230 tracing::info!( 229 tracing::info!(
231 "New PR/Issue {} on {}, adding {} Layer 3 filter(s) (total filters: {})", 230 "New PR/Issue {} on {}, adding {} Layer 3 filter(s) (total filters: {})",
@@ -366,11 +365,13 @@ pub async fn connect_with_retry(
366 ); 365 );
367 } 366 }
368 367
369 match SyncConnection::new(url, filter_service.clone(), &remote_domain, metrics.clone()).await { 368 match SyncConnection::new(url, filter_service.clone(), &remote_domain, metrics.clone())
369 .await
370 {
370 Ok(conn) => { 371 Ok(conn) => {
371 // Record successful connection 372 // Record successful connection
372 health_tracker.record_success(url); 373 health_tracker.record_success(url);
373 374
374 // Record metrics 375 // Record metrics
375 if let Some(ref m) = metrics { 376 if let Some(ref m) = metrics {
376 m.record_connection_attempt(url, true); 377 m.record_connection_attempt(url, true);
@@ -379,7 +380,7 @@ pub async fn connect_with_retry(
379 m.record_health_state(url, health_tracker.get_state(url)); 380 m.record_health_state(url, health_tracker.get_state(url));
380 m.record_failure_count(url, 0); 381 m.record_failure_count(url, 0);
381 } 382 }
382 383
383 tracing::info!("Sync connection established to {}", url); 384 tracing::info!("Sync connection established to {}", url);
384 385
385 // Run the connection (this blocks until disconnection) 386 // Run the connection (this blocks until disconnection)
@@ -388,7 +389,7 @@ pub async fn connect_with_retry(
388 // Connection ended - record as failure for reconnection backoff 389 // Connection ended - record as failure for reconnection backoff
389 // (The connection ending is considered a failure even if it worked for a while) 390 // (The connection ending is considered a failure even if it worked for a while)
390 health_tracker.record_failure(url); 391 health_tracker.record_failure(url);
391 392
392 // Update metrics for disconnection 393 // Update metrics for disconnection
393 if let Some(ref m) = metrics { 394 if let Some(ref m) = metrics {
394 m.set_relay_connected(url, false); 395 m.set_relay_connected(url, false);
@@ -396,7 +397,7 @@ pub async fn connect_with_retry(
396 m.record_health_state(url, health_tracker.get_state(url)); 397 m.record_health_state(url, health_tracker.get_state(url));
397 m.record_failure_count(url, health_tracker.get_failure_count(url)); 398 m.record_failure_count(url, health_tracker.get_failure_count(url));
398 } 399 }
399 400
400 tracing::warn!("Sync connection to {} ended, will reconnect", url); 401 tracing::warn!("Sync connection to {} ended, will reconnect", url);
401 } 402 }
402 Err(e) => { 403 Err(e) => {
@@ -405,14 +406,14 @@ pub async fn connect_with_retry(
405 406
406 let failure_count = health_tracker.get_failure_count(url); 407 let failure_count = health_tracker.get_failure_count(url);
407 let state = health_tracker.get_state(url); 408 let state = health_tracker.get_state(url);
408 409
409 // Record metrics 410 // Record metrics
410 if let Some(ref m) = metrics { 411 if let Some(ref m) = metrics {
411 m.record_connection_attempt(url, false); 412 m.record_connection_attempt(url, false);
412 m.set_relay_connected(url, false); 413 m.set_relay_connected(url, false);
413 m.record_health_state(url, state); 414 m.record_health_state(url, state);
414 m.record_failure_count(url, failure_count); 415 m.record_failure_count(url, failure_count);
415 416
416 // Track dead relays 417 // Track dead relays
417 if state == super::health::HealthState::Dead { 418 if state == super::health::HealthState::Dead {
418 m.inc_dead_count(); 419 m.inc_dead_count();
@@ -435,11 +436,7 @@ pub async fn connect_with_retry(
435 .get_remaining_backoff(url) 436 .get_remaining_backoff(url)
436 .unwrap_or(Duration::from_secs(5)); 437 .unwrap_or(Duration::from_secs(5));
437 438
438 tracing::debug!( 439 tracing::debug!("Waiting {:?} before reconnecting to {}", wait_duration, url);
439 "Waiting {:?} before reconnecting to {}",
440 wait_duration,
441 url
442 );
443 tokio::time::sleep(wait_duration).await; 440 tokio::time::sleep(wait_duration).await;
444 } 441 }
445} 442}
@@ -473,4 +470,4 @@ mod tests {
473 Some("relay.example.com".to_string()) 470 Some("relay.example.com".to_string())
474 ); 471 );
475 } 472 }
476} \ No newline at end of file 473}
diff --git a/src/sync/subscription.rs b/src/sync/subscription.rs
index c37404f..bbeaa2a 100644
--- a/src/sync/subscription.rs
+++ b/src/sync/subscription.rs
@@ -26,12 +26,6 @@ use super::filter::FilterService;
26/// Maximum number of filters before consolidation is triggered 26/// Maximum number of filters before consolidation is triggered
27const CONSOLIDATION_THRESHOLD: usize = 150; 27const CONSOLIDATION_THRESHOLD: usize = 150;
28 28
29/// Kind 30617 - Repository Announcement (NIP-34)
30const KIND_REPOSITORY_ANNOUNCEMENT: u16 = 30617;
31
32/// Kind 30618 - Maintainer List (NIP-34)
33const KIND_MAINTAINER_LIST: u16 = 30618;
34
35/// Manages subscriptions for a single relay connection 29/// Manages subscriptions for a single relay connection
36/// 30///
37/// Tracks which announcements and events have been subscribed to, 31/// Tracks which announcements and events have been subscribed to,
@@ -113,10 +107,7 @@ impl SubscriptionManager {
113 107
114 // Build Layer 3 filter for this event 108 // Build Layer 3 filter for this event
115 // Layer 3 filters target events with 'e' tags pointing to this event 109 // Layer 3 filters target events with 'e' tags pointing to this event
116 let filter = Filter::new().custom_tag( 110 let filter = Filter::new().custom_tag(SingleLetterTag::lowercase(Alphabet::E), event_id);
117 SingleLetterTag::lowercase(Alphabet::E),
118 event_id,
119 );
120 111
121 Some(vec![filter]) 112 Some(vec![filter])
122 } 113 }
@@ -212,67 +203,27 @@ impl SubscriptionManager {
212 } 203 }
213 }; 204 };
214 205
215 // Determine the kind for the coordinate 206 // Verify this is an announcement kind
216 let kind = event.kind.as_u16(); 207 if !matches!(event.kind, Kind::GitRepoAnnouncement | Kind::RepoState) {
217 if kind != KIND_REPOSITORY_ANNOUNCEMENT && kind != KIND_MAINTAINER_LIST {
218 tracing::warn!( 208 tracing::warn!(
219 "Event {} is not an announcement (kind {}), cannot build Layer 2 filter", 209 "Event {} is not an announcement (kind {}), cannot build Layer 2 filter",
220 event.id.to_hex(), 210 event.id.to_hex(),
221 kind 211 event.kind
222 ); 212 );
223 return Vec::new(); 213 return Vec::new();
224 } 214 }
225 215
226 // Build the addressable coordinate: kind:pubkey:identifier 216 // Build the addressable coordinate: kind:pubkey:identifier
227 let coord = format!("{}:{}:{}", kind, event.pubkey.to_hex(), identifier); 217 let coord = format!(
218 "{}:{}:{}",
219 event.kind.as_u16(),
220 event.pubkey.to_hex(),
221 identifier
222 );
228 223
229 // Create filter with 'a' tag for this coordinate 224 // Create filter with 'a' tag for this coordinate
230 let filter = Filter::new().custom_tag( 225 let filter = Filter::new().custom_tag(SingleLetterTag::lowercase(Alphabet::A), coord);
231 SingleLetterTag::lowercase(Alphabet::A),
232 coord,
233 );
234 226
235 vec![filter] 227 vec![filter]
236 } 228 }
237
238 /// Check if an event kind is an announcement kind
239 pub fn is_announcement_kind(kind: u16) -> bool {
240 kind == KIND_REPOSITORY_ANNOUNCEMENT || kind == KIND_MAINTAINER_LIST
241 }
242
243 /// Check if an event kind is a PR/Issue/Patch kind that should trigger Layer 3
244 pub fn is_pr_issue_kind(kind: u16) -> bool {
245 matches!(
246 kind,
247 1617 | // Patch proposal (NIP-34)
248 1618 | // PR
249 1619 | // PR Update
250 1621 | // Issue
251 1622 // Reply
252 )
253 }
254} 229}
255
256#[cfg(test)]
257mod tests {
258 use super::SubscriptionManager;
259
260 #[test]
261 fn test_is_announcement_kind() {
262 assert!(SubscriptionManager::is_announcement_kind(30617));
263 assert!(SubscriptionManager::is_announcement_kind(30618));
264 assert!(!SubscriptionManager::is_announcement_kind(1));
265 assert!(!SubscriptionManager::is_announcement_kind(1617));
266 }
267
268 #[test]
269 fn test_is_pr_issue_kind() {
270 assert!(SubscriptionManager::is_pr_issue_kind(1617));
271 assert!(SubscriptionManager::is_pr_issue_kind(1618));
272 assert!(SubscriptionManager::is_pr_issue_kind(1619));
273 assert!(SubscriptionManager::is_pr_issue_kind(1621));
274 assert!(SubscriptionManager::is_pr_issue_kind(1622));
275 assert!(!SubscriptionManager::is_pr_issue_kind(30617));
276 assert!(!SubscriptionManager::is_pr_issue_kind(1));
277 }
278} \ No newline at end of file
diff --git a/tests/proactive_sync_catchup.rs b/tests/proactive_sync_catchup.rs
index 944ae50..d8a2ef9 100644
--- a/tests/proactive_sync_catchup.rs
+++ b/tests/proactive_sync_catchup.rs
@@ -17,89 +17,6 @@
17use ngit_grasp::sync::SubscriptionManager; 17use ngit_grasp::sync::SubscriptionManager;
18 18
19// ============================================================================ 19// ============================================================================
20// Configuration Constants Tests
21// ============================================================================
22
23/// Test that default startup delay is 30 seconds
24#[test]
25fn test_default_startup_delay_is_30_seconds() {
26 // The spec requires 30s warm-up before startup catchup
27 const EXPECTED_STARTUP_DELAY: u64 = 30;
28
29 // This is defined in negentropy.rs as DEFAULT_STARTUP_DELAY_SECS
30 // We verify the expected value matches the spec
31 assert_eq!(EXPECTED_STARTUP_DELAY, 30);
32}
33
34/// Test that default reconnect delay is 10 seconds
35#[test]
36fn test_default_reconnect_delay_is_10_seconds() {
37 // The spec requires 10s delay after reconnection before catchup
38 const EXPECTED_RECONNECT_DELAY: u64 = 10;
39 assert_eq!(EXPECTED_RECONNECT_DELAY, 10);
40}
41
42/// Test that reconnect lookback is 3 days
43#[test]
44fn test_reconnect_lookback_is_3_days() {
45 // The spec requires 3 days lookback for reconnect catchup
46 const EXPECTED_LOOKBACK_DAYS: u64 = 3;
47 const EXPECTED_LOOKBACK_SECS: u64 = 3 * 24 * 60 * 60; // 259,200 seconds
48
49 assert_eq!(EXPECTED_LOOKBACK_DAYS, 3);
50 assert_eq!(EXPECTED_LOOKBACK_SECS, 259200);
51}
52
53/// Test daily catchup interval is 24 hours
54#[test]
55fn test_daily_catchup_interval_is_24_hours() {
56 // The spec requires daily catchup once per 24 hours
57 const EXPECTED_DAILY_INTERVAL_SECS: u64 = 86400; // 24 * 60 * 60
58 assert_eq!(EXPECTED_DAILY_INTERVAL_SECS, 86400);
59}
60
61/// Test relay stagger delay is 5 minutes
62#[test]
63fn test_relay_stagger_is_5_minutes() {
64 // The spec requires 5-minute stagger between relays for catchup
65 const EXPECTED_STAGGER_SECS: u64 = 300; // 5 * 60
66 assert_eq!(EXPECTED_STAGGER_SECS, 300);
67}
68
69// ============================================================================
70// Filter Compatibility Tests
71// ============================================================================
72
73/// Test that catchup uses announcement kinds (30617, 30618)
74#[test]
75fn test_catchup_uses_announcement_kinds() {
76 // Layer 1 filters should include announcement kinds
77 assert!(SubscriptionManager::is_announcement_kind(30617));
78 assert!(SubscriptionManager::is_announcement_kind(30618));
79}
80
81/// Test that catchup uses PR/Issue kinds for Layer 3
82#[test]
83fn test_catchup_uses_pr_issue_kinds() {
84 // Layer 3 should track PR and Issue kinds
85 assert!(SubscriptionManager::is_pr_issue_kind(1617)); // Patch proposal
86 assert!(SubscriptionManager::is_pr_issue_kind(1618)); // PR
87 assert!(SubscriptionManager::is_pr_issue_kind(1619)); // PR Update
88 assert!(SubscriptionManager::is_pr_issue_kind(1621)); // Issue
89 assert!(SubscriptionManager::is_pr_issue_kind(1622)); // Reply
90}
91
92/// Test that non-sync kinds are not included in catchup
93#[test]
94fn test_catchup_excludes_non_sync_kinds() {
95 // Regular text notes and other kinds should not be included
96 assert!(!SubscriptionManager::is_announcement_kind(1)); // Text note
97 assert!(!SubscriptionManager::is_announcement_kind(4)); // DM
98 assert!(!SubscriptionManager::is_pr_issue_kind(1)); // Text note
99 assert!(!SubscriptionManager::is_pr_issue_kind(30617)); // Announcement (wrong layer)
100}
101
102// ============================================================================
103// Catchup State Machine Tests 20// Catchup State Machine Tests
104// ============================================================================ 21// ============================================================================
105 22
@@ -108,17 +25,17 @@ fn test_catchup_excludes_non_sync_kinds() {
108fn test_startup_catchup_runs_once() { 25fn test_startup_catchup_runs_once() {
109 // After startup catchup completes, should_run_startup_catchup should return false 26 // After startup catchup completes, should_run_startup_catchup should return false
110 // This is handled by the startup_catchup_completed flag in NegentropyService 27 // This is handled by the startup_catchup_completed flag in NegentropyService
111 28
112 // Simulating the state machine: 29 // Simulating the state machine:
113 let mut startup_completed = false; 30 let mut startup_completed = false;
114 31
115 // Before running, should return true (if delay elapsed) 32 // Before running, should return true (if delay elapsed)
116 let should_run_before = !startup_completed; 33 let should_run_before = !startup_completed;
117 assert!(should_run_before); 34 assert!(should_run_before);
118 35
119 // After running, mark as completed 36 // After running, mark as completed
120 startup_completed = true; 37 startup_completed = true;
121 38
122 // Now should return false 39 // Now should return false
123 let should_run_after = !startup_completed; 40 let should_run_after = !startup_completed;
124 assert!(!should_run_after); 41 assert!(!should_run_after);
@@ -128,12 +45,12 @@ fn test_startup_catchup_runs_once() {
128#[test] 45#[test]
129fn test_daily_catchup_interval_check() { 46fn test_daily_catchup_interval_check() {
130 use std::time::{Duration, Instant}; 47 use std::time::{Duration, Instant};
131 48
132 const DAILY_INTERVAL_SECS: u64 = 86400; 49 const DAILY_INTERVAL_SECS: u64 = 86400;
133 50
134 // Simulate last catchup time 51 // Simulate last catchup time
135 let last_catchup = Instant::now(); 52 let last_catchup = Instant::now();
136 53
137 // Immediately after, should not run 54 // Immediately after, should not run
138 let should_run_immediately = last_catchup.elapsed() >= Duration::from_secs(DAILY_INTERVAL_SECS); 55 let should_run_immediately = last_catchup.elapsed() >= Duration::from_secs(DAILY_INTERVAL_SECS);
139 assert!(!should_run_immediately); 56 assert!(!should_run_immediately);
@@ -144,10 +61,10 @@ fn test_daily_catchup_interval_check() {
144fn test_new_relay_should_run_daily_catchup() { 61fn test_new_relay_should_run_daily_catchup() {
145 use std::collections::HashMap; 62 use std::collections::HashMap;
146 use std::time::Instant; 63 use std::time::Instant;
147 64
148 let last_daily_catchup: HashMap<String, Instant> = HashMap::new(); 65 let last_daily_catchup: HashMap<String, Instant> = HashMap::new();
149 let relay_url = "wss://test-relay.example.com"; 66 let relay_url = "wss://test-relay.example.com";
150 67
151 // No previous catchup recorded, should return true 68 // No previous catchup recorded, should return true
152 let should_run = !last_daily_catchup.contains_key(relay_url); 69 let should_run = !last_daily_catchup.contains_key(relay_url);
153 assert!(should_run); 70 assert!(should_run);
@@ -159,14 +76,14 @@ fn test_reconnect_catchup_after_reconnection() {
159 // Reconnect catchup should only trigger when: 76 // Reconnect catchup should only trigger when:
160 // 1. Connection was previously successful (had_previous_connection = true) 77 // 1. Connection was previously successful (had_previous_connection = true)
161 // 2. Connection was lost and restored 78 // 2. Connection was lost and restored
162 79
163 let mut had_previous_connection = false; 80 let mut had_previous_connection = false;
164 81
165 // First connection - should NOT trigger reconnect catchup 82 // First connection - should NOT trigger reconnect catchup
166 let is_reconnection_first = had_previous_connection; 83 let is_reconnection_first = had_previous_connection;
167 assert!(!is_reconnection_first); 84 assert!(!is_reconnection_first);
168 had_previous_connection = true; 85 had_previous_connection = true;
169 86
170 // Second connection (after disconnection) - SHOULD trigger 87 // Second connection (after disconnection) - SHOULD trigger
171 let is_reconnection_second = had_previous_connection; 88 let is_reconnection_second = had_previous_connection;
172 assert!(is_reconnection_second); 89 assert!(is_reconnection_second);
@@ -185,10 +102,10 @@ fn test_gap_events_validated_through_policy() {
185 // 2. Check if event exists locally 102 // 2. Check if event exists locally
186 // 3. Validate through Nip34WritePolicy 103 // 3. Validate through Nip34WritePolicy
187 // 4. Store if accepted 104 // 4. Store if accepted
188 105
189 // This is verified by the implementation in negentropy.rs:run_catchup() 106 // This is verified by the implementation in negentropy.rs:run_catchup()
190 // where PolicyResult::Accept leads to storage and PolicyResult::Reject is logged 107 // where PolicyResult::Accept leads to storage and PolicyResult::Reject is logged
191 108
192 assert!(true); // Flow verification - actual validation tested in other tests 109 assert!(true); // Flow verification - actual validation tested in other tests
193} 110}
194 111
@@ -197,14 +114,14 @@ fn test_gap_events_validated_through_policy() {
197fn test_gap_events_logged_at_warn_level() { 114fn test_gap_events_logged_at_warn_level() {
198 // The spec requires gap events to be logged at WARN level 115 // The spec requires gap events to be logged at WARN level
199 // to distinguish them from live events (which are logged at INFO) 116 // to distinguish them from live events (which are logged at INFO)
200 117
201 // This is implemented in negentropy.rs with: 118 // This is implemented in negentropy.rs with:
202 // tracing::warn!("Gap event filled via {} catchup: {} (kind {})", ...) 119 // tracing::warn!("Gap event filled via {} catchup: {} (kind {})", ...)
203 120
204 // We verify the logging pattern exists by testing the catchup types 121 // We verify the logging pattern exists by testing the catchup types
205 let catchup_types = ["startup", "reconnect", "daily"]; 122 let catchup_types = ["startup", "reconnect", "daily"];
206 assert_eq!(catchup_types.len(), 3); 123 assert_eq!(catchup_types.len(), 3);
207 124
208 for catchup_type in catchup_types { 125 for catchup_type in catchup_types {
209 assert!(!catchup_type.is_empty()); 126 assert!(!catchup_type.is_empty());
210 } 127 }
@@ -218,21 +135,21 @@ fn test_gap_events_logged_at_warn_level() {
218#[test] 135#[test]
219fn test_stagger_delay_for_multiple_relays() { 136fn test_stagger_delay_for_multiple_relays() {
220 const STAGGER_SECS: u64 = 300; // 5 minutes 137 const STAGGER_SECS: u64 = 300; // 5 minutes
221 138
222 let _relay_urls = vec![ 139 let _relay_urls = vec![
223 "wss://relay1.example.com", 140 "wss://relay1.example.com",
224 "wss://relay2.example.com", 141 "wss://relay2.example.com",
225 "wss://relay3.example.com", 142 "wss://relay3.example.com",
226 ]; 143 ];
227 144
228 // First relay (index 0) should have no stagger 145 // First relay (index 0) should have no stagger
229 let stagger_0 = 0 * STAGGER_SECS; 146 let stagger_0 = 0 * STAGGER_SECS;
230 assert_eq!(stagger_0, 0); 147 assert_eq!(stagger_0, 0);
231 148
232 // Second relay (index 1) should have 5 minute stagger 149 // Second relay (index 1) should have 5 minute stagger
233 let stagger_1 = 1 * STAGGER_SECS; 150 let stagger_1 = 1 * STAGGER_SECS;
234 assert_eq!(stagger_1, 300); 151 assert_eq!(stagger_1, 300);
235 152
236 // Third relay (index 2) should have 10 minute stagger 153 // Third relay (index 2) should have 10 minute stagger
237 let stagger_2 = 2 * STAGGER_SECS; 154 let stagger_2 = 2 * STAGGER_SECS;
238 assert_eq!(stagger_2, 600); 155 assert_eq!(stagger_2, 600);
@@ -242,15 +159,15 @@ fn test_stagger_delay_for_multiple_relays() {
242#[test] 159#[test]
243fn test_startup_catchup_waits_for_warmup() { 160fn test_startup_catchup_waits_for_warmup() {
244 use std::time::{Duration, Instant}; 161 use std::time::{Duration, Instant};
245 162
246 const STARTUP_DELAY_SECS: u64 = 30; 163 const STARTUP_DELAY_SECS: u64 = 30;
247 164
248 let startup_time = Instant::now(); 165 let startup_time = Instant::now();
249 166
250 // Immediately after startup, should not run (delay not elapsed) 167 // Immediately after startup, should not run (delay not elapsed)
251 let elapsed = startup_time.elapsed(); 168 let elapsed = startup_time.elapsed();
252 let should_run = elapsed >= Duration::from_secs(STARTUP_DELAY_SECS); 169 let should_run = elapsed >= Duration::from_secs(STARTUP_DELAY_SECS);
253 170
254 // This should be false since we just created startup_time 171 // This should be false since we just created startup_time
255 assert!(!should_run); 172 assert!(!should_run);
256} 173}
@@ -265,7 +182,7 @@ fn test_reconnect_lookback_calculation() {
265 // 3 days = 3 * 24 * 60 * 60 = 259,200 seconds 182 // 3 days = 3 * 24 * 60 * 60 = 259,200 seconds
266 let lookback_days: u64 = 3; 183 let lookback_days: u64 = 3;
267 let lookback_secs = lookback_days * 24 * 60 * 60; 184 let lookback_secs = lookback_days * 24 * 60 * 60;
268 185
269 assert_eq!(lookback_secs, 259200); 186 assert_eq!(lookback_secs, 259200);
270} 187}
271 188
@@ -290,10 +207,10 @@ fn test_startup_catchup_scenario() {
290 // 2. Run full reconciliation (no time limit) 207 // 2. Run full reconciliation (no time limit)
291 // 3. Mark as completed (runs only once) 208 // 3. Mark as completed (runs only once)
292 // 4. Stagger between relays (5 minutes) 209 // 4. Stagger between relays (5 minutes)
293 210
294 const STARTUP_DELAY: u64 = 30; 211 const STARTUP_DELAY: u64 = 30;
295 const STAGGER: u64 = 300; 212 const STAGGER: u64 = 300;
296 213
297 assert_eq!(STARTUP_DELAY, 30); 214 assert_eq!(STARTUP_DELAY, 30);
298 assert_eq!(STAGGER, 300); 215 assert_eq!(STAGGER, 300);
299} 216}
@@ -306,10 +223,10 @@ fn test_reconnect_catchup_scenario() {
306 // 2. Wait 10s reconnect delay 223 // 2. Wait 10s reconnect delay
307 // 3. Only fetch last 3 days of events 224 // 3. Only fetch last 3 days of events
308 // 4. Runs in background (doesn't block connection) 225 // 4. Runs in background (doesn't block connection)
309 226
310 const RECONNECT_DELAY: u64 = 10; 227 const RECONNECT_DELAY: u64 = 10;
311 const LOOKBACK_DAYS: u64 = 3; 228 const LOOKBACK_DAYS: u64 = 3;
312 229
313 assert_eq!(RECONNECT_DELAY, 10); 230 assert_eq!(RECONNECT_DELAY, 10);
314 assert_eq!(LOOKBACK_DAYS, 3); 231 assert_eq!(LOOKBACK_DAYS, 3);
315} 232}
@@ -322,11 +239,11 @@ fn test_daily_catchup_scenario() {
322 // 2. Run if 24h elapsed since last catchup for that relay 239 // 2. Run if 24h elapsed since last catchup for that relay
323 // 3. Full reconciliation (no time limit) 240 // 3. Full reconciliation (no time limit)
324 // 4. Stagger between relays (5 minutes) 241 // 4. Stagger between relays (5 minutes)
325 242
326 const CHECK_INTERVAL: u64 = 3600; // 1 hour 243 const CHECK_INTERVAL: u64 = 3600; // 1 hour
327 const DAILY_INTERVAL: u64 = 86400; // 24 hours 244 const DAILY_INTERVAL: u64 = 86400; // 24 hours
328 const STAGGER: u64 = 300; // 5 minutes 245 const STAGGER: u64 = 300; // 5 minutes
329 246
330 assert_eq!(CHECK_INTERVAL, 3600); 247 assert_eq!(CHECK_INTERVAL, 3600);
331 assert_eq!(DAILY_INTERVAL, 86400); 248 assert_eq!(DAILY_INTERVAL, 86400);
332 assert_eq!(STAGGER, 300); 249 assert_eq!(STAGGER, 300);
@@ -343,10 +260,10 @@ fn test_existing_events_skipped() {
343 // 1. Fetch events from relay 260 // 1. Fetch events from relay
344 // 2. For each event, check if it exists locally 261 // 2. For each event, check if it exists locally
345 // 3. Skip if exists, validate and store if not 262 // 3. Skip if exists, validate and store if not
346 263
347 // This is implemented in negentropy.rs:event_exists_locally() 264 // This is implemented in negentropy.rs:event_exists_locally()
348 // which queries the database for the event by ID 265 // which queries the database for the event by ID
349 266
350 const SKIP_EXISTING: bool = true; 267 const SKIP_EXISTING: bool = true;
351 assert!(SKIP_EXISTING); 268 assert!(SKIP_EXISTING);
352} 269}
@@ -355,15 +272,15 @@ fn test_existing_events_skipped() {
355#[test] 272#[test]
356fn test_duplicate_prevention() { 273fn test_duplicate_prevention() {
357 use std::collections::HashSet; 274 use std::collections::HashSet;
358 275
359 let mut processed_ids: HashSet<String> = HashSet::new(); 276 let mut processed_ids: HashSet<String> = HashSet::new();
360 let event_id = "abc123def456".to_string(); 277 let event_id = "abc123def456".to_string();
361 278
362 // First time seeing this event - should process 279 // First time seeing this event - should process
363 let is_new = !processed_ids.contains(&event_id); 280 let is_new = !processed_ids.contains(&event_id);
364 assert!(is_new); 281 assert!(is_new);
365 processed_ids.insert(event_id.clone()); 282 processed_ids.insert(event_id.clone());
366 283
367 // Second time - should skip 284 // Second time - should skip
368 let is_duplicate = processed_ids.contains(&event_id); 285 let is_duplicate = processed_ids.contains(&event_id);
369 assert!(is_duplicate); 286 assert!(is_duplicate);
@@ -380,18 +297,18 @@ fn test_config_fields_for_catchup() {
380 // - sync_startup_delay_secs (default: 30) 297 // - sync_startup_delay_secs (default: 30)
381 // - sync_reconnect_delay_secs (default: 10) 298 // - sync_reconnect_delay_secs (default: 10)
382 // - sync_reconnect_lookback_days (default: 3) 299 // - sync_reconnect_lookback_days (default: 3)
383 300
384 // Environment variables: 301 // Environment variables:
385 // - NGIT_SYNC_STARTUP_DELAY_SECS 302 // - NGIT_SYNC_STARTUP_DELAY_SECS
386 // - NGIT_SYNC_RECONNECT_DELAY_SECS 303 // - NGIT_SYNC_RECONNECT_DELAY_SECS
387 // - NGIT_SYNC_RECONNECT_LOOKBACK_DAYS 304 // - NGIT_SYNC_RECONNECT_LOOKBACK_DAYS
388 305
389 let expected_defaults = vec![ 306 let expected_defaults = vec![
390 ("startup_delay_secs", 30u64), 307 ("startup_delay_secs", 30u64),
391 ("reconnect_delay_secs", 10u64), 308 ("reconnect_delay_secs", 10u64),
392 ("reconnect_lookback_days", 3u64), 309 ("reconnect_lookback_days", 3u64),
393 ]; 310 ];
394 311
395 assert_eq!(expected_defaults.len(), 3); 312 assert_eq!(expected_defaults.len(), 3);
396 assert_eq!(expected_defaults[0].1, 30); 313 assert_eq!(expected_defaults[0].1, 30);
397 assert_eq!(expected_defaults[1].1, 10); 314 assert_eq!(expected_defaults[1].1, 10);
@@ -405,9 +322,9 @@ fn test_catchup_respects_config() {
405 let custom_startup_delay: u64 = 60; 322 let custom_startup_delay: u64 = 60;
406 let custom_reconnect_delay: u64 = 20; 323 let custom_reconnect_delay: u64 = 20;
407 let custom_lookback_days: u64 = 7; 324 let custom_lookback_days: u64 = 7;
408 325
409 // All should be configurable to non-default values 326 // All should be configurable to non-default values
410 assert_ne!(custom_startup_delay, 30); 327 assert_ne!(custom_startup_delay, 30);
411 assert_ne!(custom_reconnect_delay, 10); 328 assert_ne!(custom_reconnect_delay, 10);
412 assert_ne!(custom_lookback_days, 3); 329 assert_ne!(custom_lookback_days, 3);
413} \ No newline at end of file 330}
diff --git a/tests/proactive_sync_dynamic.rs b/tests/proactive_sync_dynamic.rs
index 8a3cb88..2d3232f 100644
--- a/tests/proactive_sync_dynamic.rs
+++ b/tests/proactive_sync_dynamic.rs
@@ -117,69 +117,6 @@ fn create_test_reply_event(keys: &Keys, event_id: &str) -> Event {
117} 117}
118 118
119// ============================================================================ 119// ============================================================================
120// Kind Detection Tests
121// ============================================================================
122
123/// Test that announcement kinds are correctly identified
124#[test]
125fn test_is_announcement_kind_30617() {
126 assert!(SubscriptionManager::is_announcement_kind(30617));
127}
128
129/// Test that maintainer list kind is correctly identified
130#[test]
131fn test_is_announcement_kind_30618() {
132 assert!(SubscriptionManager::is_announcement_kind(30618));
133}
134
135/// Test that non-announcement kinds are not identified as announcements
136#[test]
137fn test_is_announcement_kind_negative() {
138 assert!(!SubscriptionManager::is_announcement_kind(1)); // Text note
139 assert!(!SubscriptionManager::is_announcement_kind(1617)); // PR
140 assert!(!SubscriptionManager::is_announcement_kind(1621)); // Issue
141 assert!(!SubscriptionManager::is_announcement_kind(0)); // Unknown
142}
143
144/// Test that PR/Issue kinds are correctly identified
145#[test]
146fn test_is_pr_issue_kind_1617() {
147 assert!(SubscriptionManager::is_pr_issue_kind(1617)); // Patch proposal
148}
149
150/// Test that PR kind 1618 is correctly identified
151#[test]
152fn test_is_pr_issue_kind_1618() {
153 assert!(SubscriptionManager::is_pr_issue_kind(1618)); // PR
154}
155
156/// Test that PR update kind is correctly identified
157#[test]
158fn test_is_pr_issue_kind_1619() {
159 assert!(SubscriptionManager::is_pr_issue_kind(1619)); // PR Update
160}
161
162/// Test that Issue kind is correctly identified
163#[test]
164fn test_is_pr_issue_kind_1621() {
165 assert!(SubscriptionManager::is_pr_issue_kind(1621)); // Issue
166}
167
168/// Test that Reply kind is correctly identified
169#[test]
170fn test_is_pr_issue_kind_1622() {
171 assert!(SubscriptionManager::is_pr_issue_kind(1622)); // Reply
172}
173
174/// Test that non-PR/Issue kinds are not identified
175#[test]
176fn test_is_pr_issue_kind_negative() {
177 assert!(!SubscriptionManager::is_pr_issue_kind(30617)); // Announcement
178 assert!(!SubscriptionManager::is_pr_issue_kind(1)); // Text note
179 assert!(!SubscriptionManager::is_pr_issue_kind(0)); // Unknown
180}
181
182// ============================================================================
183// Filter Count Tests 120// Filter Count Tests
184// ============================================================================ 121// ============================================================================
185 122
@@ -188,19 +125,19 @@ fn test_is_pr_issue_kind_negative() {
188fn test_initial_filter_count() { 125fn test_initial_filter_count() {
189 // Create a minimal SubscriptionManager-like state for testing 126 // Create a minimal SubscriptionManager-like state for testing
190 // We test the logic without needing a full FilterService 127 // We test the logic without needing a full FilterService
191 128
192 // Initial state: 0 announcements, 0 events, not consolidated 129 // Initial state: 0 announcements, 0 events, not consolidated
193 // Filter count should be: 1 (Layer 1) + 0 + 0 = 1 130 // Filter count should be: 1 (Layer 1) + 0 + 0 = 1
194 let announcement_count = 0; 131 let announcement_count = 0;
195 let event_count = 0; 132 let event_count = 0;
196 let is_consolidated = false; 133 let is_consolidated = false;
197 134
198 let filter_count = if is_consolidated { 135 let filter_count = if is_consolidated {
199 1 136 1
200 } else { 137 } else {
201 1 + announcement_count + event_count 138 1 + announcement_count + event_count
202 }; 139 };
203 140
204 assert_eq!(filter_count, 1); 141 assert_eq!(filter_count, 1);
205} 142}
206 143
@@ -210,13 +147,13 @@ fn test_filter_count_with_announcements() {
210 let announcement_count = 5; 147 let announcement_count = 5;
211 let event_count = 0; 148 let event_count = 0;
212 let is_consolidated = false; 149 let is_consolidated = false;
213 150
214 let filter_count = if is_consolidated { 151 let filter_count = if is_consolidated {
215 1 152 1
216 } else { 153 } else {
217 1 + announcement_count + event_count 154 1 + announcement_count + event_count
218 }; 155 };
219 156
220 // 1 (Layer 1) + 5 (announcements) = 6 157 // 1 (Layer 1) + 5 (announcements) = 6
221 assert_eq!(filter_count, 6); 158 assert_eq!(filter_count, 6);
222} 159}
@@ -227,13 +164,13 @@ fn test_filter_count_with_events() {
227 let announcement_count = 0; 164 let announcement_count = 0;
228 let event_count = 10; 165 let event_count = 10;
229 let is_consolidated = false; 166 let is_consolidated = false;
230 167
231 let filter_count = if is_consolidated { 168 let filter_count = if is_consolidated {
232 1 169 1
233 } else { 170 } else {
234 1 + announcement_count + event_count 171 1 + announcement_count + event_count
235 }; 172 };
236 173
237 // 1 (Layer 1) + 10 (events) = 11 174 // 1 (Layer 1) + 10 (events) = 11
238 assert_eq!(filter_count, 11); 175 assert_eq!(filter_count, 11);
239} 176}
@@ -244,13 +181,13 @@ fn test_filter_count_mixed() {
244 let announcement_count = 50; 181 let announcement_count = 50;
245 let event_count = 30; 182 let event_count = 30;
246 let is_consolidated = false; 183 let is_consolidated = false;
247 184
248 let filter_count = if is_consolidated { 185 let filter_count = if is_consolidated {
249 1 186 1
250 } else { 187 } else {
251 1 + announcement_count + event_count 188 1 + announcement_count + event_count
252 }; 189 };
253 190
254 // 1 + 50 + 30 = 81 191 // 1 + 50 + 30 = 81
255 assert_eq!(filter_count, 81); 192 assert_eq!(filter_count, 81);
256} 193}
@@ -261,13 +198,13 @@ fn test_filter_count_consolidated() {
261 let announcement_count = 100; // These would be cleared on consolidation 198 let announcement_count = 100; // These would be cleared on consolidation
262 let event_count = 100; 199 let event_count = 100;
263 let is_consolidated = true; 200 let is_consolidated = true;
264 201
265 let filter_count = if is_consolidated { 202 let filter_count = if is_consolidated {
266 1 203 1
267 } else { 204 } else {
268 1 + announcement_count + event_count 205 1 + announcement_count + event_count
269 }; 206 };
270 207
271 assert_eq!(filter_count, 1); 208 assert_eq!(filter_count, 1);
272} 209}
273 210
@@ -280,9 +217,9 @@ fn test_filter_count_consolidated() {
280fn test_should_consolidate_below_threshold() { 217fn test_should_consolidate_below_threshold() {
281 let filter_count = 100; 218 let filter_count = 100;
282 let is_consolidated = false; 219 let is_consolidated = false;
283 220
284 let should_consolidate = !is_consolidated && filter_count > CONSOLIDATION_THRESHOLD; 221 let should_consolidate = !is_consolidated && filter_count > CONSOLIDATION_THRESHOLD;
285 222
286 assert!(!should_consolidate); 223 assert!(!should_consolidate);
287} 224}
288 225
@@ -291,9 +228,9 @@ fn test_should_consolidate_below_threshold() {
291fn test_should_consolidate_at_threshold() { 228fn test_should_consolidate_at_threshold() {
292 let filter_count = 151; // > 150 229 let filter_count = 151; // > 150
293 let is_consolidated = false; 230 let is_consolidated = false;
294 231
295 let should_consolidate = !is_consolidated && filter_count > CONSOLIDATION_THRESHOLD; 232 let should_consolidate = !is_consolidated && filter_count > CONSOLIDATION_THRESHOLD;
296 233
297 assert!(should_consolidate); 234 assert!(should_consolidate);
298} 235}
299 236
@@ -302,9 +239,9 @@ fn test_should_consolidate_at_threshold() {
302fn test_should_consolidate_above_threshold() { 239fn test_should_consolidate_above_threshold() {
303 let filter_count = 200; 240 let filter_count = 200;
304 let is_consolidated = false; 241 let is_consolidated = false;
305 242
306 let should_consolidate = !is_consolidated && filter_count > CONSOLIDATION_THRESHOLD; 243 let should_consolidate = !is_consolidated && filter_count > CONSOLIDATION_THRESHOLD;
307 244
308 assert!(should_consolidate); 245 assert!(should_consolidate);
309} 246}
310 247
@@ -313,9 +250,9 @@ fn test_should_consolidate_above_threshold() {
313fn test_should_consolidate_already_consolidated() { 250fn test_should_consolidate_already_consolidated() {
314 let filter_count = 200; // Would trigger, but already consolidated 251 let filter_count = 200; // Would trigger, but already consolidated
315 let is_consolidated = true; 252 let is_consolidated = true;
316 253
317 let should_consolidate = !is_consolidated && filter_count > CONSOLIDATION_THRESHOLD; 254 let should_consolidate = !is_consolidated && filter_count > CONSOLIDATION_THRESHOLD;
318 255
319 assert!(!should_consolidate); 256 assert!(!should_consolidate);
320} 257}
321 258
@@ -323,11 +260,11 @@ fn test_should_consolidate_already_consolidated() {
323#[test] 260#[test]
324fn test_consolidation_threshold_boundary() { 261fn test_consolidation_threshold_boundary() {
325 let is_consolidated = false; 262 let is_consolidated = false;
326 263
327 // 150 should NOT trigger (> 150, not >= 150) 264 // 150 should NOT trigger (> 150, not >= 150)
328 let should_consolidate_at_150 = !is_consolidated && 150 > CONSOLIDATION_THRESHOLD; 265 let should_consolidate_at_150 = !is_consolidated && 150 > CONSOLIDATION_THRESHOLD;
329 assert!(!should_consolidate_at_150); 266 assert!(!should_consolidate_at_150);
330 267
331 // 151 should trigger 268 // 151 should trigger
332 let should_consolidate_at_151 = !is_consolidated && 151 > CONSOLIDATION_THRESHOLD; 269 let should_consolidate_at_151 = !is_consolidated && 151 > CONSOLIDATION_THRESHOLD;
333 assert!(should_consolidate_at_151); 270 assert!(should_consolidate_at_151);
@@ -341,14 +278,14 @@ fn test_consolidation_threshold_boundary() {
341#[test] 278#[test]
342fn test_duplicate_announcement_prevention() { 279fn test_duplicate_announcement_prevention() {
343 let mut subscribed_announcements: HashSet<String> = HashSet::new(); 280 let mut subscribed_announcements: HashSet<String> = HashSet::new();
344 281
345 let event_id = "abc123".to_string(); 282 let event_id = "abc123".to_string();
346 283
347 // First add should succeed 284 // First add should succeed
348 let is_new = !subscribed_announcements.contains(&event_id); 285 let is_new = !subscribed_announcements.contains(&event_id);
349 assert!(is_new); 286 assert!(is_new);
350 subscribed_announcements.insert(event_id.clone()); 287 subscribed_announcements.insert(event_id.clone());
351 288
352 // Second add should fail (duplicate) 289 // Second add should fail (duplicate)
353 let is_new_again = !subscribed_announcements.contains(&event_id); 290 let is_new_again = !subscribed_announcements.contains(&event_id);
354 assert!(!is_new_again); 291 assert!(!is_new_again);
@@ -358,14 +295,14 @@ fn test_duplicate_announcement_prevention() {
358#[test] 295#[test]
359fn test_duplicate_event_prevention() { 296fn test_duplicate_event_prevention() {
360 let mut subscribed_events: HashSet<String> = HashSet::new(); 297 let mut subscribed_events: HashSet<String> = HashSet::new();
361 298
362 let event_id = "def456".to_string(); 299 let event_id = "def456".to_string();
363 300
364 // First add should succeed 301 // First add should succeed
365 let is_new = !subscribed_events.contains(&event_id); 302 let is_new = !subscribed_events.contains(&event_id);
366 assert!(is_new); 303 assert!(is_new);
367 subscribed_events.insert(event_id.clone()); 304 subscribed_events.insert(event_id.clone());
368 305
369 // Second add should fail (duplicate) 306 // Second add should fail (duplicate)
370 let is_new_again = !subscribed_events.contains(&event_id); 307 let is_new_again = !subscribed_events.contains(&event_id);
371 assert!(!is_new_again); 308 assert!(!is_new_again);
@@ -375,14 +312,14 @@ fn test_duplicate_event_prevention() {
375#[test] 312#[test]
376fn test_multiple_unique_items_tracked() { 313fn test_multiple_unique_items_tracked() {
377 let mut subscribed_announcements: HashSet<String> = HashSet::new(); 314 let mut subscribed_announcements: HashSet<String> = HashSet::new();
378 315
379 // Add multiple unique announcements 316 // Add multiple unique announcements
380 for i in 0..10 { 317 for i in 0..10 {
381 let id = format!("announcement_{}", i); 318 let id = format!("announcement_{}", i);
382 assert!(!subscribed_announcements.contains(&id)); 319 assert!(!subscribed_announcements.contains(&id));
383 subscribed_announcements.insert(id); 320 subscribed_announcements.insert(id);
384 } 321 }
385 322
386 assert_eq!(subscribed_announcements.len(), 10); 323 assert_eq!(subscribed_announcements.len(), 10);
387} 324}
388 325
@@ -395,12 +332,12 @@ fn test_multiple_unique_items_tracked() {
395fn test_announcement_has_d_tag() { 332fn test_announcement_has_d_tag() {
396 let keys = Keys::generate(); 333 let keys = Keys::generate();
397 let event = create_test_announcement(&keys, "my-repo"); 334 let event = create_test_announcement(&keys, "my-repo");
398 335
399 let has_d_tag = event.tags.iter().any(|tag| { 336 let has_d_tag = event.tags.iter().any(|tag| {
400 let tag_vec = tag.clone().to_vec(); 337 let tag_vec = tag.clone().to_vec();
401 tag_vec.len() >= 2 && tag_vec[0] == "d" 338 tag_vec.len() >= 2 && tag_vec[0] == "d"
402 }); 339 });
403 340
404 assert!(has_d_tag); 341 assert!(has_d_tag);
405} 342}
406 343
@@ -409,7 +346,7 @@ fn test_announcement_has_d_tag() {
409fn test_announcement_correct_kind() { 346fn test_announcement_correct_kind() {
410 let keys = Keys::generate(); 347 let keys = Keys::generate();
411 let event = create_test_announcement(&keys, "my-repo"); 348 let event = create_test_announcement(&keys, "my-repo");
412 349
413 assert_eq!(event.kind.as_u16(), KIND_REPOSITORY_ANNOUNCEMENT); 350 assert_eq!(event.kind.as_u16(), KIND_REPOSITORY_ANNOUNCEMENT);
414} 351}
415 352
@@ -418,7 +355,7 @@ fn test_announcement_correct_kind() {
418fn test_maintainer_list_correct_kind() { 355fn test_maintainer_list_correct_kind() {
419 let keys = Keys::generate(); 356 let keys = Keys::generate();
420 let event = create_test_maintainer_list(&keys, "maintainers"); 357 let event = create_test_maintainer_list(&keys, "maintainers");
421 358
422 assert_eq!(event.kind.as_u16(), KIND_MAINTAINER_LIST); 359 assert_eq!(event.kind.as_u16(), KIND_MAINTAINER_LIST);
423} 360}
424 361
@@ -428,12 +365,12 @@ fn test_pr_event_has_a_tag() {
428 let keys = Keys::generate(); 365 let keys = Keys::generate();
429 let coord = "30617:pubkey123:my-repo"; 366 let coord = "30617:pubkey123:my-repo";
430 let event = create_test_pr_event(&keys, coord); 367 let event = create_test_pr_event(&keys, coord);
431 368
432 let has_a_tag = event.tags.iter().any(|tag| { 369 let has_a_tag = event.tags.iter().any(|tag| {
433 let tag_vec = tag.clone().to_vec(); 370 let tag_vec = tag.clone().to_vec();
434 tag_vec.len() >= 2 && tag_vec[0] == "a" 371 tag_vec.len() >= 2 && tag_vec[0] == "a"
435 }); 372 });
436 373
437 assert!(has_a_tag); 374 assert!(has_a_tag);
438} 375}
439 376
@@ -443,12 +380,12 @@ fn test_issue_event_has_a_tag() {
443 let keys = Keys::generate(); 380 let keys = Keys::generate();
444 let coord = "30617:pubkey123:my-repo"; 381 let coord = "30617:pubkey123:my-repo";
445 let event = create_test_issue_event(&keys, coord); 382 let event = create_test_issue_event(&keys, coord);
446 383
447 let has_a_tag = event.tags.iter().any(|tag| { 384 let has_a_tag = event.tags.iter().any(|tag| {
448 let tag_vec = tag.clone().to_vec(); 385 let tag_vec = tag.clone().to_vec();
449 tag_vec.len() >= 2 && tag_vec[0] == "a" 386 tag_vec.len() >= 2 && tag_vec[0] == "a"
450 }); 387 });
451 388
452 assert!(has_a_tag); 389 assert!(has_a_tag);
453} 390}
454 391
@@ -458,12 +395,12 @@ fn test_reply_event_has_e_tag() {
458 let keys = Keys::generate(); 395 let keys = Keys::generate();
459 let event_id = "abc123def456"; 396 let event_id = "abc123def456";
460 let event = create_test_reply_event(&keys, event_id); 397 let event = create_test_reply_event(&keys, event_id);
461 398
462 let has_e_tag = event.tags.iter().any(|tag| { 399 let has_e_tag = event.tags.iter().any(|tag| {
463 let tag_vec = tag.clone().to_vec(); 400 let tag_vec = tag.clone().to_vec();
464 tag_vec.len() >= 2 && tag_vec[0] == "e" 401 tag_vec.len() >= 2 && tag_vec[0] == "e"
465 }); 402 });
466 403
467 assert!(has_e_tag); 404 assert!(has_e_tag);
468} 405}
469 406
@@ -477,50 +414,55 @@ fn test_subscription_lifecycle() {
477 let mut subscribed_announcements: HashSet<String> = HashSet::new(); 414 let mut subscribed_announcements: HashSet<String> = HashSet::new();
478 let mut subscribed_events: HashSet<String> = HashSet::new(); 415 let mut subscribed_events: HashSet<String> = HashSet::new();
479 let mut is_consolidated = false; 416 let mut is_consolidated = false;
480 417
481 // Initial state 418 // Initial state
482 let initial_count = 1 + subscribed_announcements.len() + subscribed_events.len(); 419 let initial_count = 1 + subscribed_announcements.len() + subscribed_events.len();
483 assert_eq!(initial_count, 1); 420 assert_eq!(initial_count, 1);
484 421
485 // Add some announcements 422 // Add some announcements
486 for i in 0..50 { 423 for i in 0..50 {
487 subscribed_announcements.insert(format!("ann_{}", i)); 424 subscribed_announcements.insert(format!("ann_{}", i));
488 } 425 }
489 426
490 let after_announcements = 1 + subscribed_announcements.len() + subscribed_events.len(); 427 let after_announcements = 1 + subscribed_announcements.len() + subscribed_events.len();
491 assert_eq!(after_announcements, 51); 428 assert_eq!(after_announcements, 51);
492 429
493 // Add some events 430 // Add some events
494 for i in 0..50 { 431 for i in 0..50 {
495 subscribed_events.insert(format!("evt_{}", i)); 432 subscribed_events.insert(format!("evt_{}", i));
496 } 433 }
497 434
498 let after_events = 1 + subscribed_announcements.len() + subscribed_events.len(); 435 let after_events = 1 + subscribed_announcements.len() + subscribed_events.len();
499 assert_eq!(after_events, 101); 436 assert_eq!(after_events, 101);
500 437
501 // Add more to exceed threshold 438 // Add more to exceed threshold
502 for i in 50..100 { 439 for i in 50..100 {
503 subscribed_announcements.insert(format!("ann_{}", i)); 440 subscribed_announcements.insert(format!("ann_{}", i));
504 } 441 }
505 442
506 let before_consolidation = 1 + subscribed_announcements.len() + subscribed_events.len(); 443 let before_consolidation = 1 + subscribed_announcements.len() + subscribed_events.len();
507 assert_eq!(before_consolidation, 151); 444 assert_eq!(before_consolidation, 151);
508 445
509 // Should trigger consolidation 446 // Should trigger consolidation
510 let should_consolidate = !is_consolidated && before_consolidation > CONSOLIDATION_THRESHOLD; 447 let should_consolidate = !is_consolidated && before_consolidation > CONSOLIDATION_THRESHOLD;
511 assert!(should_consolidate); 448 assert!(should_consolidate);
512 449
513 // Consolidate 450 // Consolidate
514 subscribed_announcements.clear(); 451 subscribed_announcements.clear();
515 subscribed_events.clear(); 452 subscribed_events.clear();
516 is_consolidated = true; 453 is_consolidated = true;
517 454
518 // After consolidation 455 // After consolidation
519 let after_consolidation = if is_consolidated { 1 } else { 1 + subscribed_announcements.len() + subscribed_events.len() }; 456 let after_consolidation = if is_consolidated {
457 1
458 } else {
459 1 + subscribed_announcements.len() + subscribed_events.len()
460 };
520 assert_eq!(after_consolidation, 1); 461 assert_eq!(after_consolidation, 1);
521 462
522 // Should not trigger consolidation again 463 // Should not trigger consolidation again
523 let should_consolidate_again = !is_consolidated && after_consolidation > CONSOLIDATION_THRESHOLD; 464 let should_consolidate_again =
465 !is_consolidated && after_consolidation > CONSOLIDATION_THRESHOLD;
524 assert!(!should_consolidate_again); 466 assert!(!should_consolidate_again);
525} 467}
526 468
@@ -528,11 +470,11 @@ fn test_subscription_lifecycle() {
528#[test] 470#[test]
529fn test_consolidated_blocks_additions() { 471fn test_consolidated_blocks_additions() {
530 let is_consolidated = true; 472 let is_consolidated = true;
531 473
532 // When consolidated, add_announcement should return None (simulated) 474 // When consolidated, add_announcement should return None (simulated)
533 // The logic is: if is_consolidated, return None 475 // The logic is: if is_consolidated, return None
534 let should_add = !is_consolidated; 476 let should_add = !is_consolidated;
535 477
536 assert!(!should_add); 478 assert!(!should_add);
537} 479}
538 480
@@ -542,12 +484,12 @@ fn test_non_consolidated_allows_additions() {
542 let is_consolidated = false; 484 let is_consolidated = false;
543 let mut subscribed_announcements: HashSet<String> = HashSet::new(); 485 let mut subscribed_announcements: HashSet<String> = HashSet::new();
544 let event_id = "new_announcement"; 486 let event_id = "new_announcement";
545 487
546 // When not consolidated and event not in set, should add 488 // When not consolidated and event not in set, should add
547 let should_add = !is_consolidated && !subscribed_announcements.contains(event_id); 489 let should_add = !is_consolidated && !subscribed_announcements.contains(event_id);
548 490
549 assert!(should_add); 491 assert!(should_add);
550 492
551 subscribed_announcements.insert(event_id.to_string()); 493 subscribed_announcements.insert(event_id.to_string());
552 assert!(subscribed_announcements.contains(event_id)); 494 assert!(subscribed_announcements.contains(event_id));
553} 495}
@@ -562,7 +504,7 @@ fn test_announcement_coordinate_format() {
562 let keys = Keys::generate(); 504 let keys = Keys::generate();
563 let identifier = "my-repo"; 505 let identifier = "my-repo";
564 let event = create_test_announcement(&keys, identifier); 506 let event = create_test_announcement(&keys, identifier);
565 507
566 // Extract d tag 508 // Extract d tag
567 let d_tag = event.tags.iter().find_map(|tag| { 509 let d_tag = event.tags.iter().find_map(|tag| {
568 let tag_vec = tag.clone().to_vec(); 510 let tag_vec = tag.clone().to_vec();
@@ -572,13 +514,18 @@ fn test_announcement_coordinate_format() {
572 None 514 None
573 } 515 }
574 }); 516 });
575 517
576 assert!(d_tag.is_some()); 518 assert!(d_tag.is_some());
577 assert_eq!(d_tag.unwrap(), identifier); 519 assert_eq!(d_tag.unwrap(), identifier);
578 520
579 // Build coordinate: kind:pubkey:identifier 521 // Build coordinate: kind:pubkey:identifier
580 let coord = format!("{}:{}:{}", KIND_REPOSITORY_ANNOUNCEMENT, event.pubkey.to_hex(), identifier); 522 let coord = format!(
581 523 "{}:{}:{}",
524 KIND_REPOSITORY_ANNOUNCEMENT,
525 event.pubkey.to_hex(),
526 identifier
527 );
528
582 // Verify format 529 // Verify format
583 let parts: Vec<&str> = coord.split(':').collect(); 530 let parts: Vec<&str> = coord.split(':').collect();
584 assert_eq!(parts.len(), 3); 531 assert_eq!(parts.len(), 3);
@@ -590,16 +537,21 @@ fn test_announcement_coordinate_format() {
590#[test] 537#[test]
591fn test_multiple_announcement_coordinates_unique() { 538fn test_multiple_announcement_coordinates_unique() {
592 let keys = Keys::generate(); 539 let keys = Keys::generate();
593 540
594 let identifiers = vec!["repo1", "repo2", "repo3"]; 541 let identifiers = vec!["repo1", "repo2", "repo3"];
595 let mut coords: HashSet<String> = HashSet::new(); 542 let mut coords: HashSet<String> = HashSet::new();
596 543
597 for id in identifiers { 544 for id in identifiers {
598 let event = create_test_announcement(&keys, id); 545 let event = create_test_announcement(&keys, id);
599 let coord = format!("{}:{}:{}", KIND_REPOSITORY_ANNOUNCEMENT, event.pubkey.to_hex(), id); 546 let coord = format!(
547 "{}:{}:{}",
548 KIND_REPOSITORY_ANNOUNCEMENT,
549 event.pubkey.to_hex(),
550 id
551 );
600 coords.insert(coord); 552 coords.insert(coord);
601 } 553 }
602 554
603 assert_eq!(coords.len(), 3); 555 assert_eq!(coords.len(), 3);
604} 556}
605 557
@@ -614,30 +566,34 @@ fn test_workflow_announcement_then_pr() {
614 let mut subscribed_announcements: HashSet<String> = HashSet::new(); 566 let mut subscribed_announcements: HashSet<String> = HashSet::new();
615 let mut subscribed_events: HashSet<String> = HashSet::new(); 567 let mut subscribed_events: HashSet<String> = HashSet::new();
616 let is_consolidated = false; 568 let is_consolidated = false;
617 569
618 // Step 1: Receive announcement 570 // Step 1: Receive announcement
619 let announcement = create_test_announcement(&keys, "my-repo"); 571 let announcement = create_test_announcement(&keys, "my-repo");
620 let ann_id = announcement.id.to_hex(); 572 let ann_id = announcement.id.to_hex();
621 573
622 // Should add to tracking (simulating add_announcement) 574 // Should add to tracking (simulating add_announcement)
623 let should_add_ann = !is_consolidated && !subscribed_announcements.contains(&ann_id); 575 let should_add_ann = !is_consolidated && !subscribed_announcements.contains(&ann_id);
624 assert!(should_add_ann); 576 assert!(should_add_ann);
625 subscribed_announcements.insert(ann_id.clone()); 577 subscribed_announcements.insert(ann_id.clone());
626 578
627 // Filter count should increase 579 // Filter count should increase
628 let filter_count = 1 + subscribed_announcements.len() + subscribed_events.len(); 580 let filter_count = 1 + subscribed_announcements.len() + subscribed_events.len();
629 assert_eq!(filter_count, 2); 581 assert_eq!(filter_count, 2);
630 582
631 // Step 2: Receive PR for that repo 583 // Step 2: Receive PR for that repo
632 let coord = format!("{}:{}:my-repo", KIND_REPOSITORY_ANNOUNCEMENT, keys.public_key().to_hex()); 584 let coord = format!(
585 "{}:{}:my-repo",
586 KIND_REPOSITORY_ANNOUNCEMENT,
587 keys.public_key().to_hex()
588 );
633 let pr = create_test_pr_event(&keys, &coord); 589 let pr = create_test_pr_event(&keys, &coord);
634 let pr_id = pr.id.to_hex(); 590 let pr_id = pr.id.to_hex();
635 591
636 // Should add to tracking (simulating add_event) 592 // Should add to tracking (simulating add_event)
637 let should_add_pr = !is_consolidated && !subscribed_events.contains(&pr_id); 593 let should_add_pr = !is_consolidated && !subscribed_events.contains(&pr_id);
638 assert!(should_add_pr); 594 assert!(should_add_pr);
639 subscribed_events.insert(pr_id.clone()); 595 subscribed_events.insert(pr_id.clone());
640 596
641 // Filter count should increase again 597 // Filter count should increase again
642 let filter_count = 1 + subscribed_announcements.len() + subscribed_events.len(); 598 let filter_count = 1 + subscribed_announcements.len() + subscribed_events.len();
643 assert_eq!(filter_count, 3); 599 assert_eq!(filter_count, 3);
@@ -651,16 +607,16 @@ fn test_stress_many_items_triggers_consolidation() {
651 let mut subscribed_events: HashSet<String> = HashSet::new(); 607 let mut subscribed_events: HashSet<String> = HashSet::new();
652 let mut is_consolidated = false; 608 let mut is_consolidated = false;
653 let mut consolidation_triggered = false; 609 let mut consolidation_triggered = false;
654 610
655 // Add 100 announcements 611 // Add 100 announcements
656 for i in 0..100 { 612 for i in 0..100 {
657 let event = create_test_announcement(&keys, &format!("repo-{}", i)); 613 let event = create_test_announcement(&keys, &format!("repo-{}", i));
658 let event_id = event.id.to_hex(); 614 let event_id = event.id.to_hex();
659 615
660 if !is_consolidated && !subscribed_announcements.contains(&event_id) { 616 if !is_consolidated && !subscribed_announcements.contains(&event_id) {
661 subscribed_announcements.insert(event_id); 617 subscribed_announcements.insert(event_id);
662 } 618 }
663 619
664 // Check consolidation after each add 620 // Check consolidation after each add
665 let filter_count = 1 + subscribed_announcements.len() + subscribed_events.len(); 621 let filter_count = 1 + subscribed_announcements.len() + subscribed_events.len();
666 if !is_consolidated && filter_count > CONSOLIDATION_THRESHOLD { 622 if !is_consolidated && filter_count > CONSOLIDATION_THRESHOLD {
@@ -671,18 +627,18 @@ fn test_stress_many_items_triggers_consolidation() {
671 break; 627 break;
672 } 628 }
673 } 629 }
674 630
675 // If we didn't consolidate yet, add events 631 // If we didn't consolidate yet, add events
676 if !consolidation_triggered { 632 if !consolidation_triggered {
677 for i in 0..100 { 633 for i in 0..100 {
678 let coord = format!("30617:pubkey:repo-{}", i); 634 let coord = format!("30617:pubkey:repo-{}", i);
679 let event = create_test_pr_event(&keys, &coord); 635 let event = create_test_pr_event(&keys, &coord);
680 let event_id = event.id.to_hex(); 636 let event_id = event.id.to_hex();
681 637
682 if !is_consolidated && !subscribed_events.contains(&event_id) { 638 if !is_consolidated && !subscribed_events.contains(&event_id) {
683 subscribed_events.insert(event_id); 639 subscribed_events.insert(event_id);
684 } 640 }
685 641
686 // Check consolidation after each add 642 // Check consolidation after each add
687 let filter_count = 1 + subscribed_announcements.len() + subscribed_events.len(); 643 let filter_count = 1 + subscribed_announcements.len() + subscribed_events.len();
688 if !is_consolidated && filter_count > CONSOLIDATION_THRESHOLD { 644 if !is_consolidated && filter_count > CONSOLIDATION_THRESHOLD {
@@ -694,55 +650,12 @@ fn test_stress_many_items_triggers_consolidation() {
694 } 650 }
695 } 651 }
696 } 652 }
697 653
698 // Consolidation should have been triggered 654 // Consolidation should have been triggered
699 assert!(consolidation_triggered); 655 assert!(consolidation_triggered);
700 assert!(is_consolidated); 656 assert!(is_consolidated);
701 657
702 // After consolidation, counts should be reset 658 // After consolidation, counts should be reset
703 assert_eq!(subscribed_announcements.len(), 0); 659 assert_eq!(subscribed_announcements.len(), 0);
704 assert_eq!(subscribed_events.len(), 0); 660 assert_eq!(subscribed_events.len(), 0);
705} 661}
706
707/// Test that all PR/Issue kinds are handled consistently
708#[test]
709fn test_all_pr_issue_kinds_handled() {
710 let keys = Keys::generate();
711 let coord = "30617:pubkey:repo";
712
713 // All these kinds should be identified as PR/Issue
714 let pr_kinds = vec![1617, 1618, 1619, 1621, 1622];
715
716 for kind in pr_kinds {
717 assert!(
718 SubscriptionManager::is_pr_issue_kind(kind),
719 "Kind {} should be identified as PR/Issue",
720 kind
721 );
722 }
723}
724
725/// Test that announcement and PR/Issue kinds are mutually exclusive
726#[test]
727fn test_kind_categories_mutually_exclusive() {
728 let announcement_kinds = vec![30617, 30618];
729 let pr_issue_kinds = vec![1617, 1618, 1619, 1621, 1622];
730
731 // No announcement kind should be a PR/Issue kind
732 for kind in &announcement_kinds {
733 assert!(
734 !SubscriptionManager::is_pr_issue_kind(*kind),
735 "Announcement kind {} should not be PR/Issue",
736 kind
737 );
738 }
739
740 // No PR/Issue kind should be an announcement kind
741 for kind in &pr_issue_kinds {
742 assert!(
743 !SubscriptionManager::is_announcement_kind(*kind),
744 "PR/Issue kind {} should not be announcement",
745 kind
746 );
747 }
748} \ No newline at end of file