mirror of
https://relay.ngit.dev/npub15qydau2hjma6ngxkl2cyar74wzyjshvl65za5k5rl69264ar2exs5cyejr/ngit-grasp.git
synced 2026-10-05 15:08:24 +00:00
- Add SubscriptionManager for per-connection tracking - Trigger subscription updates on new repo/PR events - Implement consolidation when filter count > 150
748 lines
23 KiB
Rust
748 lines
23 KiB
Rust
//! GRASP-02 Phase 4: Dynamic Subscription Integration Tests
|
|
//!
|
|
//! Tests verify dynamic subscription management:
|
|
//! - New announcement triggers Layer 2 subscription
|
|
//! - New PR/Issue triggers Layer 3 subscription
|
|
//! - Subscription count tracking per connection
|
|
//! - Consolidation at filter count > 150
|
|
//! - No duplicate subscriptions
|
|
//!
|
|
//! # Running Tests
|
|
//!
|
|
//! ```bash
|
|
//! cargo test --test proactive_sync_dynamic
|
|
//! cargo test --test proactive_sync_dynamic -- --nocapture
|
|
//! ```
|
|
|
|
use std::collections::HashSet;
|
|
|
|
use ngit_grasp::sync::SubscriptionManager;
|
|
use nostr_sdk::prelude::*;
|
|
|
|
/// Kind 30617 - Repository Announcement (NIP-34)
|
|
const KIND_REPOSITORY_ANNOUNCEMENT: u16 = 30617;
|
|
|
|
/// Kind 30618 - Maintainer List (NIP-34)
|
|
const KIND_MAINTAINER_LIST: u16 = 30618;
|
|
|
|
/// Maximum filters before consolidation (from spec)
|
|
const CONSOLIDATION_THRESHOLD: usize = 150;
|
|
|
|
/// Helper to create a test announcement event
|
|
fn create_test_announcement(keys: &Keys, identifier: &str) -> Event {
|
|
let tags = vec![
|
|
Tag::identifier(identifier),
|
|
Tag::custom(
|
|
TagKind::custom("clone"),
|
|
vec![format!("http://test.example.com/{}", identifier)],
|
|
),
|
|
Tag::custom(
|
|
TagKind::custom("relays"),
|
|
vec!["ws://test.example.com".to_string()],
|
|
),
|
|
];
|
|
|
|
EventBuilder::new(Kind::Custom(KIND_REPOSITORY_ANNOUNCEMENT), "Test repo")
|
|
.tags(tags)
|
|
.sign_with_keys(keys)
|
|
.expect("Failed to sign event")
|
|
}
|
|
|
|
/// Helper to create a test maintainer list event
|
|
fn create_test_maintainer_list(keys: &Keys, identifier: &str) -> Event {
|
|
let tags = vec![
|
|
Tag::identifier(identifier),
|
|
Tag::custom(
|
|
TagKind::custom("relays"),
|
|
vec!["ws://test.example.com".to_string()],
|
|
),
|
|
];
|
|
|
|
EventBuilder::new(Kind::Custom(KIND_MAINTAINER_LIST), "Maintainer list")
|
|
.tags(tags)
|
|
.sign_with_keys(keys)
|
|
.expect("Failed to sign event")
|
|
}
|
|
|
|
/// Helper to create a test PR event (kind 1617)
|
|
fn create_test_pr_event(keys: &Keys, repo_coord: &str) -> Event {
|
|
let tags = vec![Tag::custom(
|
|
TagKind::custom("a"),
|
|
vec![repo_coord.to_string()],
|
|
)];
|
|
|
|
EventBuilder::new(Kind::Custom(1617), "Test patch proposal")
|
|
.tags(tags)
|
|
.sign_with_keys(keys)
|
|
.expect("Failed to sign event")
|
|
}
|
|
|
|
/// Helper to create a test PR event (kind 1618)
|
|
fn create_test_pr_1618_event(keys: &Keys, repo_coord: &str) -> Event {
|
|
let tags = vec![Tag::custom(
|
|
TagKind::custom("a"),
|
|
vec![repo_coord.to_string()],
|
|
)];
|
|
|
|
EventBuilder::new(Kind::Custom(1618), "Test PR")
|
|
.tags(tags)
|
|
.sign_with_keys(keys)
|
|
.expect("Failed to sign event")
|
|
}
|
|
|
|
/// Helper to create a test Issue event (kind 1621)
|
|
fn create_test_issue_event(keys: &Keys, repo_coord: &str) -> Event {
|
|
let tags = vec![Tag::custom(
|
|
TagKind::custom("a"),
|
|
vec![repo_coord.to_string()],
|
|
)];
|
|
|
|
EventBuilder::new(Kind::Custom(1621), "Test issue")
|
|
.tags(tags)
|
|
.sign_with_keys(keys)
|
|
.expect("Failed to sign event")
|
|
}
|
|
|
|
/// Helper to create a test Reply event (kind 1622)
|
|
fn create_test_reply_event(keys: &Keys, event_id: &str) -> Event {
|
|
let tags = vec![Tag::custom(
|
|
TagKind::custom("e"),
|
|
vec![event_id.to_string()],
|
|
)];
|
|
|
|
EventBuilder::new(Kind::Custom(1622), "Test reply")
|
|
.tags(tags)
|
|
.sign_with_keys(keys)
|
|
.expect("Failed to sign event")
|
|
}
|
|
|
|
// ============================================================================
|
|
// Kind Detection Tests
|
|
// ============================================================================
|
|
|
|
/// Test that announcement kinds are correctly identified
|
|
#[test]
|
|
fn test_is_announcement_kind_30617() {
|
|
assert!(SubscriptionManager::is_announcement_kind(30617));
|
|
}
|
|
|
|
/// Test that maintainer list kind is correctly identified
|
|
#[test]
|
|
fn test_is_announcement_kind_30618() {
|
|
assert!(SubscriptionManager::is_announcement_kind(30618));
|
|
}
|
|
|
|
/// Test that non-announcement kinds are not identified as announcements
|
|
#[test]
|
|
fn test_is_announcement_kind_negative() {
|
|
assert!(!SubscriptionManager::is_announcement_kind(1)); // Text note
|
|
assert!(!SubscriptionManager::is_announcement_kind(1617)); // PR
|
|
assert!(!SubscriptionManager::is_announcement_kind(1621)); // Issue
|
|
assert!(!SubscriptionManager::is_announcement_kind(0)); // Unknown
|
|
}
|
|
|
|
/// Test that PR/Issue kinds are correctly identified
|
|
#[test]
|
|
fn test_is_pr_issue_kind_1617() {
|
|
assert!(SubscriptionManager::is_pr_issue_kind(1617)); // Patch proposal
|
|
}
|
|
|
|
/// Test that PR kind 1618 is correctly identified
|
|
#[test]
|
|
fn test_is_pr_issue_kind_1618() {
|
|
assert!(SubscriptionManager::is_pr_issue_kind(1618)); // PR
|
|
}
|
|
|
|
/// Test that PR update kind is correctly identified
|
|
#[test]
|
|
fn test_is_pr_issue_kind_1619() {
|
|
assert!(SubscriptionManager::is_pr_issue_kind(1619)); // PR Update
|
|
}
|
|
|
|
/// Test that Issue kind is correctly identified
|
|
#[test]
|
|
fn test_is_pr_issue_kind_1621() {
|
|
assert!(SubscriptionManager::is_pr_issue_kind(1621)); // Issue
|
|
}
|
|
|
|
/// Test that Reply kind is correctly identified
|
|
#[test]
|
|
fn test_is_pr_issue_kind_1622() {
|
|
assert!(SubscriptionManager::is_pr_issue_kind(1622)); // Reply
|
|
}
|
|
|
|
/// Test that non-PR/Issue kinds are not identified
|
|
#[test]
|
|
fn test_is_pr_issue_kind_negative() {
|
|
assert!(!SubscriptionManager::is_pr_issue_kind(30617)); // Announcement
|
|
assert!(!SubscriptionManager::is_pr_issue_kind(1)); // Text note
|
|
assert!(!SubscriptionManager::is_pr_issue_kind(0)); // Unknown
|
|
}
|
|
|
|
// ============================================================================
|
|
// Filter Count Tests
|
|
// ============================================================================
|
|
|
|
/// Test initial filter count is 1 (Layer 1 only)
|
|
#[test]
|
|
fn test_initial_filter_count() {
|
|
// Create a minimal SubscriptionManager-like state for testing
|
|
// We test the logic without needing a full FilterService
|
|
|
|
// Initial state: 0 announcements, 0 events, not consolidated
|
|
// Filter count should be: 1 (Layer 1) + 0 + 0 = 1
|
|
let announcement_count = 0;
|
|
let event_count = 0;
|
|
let is_consolidated = false;
|
|
|
|
let filter_count = if is_consolidated {
|
|
1
|
|
} else {
|
|
1 + announcement_count + event_count
|
|
};
|
|
|
|
assert_eq!(filter_count, 1);
|
|
}
|
|
|
|
/// Test filter count increases with announcements
|
|
#[test]
|
|
fn test_filter_count_with_announcements() {
|
|
let announcement_count = 5;
|
|
let event_count = 0;
|
|
let is_consolidated = false;
|
|
|
|
let filter_count = if is_consolidated {
|
|
1
|
|
} else {
|
|
1 + announcement_count + event_count
|
|
};
|
|
|
|
// 1 (Layer 1) + 5 (announcements) = 6
|
|
assert_eq!(filter_count, 6);
|
|
}
|
|
|
|
/// Test filter count increases with events
|
|
#[test]
|
|
fn test_filter_count_with_events() {
|
|
let announcement_count = 0;
|
|
let event_count = 10;
|
|
let is_consolidated = false;
|
|
|
|
let filter_count = if is_consolidated {
|
|
1
|
|
} else {
|
|
1 + announcement_count + event_count
|
|
};
|
|
|
|
// 1 (Layer 1) + 10 (events) = 11
|
|
assert_eq!(filter_count, 11);
|
|
}
|
|
|
|
/// Test filter count with both announcements and events
|
|
#[test]
|
|
fn test_filter_count_mixed() {
|
|
let announcement_count = 50;
|
|
let event_count = 30;
|
|
let is_consolidated = false;
|
|
|
|
let filter_count = if is_consolidated {
|
|
1
|
|
} else {
|
|
1 + announcement_count + event_count
|
|
};
|
|
|
|
// 1 + 50 + 30 = 81
|
|
assert_eq!(filter_count, 81);
|
|
}
|
|
|
|
/// Test filter count is 1 when consolidated
|
|
#[test]
|
|
fn test_filter_count_consolidated() {
|
|
let announcement_count = 100; // These would be cleared on consolidation
|
|
let event_count = 100;
|
|
let is_consolidated = true;
|
|
|
|
let filter_count = if is_consolidated {
|
|
1
|
|
} else {
|
|
1 + announcement_count + event_count
|
|
};
|
|
|
|
assert_eq!(filter_count, 1);
|
|
}
|
|
|
|
// ============================================================================
|
|
// Consolidation Threshold Tests
|
|
// ============================================================================
|
|
|
|
/// Test consolidation is not triggered below threshold
|
|
#[test]
|
|
fn test_should_consolidate_below_threshold() {
|
|
let filter_count = 100;
|
|
let is_consolidated = false;
|
|
|
|
let should_consolidate = !is_consolidated && filter_count > CONSOLIDATION_THRESHOLD;
|
|
|
|
assert!(!should_consolidate);
|
|
}
|
|
|
|
/// Test consolidation is triggered at threshold
|
|
#[test]
|
|
fn test_should_consolidate_at_threshold() {
|
|
let filter_count = 151; // > 150
|
|
let is_consolidated = false;
|
|
|
|
let should_consolidate = !is_consolidated && filter_count > CONSOLIDATION_THRESHOLD;
|
|
|
|
assert!(should_consolidate);
|
|
}
|
|
|
|
/// Test consolidation is triggered well above threshold
|
|
#[test]
|
|
fn test_should_consolidate_above_threshold() {
|
|
let filter_count = 200;
|
|
let is_consolidated = false;
|
|
|
|
let should_consolidate = !is_consolidated && filter_count > CONSOLIDATION_THRESHOLD;
|
|
|
|
assert!(should_consolidate);
|
|
}
|
|
|
|
/// Test consolidation is not triggered if already consolidated
|
|
#[test]
|
|
fn test_should_consolidate_already_consolidated() {
|
|
let filter_count = 200; // Would trigger, but already consolidated
|
|
let is_consolidated = true;
|
|
|
|
let should_consolidate = !is_consolidated && filter_count > CONSOLIDATION_THRESHOLD;
|
|
|
|
assert!(!should_consolidate);
|
|
}
|
|
|
|
/// Test exact threshold boundary (150 should NOT trigger, 151 should)
|
|
#[test]
|
|
fn test_consolidation_threshold_boundary() {
|
|
let is_consolidated = false;
|
|
|
|
// 150 should NOT trigger (> 150, not >= 150)
|
|
let should_consolidate_at_150 = !is_consolidated && 150 > CONSOLIDATION_THRESHOLD;
|
|
assert!(!should_consolidate_at_150);
|
|
|
|
// 151 should trigger
|
|
let should_consolidate_at_151 = !is_consolidated && 151 > CONSOLIDATION_THRESHOLD;
|
|
assert!(should_consolidate_at_151);
|
|
}
|
|
|
|
// ============================================================================
|
|
// Duplicate Prevention Tests
|
|
// ============================================================================
|
|
|
|
/// Test duplicate announcement detection
|
|
#[test]
|
|
fn test_duplicate_announcement_prevention() {
|
|
let mut subscribed_announcements: HashSet<String> = HashSet::new();
|
|
|
|
let event_id = "abc123".to_string();
|
|
|
|
// First add should succeed
|
|
let is_new = !subscribed_announcements.contains(&event_id);
|
|
assert!(is_new);
|
|
subscribed_announcements.insert(event_id.clone());
|
|
|
|
// Second add should fail (duplicate)
|
|
let is_new_again = !subscribed_announcements.contains(&event_id);
|
|
assert!(!is_new_again);
|
|
}
|
|
|
|
/// Test duplicate event detection
|
|
#[test]
|
|
fn test_duplicate_event_prevention() {
|
|
let mut subscribed_events: HashSet<String> = HashSet::new();
|
|
|
|
let event_id = "def456".to_string();
|
|
|
|
// First add should succeed
|
|
let is_new = !subscribed_events.contains(&event_id);
|
|
assert!(is_new);
|
|
subscribed_events.insert(event_id.clone());
|
|
|
|
// Second add should fail (duplicate)
|
|
let is_new_again = !subscribed_events.contains(&event_id);
|
|
assert!(!is_new_again);
|
|
}
|
|
|
|
/// Test multiple unique items are tracked correctly
|
|
#[test]
|
|
fn test_multiple_unique_items_tracked() {
|
|
let mut subscribed_announcements: HashSet<String> = HashSet::new();
|
|
|
|
// Add multiple unique announcements
|
|
for i in 0..10 {
|
|
let id = format!("announcement_{}", i);
|
|
assert!(!subscribed_announcements.contains(&id));
|
|
subscribed_announcements.insert(id);
|
|
}
|
|
|
|
assert_eq!(subscribed_announcements.len(), 10);
|
|
}
|
|
|
|
// ============================================================================
|
|
// Event Creation and Validation Tests
|
|
// ============================================================================
|
|
|
|
/// Test announcement event has required d tag
|
|
#[test]
|
|
fn test_announcement_has_d_tag() {
|
|
let keys = Keys::generate();
|
|
let event = create_test_announcement(&keys, "my-repo");
|
|
|
|
let has_d_tag = event.tags.iter().any(|tag| {
|
|
let tag_vec = tag.clone().to_vec();
|
|
tag_vec.len() >= 2 && tag_vec[0] == "d"
|
|
});
|
|
|
|
assert!(has_d_tag);
|
|
}
|
|
|
|
/// Test announcement event has correct kind
|
|
#[test]
|
|
fn test_announcement_correct_kind() {
|
|
let keys = Keys::generate();
|
|
let event = create_test_announcement(&keys, "my-repo");
|
|
|
|
assert_eq!(event.kind.as_u16(), KIND_REPOSITORY_ANNOUNCEMENT);
|
|
}
|
|
|
|
/// Test maintainer list event has correct kind
|
|
#[test]
|
|
fn test_maintainer_list_correct_kind() {
|
|
let keys = Keys::generate();
|
|
let event = create_test_maintainer_list(&keys, "maintainers");
|
|
|
|
assert_eq!(event.kind.as_u16(), KIND_MAINTAINER_LIST);
|
|
}
|
|
|
|
/// Test PR event has a tag
|
|
#[test]
|
|
fn test_pr_event_has_a_tag() {
|
|
let keys = Keys::generate();
|
|
let coord = "30617:pubkey123:my-repo";
|
|
let event = create_test_pr_event(&keys, coord);
|
|
|
|
let has_a_tag = event.tags.iter().any(|tag| {
|
|
let tag_vec = tag.clone().to_vec();
|
|
tag_vec.len() >= 2 && tag_vec[0] == "a"
|
|
});
|
|
|
|
assert!(has_a_tag);
|
|
}
|
|
|
|
/// Test issue event has a tag
|
|
#[test]
|
|
fn test_issue_event_has_a_tag() {
|
|
let keys = Keys::generate();
|
|
let coord = "30617:pubkey123:my-repo";
|
|
let event = create_test_issue_event(&keys, coord);
|
|
|
|
let has_a_tag = event.tags.iter().any(|tag| {
|
|
let tag_vec = tag.clone().to_vec();
|
|
tag_vec.len() >= 2 && tag_vec[0] == "a"
|
|
});
|
|
|
|
assert!(has_a_tag);
|
|
}
|
|
|
|
/// Test reply event has e tag
|
|
#[test]
|
|
fn test_reply_event_has_e_tag() {
|
|
let keys = Keys::generate();
|
|
let event_id = "abc123def456";
|
|
let event = create_test_reply_event(&keys, event_id);
|
|
|
|
let has_e_tag = event.tags.iter().any(|tag| {
|
|
let tag_vec = tag.clone().to_vec();
|
|
tag_vec.len() >= 2 && tag_vec[0] == "e"
|
|
});
|
|
|
|
assert!(has_e_tag);
|
|
}
|
|
|
|
// ============================================================================
|
|
// Subscription Lifecycle Tests
|
|
// ============================================================================
|
|
|
|
/// Test subscription lifecycle: initial -> add announcements -> add events -> consolidate
|
|
#[test]
|
|
fn test_subscription_lifecycle() {
|
|
let mut subscribed_announcements: HashSet<String> = HashSet::new();
|
|
let mut subscribed_events: HashSet<String> = HashSet::new();
|
|
let mut is_consolidated = false;
|
|
|
|
// Initial state
|
|
let initial_count = 1 + subscribed_announcements.len() + subscribed_events.len();
|
|
assert_eq!(initial_count, 1);
|
|
|
|
// Add some announcements
|
|
for i in 0..50 {
|
|
subscribed_announcements.insert(format!("ann_{}", i));
|
|
}
|
|
|
|
let after_announcements = 1 + subscribed_announcements.len() + subscribed_events.len();
|
|
assert_eq!(after_announcements, 51);
|
|
|
|
// Add some events
|
|
for i in 0..50 {
|
|
subscribed_events.insert(format!("evt_{}", i));
|
|
}
|
|
|
|
let after_events = 1 + subscribed_announcements.len() + subscribed_events.len();
|
|
assert_eq!(after_events, 101);
|
|
|
|
// Add more to exceed threshold
|
|
for i in 50..100 {
|
|
subscribed_announcements.insert(format!("ann_{}", i));
|
|
}
|
|
|
|
let before_consolidation = 1 + subscribed_announcements.len() + subscribed_events.len();
|
|
assert_eq!(before_consolidation, 151);
|
|
|
|
// Should trigger consolidation
|
|
let should_consolidate = !is_consolidated && before_consolidation > CONSOLIDATION_THRESHOLD;
|
|
assert!(should_consolidate);
|
|
|
|
// Consolidate
|
|
subscribed_announcements.clear();
|
|
subscribed_events.clear();
|
|
is_consolidated = true;
|
|
|
|
// After consolidation
|
|
let after_consolidation = if is_consolidated { 1 } else { 1 + subscribed_announcements.len() + subscribed_events.len() };
|
|
assert_eq!(after_consolidation, 1);
|
|
|
|
// Should not trigger consolidation again
|
|
let should_consolidate_again = !is_consolidated && after_consolidation > CONSOLIDATION_THRESHOLD;
|
|
assert!(!should_consolidate_again);
|
|
}
|
|
|
|
/// Test that consolidated state blocks new additions
|
|
#[test]
|
|
fn test_consolidated_blocks_additions() {
|
|
let is_consolidated = true;
|
|
|
|
// When consolidated, add_announcement should return None (simulated)
|
|
// The logic is: if is_consolidated, return None
|
|
let should_add = !is_consolidated;
|
|
|
|
assert!(!should_add);
|
|
}
|
|
|
|
/// Test that non-consolidated state allows additions
|
|
#[test]
|
|
fn test_non_consolidated_allows_additions() {
|
|
let is_consolidated = false;
|
|
let mut subscribed_announcements: HashSet<String> = HashSet::new();
|
|
let event_id = "new_announcement";
|
|
|
|
// When not consolidated and event not in set, should add
|
|
let should_add = !is_consolidated && !subscribed_announcements.contains(event_id);
|
|
|
|
assert!(should_add);
|
|
|
|
subscribed_announcements.insert(event_id.to_string());
|
|
assert!(subscribed_announcements.contains(event_id));
|
|
}
|
|
|
|
// ============================================================================
|
|
// Filter Building Tests (coordinate format)
|
|
// ============================================================================
|
|
|
|
/// Test announcement coordinate format
|
|
#[test]
|
|
fn test_announcement_coordinate_format() {
|
|
let keys = Keys::generate();
|
|
let identifier = "my-repo";
|
|
let event = create_test_announcement(&keys, identifier);
|
|
|
|
// Extract d tag
|
|
let d_tag = event.tags.iter().find_map(|tag| {
|
|
let tag_vec = tag.clone().to_vec();
|
|
if tag_vec.len() >= 2 && tag_vec[0] == "d" {
|
|
Some(tag_vec[1].clone())
|
|
} else {
|
|
None
|
|
}
|
|
});
|
|
|
|
assert!(d_tag.is_some());
|
|
assert_eq!(d_tag.unwrap(), identifier);
|
|
|
|
// Build coordinate: kind:pubkey:identifier
|
|
let coord = format!("{}:{}:{}", KIND_REPOSITORY_ANNOUNCEMENT, event.pubkey.to_hex(), identifier);
|
|
|
|
// Verify format
|
|
let parts: Vec<&str> = coord.split(':').collect();
|
|
assert_eq!(parts.len(), 3);
|
|
assert_eq!(parts[0], "30617");
|
|
assert_eq!(parts[2], identifier);
|
|
}
|
|
|
|
/// Test multiple announcement coordinates are unique
|
|
#[test]
|
|
fn test_multiple_announcement_coordinates_unique() {
|
|
let keys = Keys::generate();
|
|
|
|
let identifiers = vec!["repo1", "repo2", "repo3"];
|
|
let mut coords: HashSet<String> = HashSet::new();
|
|
|
|
for id in identifiers {
|
|
let event = create_test_announcement(&keys, id);
|
|
let coord = format!("{}:{}:{}", KIND_REPOSITORY_ANNOUNCEMENT, event.pubkey.to_hex(), id);
|
|
coords.insert(coord);
|
|
}
|
|
|
|
assert_eq!(coords.len(), 3);
|
|
}
|
|
|
|
// ============================================================================
|
|
// Integration-style Tests
|
|
// ============================================================================
|
|
|
|
/// Test simulated workflow: announcement received, then PR received
|
|
#[test]
|
|
fn test_workflow_announcement_then_pr() {
|
|
let keys = Keys::generate();
|
|
let mut subscribed_announcements: HashSet<String> = HashSet::new();
|
|
let mut subscribed_events: HashSet<String> = HashSet::new();
|
|
let is_consolidated = false;
|
|
|
|
// Step 1: Receive announcement
|
|
let announcement = create_test_announcement(&keys, "my-repo");
|
|
let ann_id = announcement.id.to_hex();
|
|
|
|
// Should add to tracking (simulating add_announcement)
|
|
let should_add_ann = !is_consolidated && !subscribed_announcements.contains(&ann_id);
|
|
assert!(should_add_ann);
|
|
subscribed_announcements.insert(ann_id.clone());
|
|
|
|
// Filter count should increase
|
|
let filter_count = 1 + subscribed_announcements.len() + subscribed_events.len();
|
|
assert_eq!(filter_count, 2);
|
|
|
|
// Step 2: Receive PR for that repo
|
|
let coord = format!("{}:{}:my-repo", KIND_REPOSITORY_ANNOUNCEMENT, keys.public_key().to_hex());
|
|
let pr = create_test_pr_event(&keys, &coord);
|
|
let pr_id = pr.id.to_hex();
|
|
|
|
// Should add to tracking (simulating add_event)
|
|
let should_add_pr = !is_consolidated && !subscribed_events.contains(&pr_id);
|
|
assert!(should_add_pr);
|
|
subscribed_events.insert(pr_id.clone());
|
|
|
|
// Filter count should increase again
|
|
let filter_count = 1 + subscribed_announcements.len() + subscribed_events.len();
|
|
assert_eq!(filter_count, 3);
|
|
}
|
|
|
|
/// Test stress: adding many items triggers consolidation
|
|
#[test]
|
|
fn test_stress_many_items_triggers_consolidation() {
|
|
let keys = Keys::generate();
|
|
let mut subscribed_announcements: HashSet<String> = HashSet::new();
|
|
let mut subscribed_events: HashSet<String> = HashSet::new();
|
|
let mut is_consolidated = false;
|
|
let mut consolidation_triggered = false;
|
|
|
|
// Add 100 announcements
|
|
for i in 0..100 {
|
|
let event = create_test_announcement(&keys, &format!("repo-{}", i));
|
|
let event_id = event.id.to_hex();
|
|
|
|
if !is_consolidated && !subscribed_announcements.contains(&event_id) {
|
|
subscribed_announcements.insert(event_id);
|
|
}
|
|
|
|
// Check consolidation after each add
|
|
let filter_count = 1 + subscribed_announcements.len() + subscribed_events.len();
|
|
if !is_consolidated && filter_count > CONSOLIDATION_THRESHOLD {
|
|
consolidation_triggered = true;
|
|
subscribed_announcements.clear();
|
|
subscribed_events.clear();
|
|
is_consolidated = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// If we didn't consolidate yet, add events
|
|
if !consolidation_triggered {
|
|
for i in 0..100 {
|
|
let coord = format!("30617:pubkey:repo-{}", i);
|
|
let event = create_test_pr_event(&keys, &coord);
|
|
let event_id = event.id.to_hex();
|
|
|
|
if !is_consolidated && !subscribed_events.contains(&event_id) {
|
|
subscribed_events.insert(event_id);
|
|
}
|
|
|
|
// Check consolidation after each add
|
|
let filter_count = 1 + subscribed_announcements.len() + subscribed_events.len();
|
|
if !is_consolidated && filter_count > CONSOLIDATION_THRESHOLD {
|
|
consolidation_triggered = true;
|
|
subscribed_announcements.clear();
|
|
subscribed_events.clear();
|
|
is_consolidated = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Consolidation should have been triggered
|
|
assert!(consolidation_triggered);
|
|
assert!(is_consolidated);
|
|
|
|
// After consolidation, counts should be reset
|
|
assert_eq!(subscribed_announcements.len(), 0);
|
|
assert_eq!(subscribed_events.len(), 0);
|
|
}
|
|
|
|
/// Test that all PR/Issue kinds are handled consistently
|
|
#[test]
|
|
fn test_all_pr_issue_kinds_handled() {
|
|
let keys = Keys::generate();
|
|
let coord = "30617:pubkey:repo";
|
|
|
|
// All these kinds should be identified as PR/Issue
|
|
let pr_kinds = vec![1617, 1618, 1619, 1621, 1622];
|
|
|
|
for kind in pr_kinds {
|
|
assert!(
|
|
SubscriptionManager::is_pr_issue_kind(kind),
|
|
"Kind {} should be identified as PR/Issue",
|
|
kind
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Test that announcement and PR/Issue kinds are mutually exclusive
|
|
#[test]
|
|
fn test_kind_categories_mutually_exclusive() {
|
|
let announcement_kinds = vec![30617, 30618];
|
|
let pr_issue_kinds = vec![1617, 1618, 1619, 1621, 1622];
|
|
|
|
// No announcement kind should be a PR/Issue kind
|
|
for kind in &announcement_kinds {
|
|
assert!(
|
|
!SubscriptionManager::is_pr_issue_kind(*kind),
|
|
"Announcement kind {} should not be PR/Issue",
|
|
kind
|
|
);
|
|
}
|
|
|
|
// No PR/Issue kind should be an announcement kind
|
|
for kind in &pr_issue_kinds {
|
|
assert!(
|
|
!SubscriptionManager::is_announcement_kind(*kind),
|
|
"PR/Issue kind {} should not be announcement",
|
|
kind
|
|
);
|
|
}
|
|
} |