upleb.uk

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

summaryrefslogtreecommitdiff
path: root/tests/proactive_sync_dynamic.rs
diff options
context:
space:
mode:
Diffstat (limited to 'tests/proactive_sync_dynamic.rs')
-rw-r--r--tests/proactive_sync_dynamic.rs661
1 files changed, 0 insertions, 661 deletions
diff --git a/tests/proactive_sync_dynamic.rs b/tests/proactive_sync_dynamic.rs
deleted file mode 100644
index 2d3232f..0000000
--- a/tests/proactive_sync_dynamic.rs
+++ /dev/null
@@ -1,661 +0,0 @@
1//! GRASP-02 Phase 4: Dynamic Subscription Integration Tests
2//!
3//! Tests verify dynamic subscription management:
4//! - New announcement triggers Layer 2 subscription
5//! - New PR/Issue triggers Layer 3 subscription
6//! - Subscription count tracking per connection
7//! - Consolidation at filter count > 150
8//! - No duplicate subscriptions
9//!
10//! # Running Tests
11//!
12//! ```bash
13//! cargo test --test proactive_sync_dynamic
14//! cargo test --test proactive_sync_dynamic -- --nocapture
15//! ```
16
17use std::collections::HashSet;
18
19use ngit_grasp::sync::SubscriptionManager;
20use nostr_sdk::prelude::*;
21
22/// Kind 30617 - Repository Announcement (NIP-34)
23const KIND_REPOSITORY_ANNOUNCEMENT: u16 = 30617;
24
25/// Kind 30618 - Maintainer List (NIP-34)
26const KIND_MAINTAINER_LIST: u16 = 30618;
27
28/// Maximum filters before consolidation (from spec)
29const CONSOLIDATION_THRESHOLD: usize = 150;
30
31/// Helper to create a test announcement event
32fn create_test_announcement(keys: &Keys, identifier: &str) -> Event {
33 let tags = vec![
34 Tag::identifier(identifier),
35 Tag::custom(
36 TagKind::custom("clone"),
37 vec![format!("http://test.example.com/{}", identifier)],
38 ),
39 Tag::custom(
40 TagKind::custom("relays"),
41 vec!["ws://test.example.com".to_string()],
42 ),
43 ];
44
45 EventBuilder::new(Kind::Custom(KIND_REPOSITORY_ANNOUNCEMENT), "Test repo")
46 .tags(tags)
47 .sign_with_keys(keys)
48 .expect("Failed to sign event")
49}
50
51/// Helper to create a test maintainer list event
52fn create_test_maintainer_list(keys: &Keys, identifier: &str) -> Event {
53 let tags = vec![
54 Tag::identifier(identifier),
55 Tag::custom(
56 TagKind::custom("relays"),
57 vec!["ws://test.example.com".to_string()],
58 ),
59 ];
60
61 EventBuilder::new(Kind::Custom(KIND_MAINTAINER_LIST), "Maintainer list")
62 .tags(tags)
63 .sign_with_keys(keys)
64 .expect("Failed to sign event")
65}
66
67/// Helper to create a test PR event (kind 1617)
68fn create_test_pr_event(keys: &Keys, repo_coord: &str) -> Event {
69 let tags = vec![Tag::custom(
70 TagKind::custom("a"),
71 vec![repo_coord.to_string()],
72 )];
73
74 EventBuilder::new(Kind::Custom(1617), "Test patch proposal")
75 .tags(tags)
76 .sign_with_keys(keys)
77 .expect("Failed to sign event")
78}
79
80/// Helper to create a test PR event (kind 1618)
81fn create_test_pr_1618_event(keys: &Keys, repo_coord: &str) -> Event {
82 let tags = vec![Tag::custom(
83 TagKind::custom("a"),
84 vec![repo_coord.to_string()],
85 )];
86
87 EventBuilder::new(Kind::Custom(1618), "Test PR")
88 .tags(tags)
89 .sign_with_keys(keys)
90 .expect("Failed to sign event")
91}
92
93/// Helper to create a test Issue event (kind 1621)
94fn create_test_issue_event(keys: &Keys, repo_coord: &str) -> Event {
95 let tags = vec![Tag::custom(
96 TagKind::custom("a"),
97 vec![repo_coord.to_string()],
98 )];
99
100 EventBuilder::new(Kind::Custom(1621), "Test issue")
101 .tags(tags)
102 .sign_with_keys(keys)
103 .expect("Failed to sign event")
104}
105
106/// Helper to create a test Reply event (kind 1622)
107fn create_test_reply_event(keys: &Keys, event_id: &str) -> Event {
108 let tags = vec![Tag::custom(
109 TagKind::custom("e"),
110 vec![event_id.to_string()],
111 )];
112
113 EventBuilder::new(Kind::Custom(1622), "Test reply")
114 .tags(tags)
115 .sign_with_keys(keys)
116 .expect("Failed to sign event")
117}
118
119// ============================================================================
120// Filter Count Tests
121// ============================================================================
122
123/// Test initial filter count is 1 (Layer 1 only)
124#[test]
125fn test_initial_filter_count() {
126 // Create a minimal SubscriptionManager-like state for testing
127 // We test the logic without needing a full FilterService
128
129 // Initial state: 0 announcements, 0 events, not consolidated
130 // Filter count should be: 1 (Layer 1) + 0 + 0 = 1
131 let announcement_count = 0;
132 let event_count = 0;
133 let is_consolidated = false;
134
135 let filter_count = if is_consolidated {
136 1
137 } else {
138 1 + announcement_count + event_count
139 };
140
141 assert_eq!(filter_count, 1);
142}
143
144/// Test filter count increases with announcements
145#[test]
146fn test_filter_count_with_announcements() {
147 let announcement_count = 5;
148 let event_count = 0;
149 let is_consolidated = false;
150
151 let filter_count = if is_consolidated {
152 1
153 } else {
154 1 + announcement_count + event_count
155 };
156
157 // 1 (Layer 1) + 5 (announcements) = 6
158 assert_eq!(filter_count, 6);
159}
160
161/// Test filter count increases with events
162#[test]
163fn test_filter_count_with_events() {
164 let announcement_count = 0;
165 let event_count = 10;
166 let is_consolidated = false;
167
168 let filter_count = if is_consolidated {
169 1
170 } else {
171 1 + announcement_count + event_count
172 };
173
174 // 1 (Layer 1) + 10 (events) = 11
175 assert_eq!(filter_count, 11);
176}
177
178/// Test filter count with both announcements and events
179#[test]
180fn test_filter_count_mixed() {
181 let announcement_count = 50;
182 let event_count = 30;
183 let is_consolidated = false;
184
185 let filter_count = if is_consolidated {
186 1
187 } else {
188 1 + announcement_count + event_count
189 };
190
191 // 1 + 50 + 30 = 81
192 assert_eq!(filter_count, 81);
193}
194
195/// Test filter count is 1 when consolidated
196#[test]
197fn test_filter_count_consolidated() {
198 let announcement_count = 100; // These would be cleared on consolidation
199 let event_count = 100;
200 let is_consolidated = true;
201
202 let filter_count = if is_consolidated {
203 1
204 } else {
205 1 + announcement_count + event_count
206 };
207
208 assert_eq!(filter_count, 1);
209}
210
211// ============================================================================
212// Consolidation Threshold Tests
213// ============================================================================
214
215/// Test consolidation is not triggered below threshold
216#[test]
217fn test_should_consolidate_below_threshold() {
218 let filter_count = 100;
219 let is_consolidated = false;
220
221 let should_consolidate = !is_consolidated && filter_count > CONSOLIDATION_THRESHOLD;
222
223 assert!(!should_consolidate);
224}
225
226/// Test consolidation is triggered at threshold
227#[test]
228fn test_should_consolidate_at_threshold() {
229 let filter_count = 151; // > 150
230 let is_consolidated = false;
231
232 let should_consolidate = !is_consolidated && filter_count > CONSOLIDATION_THRESHOLD;
233
234 assert!(should_consolidate);
235}
236
237/// Test consolidation is triggered well above threshold
238#[test]
239fn test_should_consolidate_above_threshold() {
240 let filter_count = 200;
241 let is_consolidated = false;
242
243 let should_consolidate = !is_consolidated && filter_count > CONSOLIDATION_THRESHOLD;
244
245 assert!(should_consolidate);
246}
247
248/// Test consolidation is not triggered if already consolidated
249#[test]
250fn test_should_consolidate_already_consolidated() {
251 let filter_count = 200; // Would trigger, but already consolidated
252 let is_consolidated = true;
253
254 let should_consolidate = !is_consolidated && filter_count > CONSOLIDATION_THRESHOLD;
255
256 assert!(!should_consolidate);
257}
258
259/// Test exact threshold boundary (150 should NOT trigger, 151 should)
260#[test]
261fn test_consolidation_threshold_boundary() {
262 let is_consolidated = false;
263
264 // 150 should NOT trigger (> 150, not >= 150)
265 let should_consolidate_at_150 = !is_consolidated && 150 > CONSOLIDATION_THRESHOLD;
266 assert!(!should_consolidate_at_150);
267
268 // 151 should trigger
269 let should_consolidate_at_151 = !is_consolidated && 151 > CONSOLIDATION_THRESHOLD;
270 assert!(should_consolidate_at_151);
271}
272
273// ============================================================================
274// Duplicate Prevention Tests
275// ============================================================================
276
277/// Test duplicate announcement detection
278#[test]
279fn test_duplicate_announcement_prevention() {
280 let mut subscribed_announcements: HashSet<String> = HashSet::new();
281
282 let event_id = "abc123".to_string();
283
284 // First add should succeed
285 let is_new = !subscribed_announcements.contains(&event_id);
286 assert!(is_new);
287 subscribed_announcements.insert(event_id.clone());
288
289 // Second add should fail (duplicate)
290 let is_new_again = !subscribed_announcements.contains(&event_id);
291 assert!(!is_new_again);
292}
293
294/// Test duplicate event detection
295#[test]
296fn test_duplicate_event_prevention() {
297 let mut subscribed_events: HashSet<String> = HashSet::new();
298
299 let event_id = "def456".to_string();
300
301 // First add should succeed
302 let is_new = !subscribed_events.contains(&event_id);
303 assert!(is_new);
304 subscribed_events.insert(event_id.clone());
305
306 // Second add should fail (duplicate)
307 let is_new_again = !subscribed_events.contains(&event_id);
308 assert!(!is_new_again);
309}
310
311/// Test multiple unique items are tracked correctly
312#[test]
313fn test_multiple_unique_items_tracked() {
314 let mut subscribed_announcements: HashSet<String> = HashSet::new();
315
316 // Add multiple unique announcements
317 for i in 0..10 {
318 let id = format!("announcement_{}", i);
319 assert!(!subscribed_announcements.contains(&id));
320 subscribed_announcements.insert(id);
321 }
322
323 assert_eq!(subscribed_announcements.len(), 10);
324}
325
326// ============================================================================
327// Event Creation and Validation Tests
328// ============================================================================
329
330/// Test announcement event has required d tag
331#[test]
332fn test_announcement_has_d_tag() {
333 let keys = Keys::generate();
334 let event = create_test_announcement(&keys, "my-repo");
335
336 let has_d_tag = event.tags.iter().any(|tag| {
337 let tag_vec = tag.clone().to_vec();
338 tag_vec.len() >= 2 && tag_vec[0] == "d"
339 });
340
341 assert!(has_d_tag);
342}
343
344/// Test announcement event has correct kind
345#[test]
346fn test_announcement_correct_kind() {
347 let keys = Keys::generate();
348 let event = create_test_announcement(&keys, "my-repo");
349
350 assert_eq!(event.kind.as_u16(), KIND_REPOSITORY_ANNOUNCEMENT);
351}
352
353/// Test maintainer list event has correct kind
354#[test]
355fn test_maintainer_list_correct_kind() {
356 let keys = Keys::generate();
357 let event = create_test_maintainer_list(&keys, "maintainers");
358
359 assert_eq!(event.kind.as_u16(), KIND_MAINTAINER_LIST);
360}
361
362/// Test PR event has a tag
363#[test]
364fn test_pr_event_has_a_tag() {
365 let keys = Keys::generate();
366 let coord = "30617:pubkey123:my-repo";
367 let event = create_test_pr_event(&keys, coord);
368
369 let has_a_tag = event.tags.iter().any(|tag| {
370 let tag_vec = tag.clone().to_vec();
371 tag_vec.len() >= 2 && tag_vec[0] == "a"
372 });
373
374 assert!(has_a_tag);
375}
376
377/// Test issue event has a tag
378#[test]
379fn test_issue_event_has_a_tag() {
380 let keys = Keys::generate();
381 let coord = "30617:pubkey123:my-repo";
382 let event = create_test_issue_event(&keys, coord);
383
384 let has_a_tag = event.tags.iter().any(|tag| {
385 let tag_vec = tag.clone().to_vec();
386 tag_vec.len() >= 2 && tag_vec[0] == "a"
387 });
388
389 assert!(has_a_tag);
390}
391
392/// Test reply event has e tag
393#[test]
394fn test_reply_event_has_e_tag() {
395 let keys = Keys::generate();
396 let event_id = "abc123def456";
397 let event = create_test_reply_event(&keys, event_id);
398
399 let has_e_tag = event.tags.iter().any(|tag| {
400 let tag_vec = tag.clone().to_vec();
401 tag_vec.len() >= 2 && tag_vec[0] == "e"
402 });
403
404 assert!(has_e_tag);
405}
406
407// ============================================================================
408// Subscription Lifecycle Tests
409// ============================================================================
410
411/// Test subscription lifecycle: initial -> add announcements -> add events -> consolidate
412#[test]
413fn test_subscription_lifecycle() {
414 let mut subscribed_announcements: HashSet<String> = HashSet::new();
415 let mut subscribed_events: HashSet<String> = HashSet::new();
416 let mut is_consolidated = false;
417
418 // Initial state
419 let initial_count = 1 + subscribed_announcements.len() + subscribed_events.len();
420 assert_eq!(initial_count, 1);
421
422 // Add some announcements
423 for i in 0..50 {
424 subscribed_announcements.insert(format!("ann_{}", i));
425 }
426
427 let after_announcements = 1 + subscribed_announcements.len() + subscribed_events.len();
428 assert_eq!(after_announcements, 51);
429
430 // Add some events
431 for i in 0..50 {
432 subscribed_events.insert(format!("evt_{}", i));
433 }
434
435 let after_events = 1 + subscribed_announcements.len() + subscribed_events.len();
436 assert_eq!(after_events, 101);
437
438 // Add more to exceed threshold
439 for i in 50..100 {
440 subscribed_announcements.insert(format!("ann_{}", i));
441 }
442
443 let before_consolidation = 1 + subscribed_announcements.len() + subscribed_events.len();
444 assert_eq!(before_consolidation, 151);
445
446 // Should trigger consolidation
447 let should_consolidate = !is_consolidated && before_consolidation > CONSOLIDATION_THRESHOLD;
448 assert!(should_consolidate);
449
450 // Consolidate
451 subscribed_announcements.clear();
452 subscribed_events.clear();
453 is_consolidated = true;
454
455 // After consolidation
456 let after_consolidation = if is_consolidated {
457 1
458 } else {
459 1 + subscribed_announcements.len() + subscribed_events.len()
460 };
461 assert_eq!(after_consolidation, 1);
462
463 // Should not trigger consolidation again
464 let should_consolidate_again =
465 !is_consolidated && after_consolidation > CONSOLIDATION_THRESHOLD;
466 assert!(!should_consolidate_again);
467}
468
469/// Test that consolidated state blocks new additions
470#[test]
471fn test_consolidated_blocks_additions() {
472 let is_consolidated = true;
473
474 // When consolidated, add_announcement should return None (simulated)
475 // The logic is: if is_consolidated, return None
476 let should_add = !is_consolidated;
477
478 assert!(!should_add);
479}
480
481/// Test that non-consolidated state allows additions
482#[test]
483fn test_non_consolidated_allows_additions() {
484 let is_consolidated = false;
485 let mut subscribed_announcements: HashSet<String> = HashSet::new();
486 let event_id = "new_announcement";
487
488 // When not consolidated and event not in set, should add
489 let should_add = !is_consolidated && !subscribed_announcements.contains(event_id);
490
491 assert!(should_add);
492
493 subscribed_announcements.insert(event_id.to_string());
494 assert!(subscribed_announcements.contains(event_id));
495}
496
497// ============================================================================
498// Filter Building Tests (coordinate format)
499// ============================================================================
500
501/// Test announcement coordinate format
502#[test]
503fn test_announcement_coordinate_format() {
504 let keys = Keys::generate();
505 let identifier = "my-repo";
506 let event = create_test_announcement(&keys, identifier);
507
508 // Extract d tag
509 let d_tag = event.tags.iter().find_map(|tag| {
510 let tag_vec = tag.clone().to_vec();
511 if tag_vec.len() >= 2 && tag_vec[0] == "d" {
512 Some(tag_vec[1].clone())
513 } else {
514 None
515 }
516 });
517
518 assert!(d_tag.is_some());
519 assert_eq!(d_tag.unwrap(), identifier);
520
521 // Build coordinate: kind:pubkey:identifier
522 let coord = format!(
523 "{}:{}:{}",
524 KIND_REPOSITORY_ANNOUNCEMENT,
525 event.pubkey.to_hex(),
526 identifier
527 );
528
529 // Verify format
530 let parts: Vec<&str> = coord.split(':').collect();
531 assert_eq!(parts.len(), 3);
532 assert_eq!(parts[0], "30617");
533 assert_eq!(parts[2], identifier);
534}
535
536/// Test multiple announcement coordinates are unique
537#[test]
538fn test_multiple_announcement_coordinates_unique() {
539 let keys = Keys::generate();
540
541 let identifiers = vec!["repo1", "repo2", "repo3"];
542 let mut coords: HashSet<String> = HashSet::new();
543
544 for id in identifiers {
545 let event = create_test_announcement(&keys, id);
546 let coord = format!(
547 "{}:{}:{}",
548 KIND_REPOSITORY_ANNOUNCEMENT,
549 event.pubkey.to_hex(),
550 id
551 );
552 coords.insert(coord);
553 }
554
555 assert_eq!(coords.len(), 3);
556}
557
558// ============================================================================
559// Integration-style Tests
560// ============================================================================
561
562/// Test simulated workflow: announcement received, then PR received
563#[test]
564fn test_workflow_announcement_then_pr() {
565 let keys = Keys::generate();
566 let mut subscribed_announcements: HashSet<String> = HashSet::new();
567 let mut subscribed_events: HashSet<String> = HashSet::new();
568 let is_consolidated = false;
569
570 // Step 1: Receive announcement
571 let announcement = create_test_announcement(&keys, "my-repo");
572 let ann_id = announcement.id.to_hex();
573
574 // Should add to tracking (simulating add_announcement)
575 let should_add_ann = !is_consolidated && !subscribed_announcements.contains(&ann_id);
576 assert!(should_add_ann);
577 subscribed_announcements.insert(ann_id.clone());
578
579 // Filter count should increase
580 let filter_count = 1 + subscribed_announcements.len() + subscribed_events.len();
581 assert_eq!(filter_count, 2);
582
583 // Step 2: Receive PR for that repo
584 let coord = format!(
585 "{}:{}:my-repo",
586 KIND_REPOSITORY_ANNOUNCEMENT,
587 keys.public_key().to_hex()
588 );
589 let pr = create_test_pr_event(&keys, &coord);
590 let pr_id = pr.id.to_hex();
591
592 // Should add to tracking (simulating add_event)
593 let should_add_pr = !is_consolidated && !subscribed_events.contains(&pr_id);
594 assert!(should_add_pr);
595 subscribed_events.insert(pr_id.clone());
596
597 // Filter count should increase again
598 let filter_count = 1 + subscribed_announcements.len() + subscribed_events.len();
599 assert_eq!(filter_count, 3);
600}
601
602/// Test stress: adding many items triggers consolidation
603#[test]
604fn test_stress_many_items_triggers_consolidation() {
605 let keys = Keys::generate();
606 let mut subscribed_announcements: HashSet<String> = HashSet::new();
607 let mut subscribed_events: HashSet<String> = HashSet::new();
608 let mut is_consolidated = false;
609 let mut consolidation_triggered = false;
610
611 // Add 100 announcements
612 for i in 0..100 {
613 let event = create_test_announcement(&keys, &format!("repo-{}", i));
614 let event_id = event.id.to_hex();
615
616 if !is_consolidated && !subscribed_announcements.contains(&event_id) {
617 subscribed_announcements.insert(event_id);
618 }
619
620 // Check consolidation after each add
621 let filter_count = 1 + subscribed_announcements.len() + subscribed_events.len();
622 if !is_consolidated && filter_count > CONSOLIDATION_THRESHOLD {
623 consolidation_triggered = true;
624 subscribed_announcements.clear();
625 subscribed_events.clear();
626 is_consolidated = true;
627 break;
628 }
629 }
630
631 // If we didn't consolidate yet, add events
632 if !consolidation_triggered {
633 for i in 0..100 {
634 let coord = format!("30617:pubkey:repo-{}", i);
635 let event = create_test_pr_event(&keys, &coord);
636 let event_id = event.id.to_hex();
637
638 if !is_consolidated && !subscribed_events.contains(&event_id) {
639 subscribed_events.insert(event_id);
640 }
641
642 // Check consolidation after each add
643 let filter_count = 1 + subscribed_announcements.len() + subscribed_events.len();
644 if !is_consolidated && filter_count > CONSOLIDATION_THRESHOLD {
645 consolidation_triggered = true;
646 subscribed_announcements.clear();
647 subscribed_events.clear();
648 is_consolidated = true;
649 break;
650 }
651 }
652 }
653
654 // Consolidation should have been triggered
655 assert!(consolidation_triggered);
656 assert!(is_consolidated);
657
658 // After consolidation, counts should be reset
659 assert_eq!(subscribed_announcements.len(), 0);
660 assert_eq!(subscribed_events.len(), 0);
661}