From 91dc5e8d718475a73815892452a58e1dbf56c8d9 Mon Sep 17 00:00:00 2001 From: DanConwayDev Date: Mon, 8 Dec 2025 20:39:58 +0000 Subject: delete old bad AI genreated tests --- tests/proactive_sync_catchup.rs | 330 ------------------ tests/proactive_sync_dynamic.rs | 661 ------------------------------------- tests/proactive_sync_multi.rs | 194 ----------- tests/proactive_sync_resilience.rs | 476 -------------------------- 4 files changed, 1661 deletions(-) delete mode 100644 tests/proactive_sync_catchup.rs delete mode 100644 tests/proactive_sync_dynamic.rs delete mode 100644 tests/proactive_sync_multi.rs delete mode 100644 tests/proactive_sync_resilience.rs (limited to 'tests') diff --git a/tests/proactive_sync_catchup.rs b/tests/proactive_sync_catchup.rs deleted file mode 100644 index d8a2ef9..0000000 --- a/tests/proactive_sync_catchup.rs +++ /dev/null @@ -1,330 +0,0 @@ -//! GRASP-02 Phase 5: Negentropy Catchup Integration Tests -//! -//! Tests verify negentropy catchup functionality: -//! - Startup catchup after warm-up delay (30s default) -//! - Reconnect catchup recovers recent gaps (last 3 days) -//! - Daily catchup runs once per 24h with stagger -//! - Catchup uses same filters as live sync -//! - Gap events logged at WARN level -//! -//! # Running Tests -//! -//! ```bash -//! cargo test --test proactive_sync_catchup -//! cargo test --test proactive_sync_catchup -- --nocapture -//! ``` - -use ngit_grasp::sync::SubscriptionManager; - -// ============================================================================ -// Catchup State Machine Tests -// ============================================================================ - -/// Test startup catchup should only run once -#[test] -fn test_startup_catchup_runs_once() { - // After startup catchup completes, should_run_startup_catchup should return false - // This is handled by the startup_catchup_completed flag in NegentropyService - - // Simulating the state machine: - let mut startup_completed = false; - - // Before running, should return true (if delay elapsed) - let should_run_before = !startup_completed; - assert!(should_run_before); - - // After running, mark as completed - startup_completed = true; - - // Now should return false - let should_run_after = !startup_completed; - assert!(!should_run_after); -} - -/// Test daily catchup interval checking -#[test] -fn test_daily_catchup_interval_check() { - use std::time::{Duration, Instant}; - - const DAILY_INTERVAL_SECS: u64 = 86400; - - // Simulate last catchup time - let last_catchup = Instant::now(); - - // Immediately after, should not run - let should_run_immediately = last_catchup.elapsed() >= Duration::from_secs(DAILY_INTERVAL_SECS); - assert!(!should_run_immediately); -} - -/// Test that new relay (no previous catchup) should run daily catchup -#[test] -fn test_new_relay_should_run_daily_catchup() { - use std::collections::HashMap; - use std::time::Instant; - - let last_daily_catchup: HashMap = HashMap::new(); - let relay_url = "wss://test-relay.example.com"; - - // No previous catchup recorded, should return true - let should_run = !last_daily_catchup.contains_key(relay_url); - assert!(should_run); -} - -/// Test reconnect catchup only after successful reconnection -#[test] -fn test_reconnect_catchup_after_reconnection() { - // Reconnect catchup should only trigger when: - // 1. Connection was previously successful (had_previous_connection = true) - // 2. Connection was lost and restored - - let mut had_previous_connection = false; - - // First connection - should NOT trigger reconnect catchup - let is_reconnection_first = had_previous_connection; - assert!(!is_reconnection_first); - had_previous_connection = true; - - // Second connection (after disconnection) - SHOULD trigger - let is_reconnection_second = had_previous_connection; - assert!(is_reconnection_second); -} - -// ============================================================================ -// Gap Event Flow Tests -// ============================================================================ - -/// Test that gap events go through policy validation -#[test] -fn test_gap_events_validated_through_policy() { - // The NegentropyService uses write_policy.admit_event() for validation - // This test verifies the flow exists: - // 1. Fetch events from relay - // 2. Check if event exists locally - // 3. Validate through Nip34WritePolicy - // 4. Store if accepted - - // This is verified by the implementation in negentropy.rs:run_catchup() - // where PolicyResult::Accept leads to storage and PolicyResult::Reject is logged - - assert!(true); // Flow verification - actual validation tested in other tests -} - -/// Test that gap events are distinguished from live events -#[test] -fn test_gap_events_logged_at_warn_level() { - // The spec requires gap events to be logged at WARN level - // to distinguish them from live events (which are logged at INFO) - - // This is implemented in negentropy.rs with: - // tracing::warn!("Gap event filled via {} catchup: {} (kind {})", ...) - - // We verify the logging pattern exists by testing the catchup types - let catchup_types = ["startup", "reconnect", "daily"]; - assert_eq!(catchup_types.len(), 3); - - for catchup_type in catchup_types { - assert!(!catchup_type.is_empty()); - } -} - -// ============================================================================ -// Stagger Logic Tests -// ============================================================================ - -/// Test stagger delay calculation for multiple relays -#[test] -fn test_stagger_delay_for_multiple_relays() { - const STAGGER_SECS: u64 = 300; // 5 minutes - - let _relay_urls = vec![ - "wss://relay1.example.com", - "wss://relay2.example.com", - "wss://relay3.example.com", - ]; - - // First relay (index 0) should have no stagger - let stagger_0 = 0 * STAGGER_SECS; - assert_eq!(stagger_0, 0); - - // Second relay (index 1) should have 5 minute stagger - let stagger_1 = 1 * STAGGER_SECS; - assert_eq!(stagger_1, 300); - - // Third relay (index 2) should have 10 minute stagger - let stagger_2 = 2 * STAGGER_SECS; - assert_eq!(stagger_2, 600); -} - -/// Test that startup catchup waits for warm-up -#[test] -fn test_startup_catchup_waits_for_warmup() { - use std::time::{Duration, Instant}; - - const STARTUP_DELAY_SECS: u64 = 30; - - let startup_time = Instant::now(); - - // Immediately after startup, should not run (delay not elapsed) - let elapsed = startup_time.elapsed(); - let should_run = elapsed >= Duration::from_secs(STARTUP_DELAY_SECS); - - // This should be false since we just created startup_time - assert!(!should_run); -} - -// ============================================================================ -// Lookback Period Tests -// ============================================================================ - -/// Test reconnect lookback calculation -#[test] -fn test_reconnect_lookback_calculation() { - // 3 days = 3 * 24 * 60 * 60 = 259,200 seconds - let lookback_days: u64 = 3; - let lookback_secs = lookback_days * 24 * 60 * 60; - - assert_eq!(lookback_secs, 259200); -} - -/// Test that daily catchup uses no lookback (full reconciliation) -#[test] -fn test_daily_catchup_full_reconciliation() { - // Daily catchup should reconcile all events, not just recent ones - // This is implemented by passing None to the since parameter - let since: Option = None; - assert!(since.is_none()); -} - -// ============================================================================ -// Three Catchup Scenario Tests -// ============================================================================ - -/// Test startup catchup scenario -#[test] -fn test_startup_catchup_scenario() { - // Startup catchup: - // 1. Wait 30s for warm-up - // 2. Run full reconciliation (no time limit) - // 3. Mark as completed (runs only once) - // 4. Stagger between relays (5 minutes) - - const STARTUP_DELAY: u64 = 30; - const STAGGER: u64 = 300; - - assert_eq!(STARTUP_DELAY, 30); - assert_eq!(STAGGER, 300); -} - -/// Test reconnect catchup scenario -#[test] -fn test_reconnect_catchup_scenario() { - // Reconnect catchup: - // 1. Trigger after connection restore (not first connection) - // 2. Wait 10s reconnect delay - // 3. Only fetch last 3 days of events - // 4. Runs in background (doesn't block connection) - - const RECONNECT_DELAY: u64 = 10; - const LOOKBACK_DAYS: u64 = 3; - - assert_eq!(RECONNECT_DELAY, 10); - assert_eq!(LOOKBACK_DAYS, 3); -} - -/// Test daily catchup scenario -#[test] -fn test_daily_catchup_scenario() { - // Daily catchup: - // 1. Check hourly if any relay needs catchup - // 2. Run if 24h elapsed since last catchup for that relay - // 3. Full reconciliation (no time limit) - // 4. Stagger between relays (5 minutes) - - const CHECK_INTERVAL: u64 = 3600; // 1 hour - const DAILY_INTERVAL: u64 = 86400; // 24 hours - const STAGGER: u64 = 300; // 5 minutes - - assert_eq!(CHECK_INTERVAL, 3600); - assert_eq!(DAILY_INTERVAL, 86400); - assert_eq!(STAGGER, 300); -} - -// ============================================================================ -// Event Existence Check Tests -// ============================================================================ - -/// Test that existing events are skipped during catchup -#[test] -fn test_existing_events_skipped() { - // The catchup flow should: - // 1. Fetch events from relay - // 2. For each event, check if it exists locally - // 3. Skip if exists, validate and store if not - - // This is implemented in negentropy.rs:event_exists_locally() - // which queries the database for the event by ID - - const SKIP_EXISTING: bool = true; - assert!(SKIP_EXISTING); -} - -/// Test duplicate prevention during catchup -#[test] -fn test_duplicate_prevention() { - use std::collections::HashSet; - - let mut processed_ids: HashSet = HashSet::new(); - let event_id = "abc123def456".to_string(); - - // First time seeing this event - should process - let is_new = !processed_ids.contains(&event_id); - assert!(is_new); - processed_ids.insert(event_id.clone()); - - // Second time - should skip - let is_duplicate = processed_ids.contains(&event_id); - assert!(is_duplicate); -} - -// ============================================================================ -// Configuration Integration Tests -// ============================================================================ - -/// Test config fields exist for catchup timing -#[test] -fn test_config_fields_for_catchup() { - // The Config struct should have these fields: - // - sync_startup_delay_secs (default: 30) - // - sync_reconnect_delay_secs (default: 10) - // - sync_reconnect_lookback_days (default: 3) - - // Environment variables: - // - NGIT_SYNC_STARTUP_DELAY_SECS - // - NGIT_SYNC_RECONNECT_DELAY_SECS - // - NGIT_SYNC_RECONNECT_LOOKBACK_DAYS - - let expected_defaults = vec![ - ("startup_delay_secs", 30u64), - ("reconnect_delay_secs", 10u64), - ("reconnect_lookback_days", 3u64), - ]; - - assert_eq!(expected_defaults.len(), 3); - assert_eq!(expected_defaults[0].1, 30); - assert_eq!(expected_defaults[1].1, 10); - assert_eq!(expected_defaults[2].1, 3); -} - -/// Test that catchup respects configured delays -#[test] -fn test_catchup_respects_config() { - // Custom delays should be used instead of defaults - let custom_startup_delay: u64 = 60; - let custom_reconnect_delay: u64 = 20; - let custom_lookback_days: u64 = 7; - - // All should be configurable to non-default values - assert_ne!(custom_startup_delay, 30); - assert_ne!(custom_reconnect_delay, 10); - assert_ne!(custom_lookback_days, 3); -} 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 @@ -//! 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") -} - -// ============================================================================ -// 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 = 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 = 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 = 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 = HashSet::new(); - let mut subscribed_events: HashSet = 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 = 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 = 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 = HashSet::new(); - let mut subscribed_events: HashSet = 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 = HashSet::new(); - let mut subscribed_events: HashSet = 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); -} diff --git a/tests/proactive_sync_multi.rs b/tests/proactive_sync_multi.rs deleted file mode 100644 index de4183a..0000000 --- a/tests/proactive_sync_multi.rs +++ /dev/null @@ -1,194 +0,0 @@ -//! GRASP-02 Phase 2: Multi-Relay Proactive Sync Integration Tests -//! -//! Tests the multi-relay proactive sync functionality. -//! -//! Note: Integration tests for sync timing are inherently flaky due to -//! subprocess communication latency. Unit tests for FilterService and -//! SyncManager cover the core logic in src/sync/filter.rs and manager.rs. -//! -//! # Running Tests -//! -//! ```bash -//! cargo test --test proactive_sync_multi -//! ``` - -mod common; - -use std::time::Duration; - -use common::TestRelay; -use nostr_sdk::prelude::*; - -/// Kind 30617 - Repository Announcement (NIP-34) -const KIND_REPOSITORY_ANNOUNCEMENT: u16 = 30617; - -/// Test that sync relay starts successfully when configured with another relay URL -#[tokio::test] -async fn test_sync_relay_starts_with_source_url() { - // Start source relay (relay_a) - let relay_a = TestRelay::start().await; - - // Give relay_a time to start - tokio::time::sleep(Duration::from_millis(200)).await; - - // Start syncing relay (relay_sync) configured to sync from relay_a - let relay_sync = TestRelay::start_with_sync(Some(relay_a.url().into())).await; - - // Give time for connection establishment - tokio::time::sleep(Duration::from_millis(500)).await; - - // If we got here without panic, the relay started successfully with sync config - relay_sync.stop().await; - relay_a.stop().await; -} - -/// Test that relay starts successfully without sync URL (discovery mode) -#[tokio::test] -async fn test_relay_starts_without_sync_url() { - // Start a regular relay (no sync configured) - let relay = TestRelay::start().await; - - // Give relay time to start - tokio::time::sleep(Duration::from_millis(300)).await; - - // Verify we can connect to it - let client = Client::default(); - client - .add_relay(relay.url()) - .await - .expect("Failed to add relay"); - client.connect().await; - - // If we got here, the relay is running - client.disconnect().await; - relay.stop().await; -} - -/// Test that multiple relays can start independently -#[tokio::test] -async fn test_multiple_independent_relays() { - // Start three independent relays - let relay_a = TestRelay::start().await; - let relay_b = TestRelay::start().await; - let relay_c = TestRelay::start().await; - - // Give time for all to start - tokio::time::sleep(Duration::from_millis(300)).await; - - // Verify all have unique URLs - assert_ne!(relay_a.url(), relay_b.url()); - assert_ne!(relay_b.url(), relay_c.url()); - assert_ne!(relay_a.url(), relay_c.url()); - - // Verify all have unique domains - assert_ne!(relay_a.domain(), relay_b.domain()); - assert_ne!(relay_b.domain(), relay_c.domain()); - assert_ne!(relay_a.domain(), relay_c.domain()); - - // Clean up - relay_c.stop().await; - relay_b.stop().await; - relay_a.stop().await; -} - -/// Test that events can be sent to a source relay -#[tokio::test] -async fn test_event_submission_to_relay() { - // Start relay - let relay = TestRelay::start().await; - tokio::time::sleep(Duration::from_millis(200)).await; - - // Create test keys - let keys = Keys::generate(); - - // Create a simple announcement-like event (kind 30617) - // Note: This tests event submission, not full announcement validation - let tags = vec![ - Tag::identifier("test-repo"), - Tag::custom( - TagKind::custom("clone"), - vec![format!("http://{}/test-repo", relay.domain())], - ), - Tag::custom( - TagKind::custom("relays"), - vec![format!("ws://{}", relay.domain())], - ), - ]; - - let event = EventBuilder::new( - Kind::Custom(KIND_REPOSITORY_ANNOUNCEMENT), - "Test repository", - ) - .tags(tags) - .sign_with_keys(&keys) - .expect("Failed to sign event"); - - // Try to send event to relay - let client = Client::default(); - client - .add_relay(relay.url()) - .await - .expect("Failed to add relay"); - client.connect().await; - - // Send event - it may or may not be accepted depending on validation - // The point is the connection and submission work - let result = client.send_event(&event).await; - - // Clean up - client.disconnect().await; - relay.stop().await; - - // Verify send completed (success or rejection is fine, no transport error) - assert!(result.is_ok() || result.is_err()); -} - -/// Test domain extraction from relay URL (unit test style) -#[test] -fn test_domain_extraction() { - // This tests the domain() method of TestRelay indirectly - // by verifying the format matches expectations - - // Domain should be in format "127.0.0.1:PORT" - let example_domain = "127.0.0.1:8080"; - assert!(example_domain.starts_with("127.0.0.1:")); - - // URL should be in format "ws://127.0.0.1:PORT" - let example_url = "ws://127.0.0.1:8080"; - assert!(example_url.starts_with("ws://127.0.0.1:")); -} - -/// Test that sync configuration is properly passed to relay process -#[tokio::test] -async fn test_sync_configuration_applied() { - // Start source relay - let relay_source = TestRelay::start().await; - tokio::time::sleep(Duration::from_millis(200)).await; - - // Start syncing relay with explicit sync URL - let relay_sync = TestRelay::start_with_sync(Some(relay_source.url().into())).await; - tokio::time::sleep(Duration::from_millis(300)).await; - - // Both relays should be running - // The sync relay has NGIT_SYNC_BOOTSTRAP_RELAY_URL set (verified by relay starting) - - let client_source = Client::default(); - client_source - .add_relay(relay_source.url()) - .await - .expect("Failed to add source relay"); - client_source.connect().await; - - let client_sync = Client::default(); - client_sync - .add_relay(relay_sync.url()) - .await - .expect("Failed to add sync relay"); - client_sync.connect().await; - - // Both should be accessible - client_sync.disconnect().await; - client_source.disconnect().await; - relay_sync.stop().await; - relay_source.stop().await; -} diff --git a/tests/proactive_sync_resilience.rs b/tests/proactive_sync_resilience.rs deleted file mode 100644 index 60b18dd..0000000 --- a/tests/proactive_sync_resilience.rs +++ /dev/null @@ -1,476 +0,0 @@ -//! Integration tests for GRASP-02 Phase 3: Resilience & Health Tracking -//! -//! Tests verify: -//! - Exponential backoff on connection failures (5s → 1h max) -//! - Dead relay detection after 24h of failures -//! - Successful connection resets to Healthy -//! - Dead relays retry minimally (once per day) -//! - Health state tracking is thread-safe - -use std::time::{Duration, Instant}; - -use ngit_grasp::sync::health::{HealthState, RelayHealthTracker}; - -/// Test that a single failure transitions relay to Degraded state -#[test] -fn test_single_failure_causes_degraded_state() { - let tracker = RelayHealthTracker::with_defaults(); - let url = "wss://test-relay.example.com"; - - // Initial state should allow connection - assert!(tracker.should_attempt_connection(url)); - - // Record a failure - tracker.record_failure(url); - - // Should be in degraded state - assert_eq!(tracker.get_state(url), HealthState::Degraded); - assert_eq!(tracker.get_failure_count(url), 1); -} - -/// Test that successful connection resets to Healthy state -#[test] -fn test_success_resets_to_healthy() { - let tracker = RelayHealthTracker::with_defaults(); - let url = "wss://test-relay.example.com"; - - // Simulate multiple failures - tracker.record_failure(url); - tracker.record_failure(url); - tracker.record_failure(url); - - assert_eq!(tracker.get_state(url), HealthState::Degraded); - assert_eq!(tracker.get_failure_count(url), 3); - - // Success should reset everything - tracker.record_success(url); - - assert_eq!(tracker.get_state(url), HealthState::Healthy); - assert_eq!(tracker.get_failure_count(url), 0); - assert!(tracker.should_attempt_connection(url)); -} - -/// Test that backoff increases exponentially -#[test] -fn test_exponential_backoff_calculation() { - let max_backoff = 3600u64; // 1 hour - - // failure 1: 5s (5 * 2^0) - assert_eq!( - RelayHealthTracker::get_backoff_duration(1, max_backoff), - Duration::from_secs(5) - ); - - // failure 2: 10s (5 * 2^1) - assert_eq!( - RelayHealthTracker::get_backoff_duration(2, max_backoff), - Duration::from_secs(10) - ); - - // failure 3: 20s (5 * 2^2) - assert_eq!( - RelayHealthTracker::get_backoff_duration(3, max_backoff), - Duration::from_secs(20) - ); - - // failure 4: 40s (5 * 2^3) - assert_eq!( - RelayHealthTracker::get_backoff_duration(4, max_backoff), - Duration::from_secs(40) - ); - - // failure 5: 80s (5 * 2^4) - assert_eq!( - RelayHealthTracker::get_backoff_duration(5, max_backoff), - Duration::from_secs(80) - ); - - // failure 6: 160s (5 * 2^5) - assert_eq!( - RelayHealthTracker::get_backoff_duration(6, max_backoff), - Duration::from_secs(160) - ); - - // failure 7: 320s (5 * 2^6) - assert_eq!( - RelayHealthTracker::get_backoff_duration(7, max_backoff), - Duration::from_secs(320) - ); - - // failure 8: 640s (5 * 2^7) - assert_eq!( - RelayHealthTracker::get_backoff_duration(8, max_backoff), - Duration::from_secs(640) - ); - - // failure 9: 1280s (5 * 2^8) - assert_eq!( - RelayHealthTracker::get_backoff_duration(9, max_backoff), - Duration::from_secs(1280) - ); - - // failure 10: 2560s (5 * 2^9) - assert_eq!( - RelayHealthTracker::get_backoff_duration(10, max_backoff), - Duration::from_secs(2560) - ); -} - -/// Test that backoff is capped at max_backoff -#[test] -fn test_backoff_capped_at_maximum() { - let max_backoff = 3600u64; // 1 hour - - // After many failures, should cap at max_backoff - assert_eq!( - RelayHealthTracker::get_backoff_duration(15, max_backoff), - Duration::from_secs(max_backoff) - ); - - assert_eq!( - RelayHealthTracker::get_backoff_duration(20, max_backoff), - Duration::from_secs(max_backoff) - ); - - assert_eq!( - RelayHealthTracker::get_backoff_duration(100, max_backoff), - Duration::from_secs(max_backoff) - ); -} - -/// Test that custom max_backoff is respected -#[test] -fn test_custom_max_backoff() { - let custom_max = 60u64; // 1 minute max - - // After several failures, should cap at custom max - assert_eq!( - RelayHealthTracker::get_backoff_duration(10, custom_max), - Duration::from_secs(custom_max) - ); - - // Tracker with custom max should use it - let tracker = RelayHealthTracker::with_max_backoff(custom_max); - let url = "wss://test-relay.example.com"; - - // Simulate many failures - for _ in 0..20 { - tracker.record_failure(url); - } - - // Should still be degraded (not dead without 24h) - assert_eq!(tracker.get_state(url), HealthState::Degraded); -} - -/// Test that backoff blocks immediate reconnection -#[test] -fn test_backoff_blocks_immediate_reconnection() { - let tracker = RelayHealthTracker::with_defaults(); - let url = "wss://test-relay.example.com"; - - // First connection attempt should be allowed - assert!(tracker.should_attempt_connection(url)); - - // Record a failure - tracker.record_failure(url); - - // Immediately after failure, connection should be blocked (backoff active) - assert!(!tracker.should_attempt_connection(url)); - - // Should have remaining backoff - let remaining = tracker.get_remaining_backoff(url); - assert!(remaining.is_some()); - assert!(remaining.unwrap() > Duration::ZERO); -} - -/// Test that multiple relays are tracked independently -#[test] -fn test_multiple_relays_independent() { - let tracker = RelayHealthTracker::with_defaults(); - let url1 = "wss://relay1.example.com"; - let url2 = "wss://relay2.example.com"; - let url3 = "wss://relay3.example.com"; - - // Fail relay1 multiple times - tracker.record_failure(url1); - tracker.record_failure(url1); - tracker.record_failure(url1); - - // Succeed on relay2 - tracker.record_success(url2); - - // Fail relay3 once - tracker.record_failure(url3); - - // Verify independent states - assert_eq!(tracker.get_state(url1), HealthState::Degraded); - assert_eq!(tracker.get_failure_count(url1), 3); - - assert_eq!(tracker.get_state(url2), HealthState::Healthy); - assert_eq!(tracker.get_failure_count(url2), 0); - - assert_eq!(tracker.get_state(url3), HealthState::Degraded); - assert_eq!(tracker.get_failure_count(url3), 1); -} - -/// Test is_dead returns false for degraded relays -#[test] -fn test_is_dead_false_for_degraded() { - let tracker = RelayHealthTracker::with_defaults(); - let url = "wss://test-relay.example.com"; - - // Simulate failures - for _ in 0..10 { - tracker.record_failure(url); - } - - // Should be degraded but not dead (24h hasn't passed) - assert_eq!(tracker.get_state(url), HealthState::Degraded); - assert!(!tracker.is_dead(url)); -} - -/// Test get_tracked_relays returns all tracked URLs -#[test] -fn test_get_tracked_relays() { - let tracker = RelayHealthTracker::with_defaults(); - - // Track multiple relays - tracker.record_success("wss://relay1.example.com"); - tracker.record_failure("wss://relay2.example.com"); - tracker.record_success("wss://relay3.example.com"); - - let tracked = tracker.get_tracked_relays(); - assert_eq!(tracked.len(), 3); - assert!(tracked.contains(&"wss://relay1.example.com".to_string())); - assert!(tracked.contains(&"wss://relay2.example.com".to_string())); - assert!(tracked.contains(&"wss://relay3.example.com".to_string())); -} - -/// Test get_health returns cloned health info -#[test] -fn test_get_health_returns_clone() { - let tracker = RelayHealthTracker::with_defaults(); - let url = "wss://test-relay.example.com"; - - // Record success - tracker.record_success(url); - - // Get health info - let health = tracker.get_health(url); - assert!(health.is_some()); - - let health = health.unwrap(); - assert_eq!(health.state, HealthState::Healthy); - assert!(health.last_success_time.is_some()); - assert_eq!(health.consecutive_failures, 0); -} - -/// Test get_health returns None for non-existent relay -#[test] -fn test_get_health_nonexistent() { - let tracker = RelayHealthTracker::with_defaults(); - - let health = tracker.get_health("wss://nonexistent.example.com"); - assert!(health.is_none()); -} - -/// Test that new relays default to allowing connection -#[test] -fn test_new_relay_allows_connection() { - let tracker = RelayHealthTracker::with_defaults(); - - // A never-seen relay should allow connection - assert!(tracker.should_attempt_connection("wss://brand-new-relay.example.com")); -} - -/// Test health state display -#[test] -fn test_health_state_display() { - assert_eq!(HealthState::Healthy.to_string(), "healthy"); - assert_eq!(HealthState::Degraded.to_string(), "degraded"); - assert_eq!(HealthState::Dead.to_string(), "dead"); -} - -/// Test thread safety with concurrent access -#[tokio::test] -async fn test_concurrent_health_tracking() { - use std::sync::Arc; - - let tracker = Arc::new(RelayHealthTracker::with_defaults()); - let url = "wss://concurrent-test-relay.example.com"; - - // Spawn multiple tasks that access the tracker concurrently - let mut handles = vec![]; - - for i in 0..10 { - let tracker_clone = tracker.clone(); - let url_owned = url.to_string(); - let handle = tokio::spawn(async move { - if i % 2 == 0 { - tracker_clone.record_failure(&url_owned); - } else { - tracker_clone.record_success(&url_owned); - } - tracker_clone.get_state(&url_owned); - tracker_clone.should_attempt_connection(&url_owned); - }); - handles.push(handle); - } - - // Wait for all tasks - for handle in handles { - handle.await.unwrap(); - } - - // Tracker should still be usable - let health = tracker.get_health(url); - assert!(health.is_some()); -} - -/// Test that failure streak tracking works correctly -#[test] -fn test_failure_streak_tracking() { - let tracker = RelayHealthTracker::with_defaults(); - let url = "wss://test-relay.example.com"; - - // Build up a failure streak - for i in 1..=5 { - tracker.record_failure(url); - assert_eq!(tracker.get_failure_count(url), i); - } - - // Success should reset the streak - tracker.record_success(url); - assert_eq!(tracker.get_failure_count(url), 0); - - // Start a new streak - tracker.record_failure(url); - assert_eq!(tracker.get_failure_count(url), 1); -} - -/// Test recovery from degraded state -#[test] -fn test_recovery_from_degraded() { - let tracker = RelayHealthTracker::with_defaults(); - let url = "wss://test-relay.example.com"; - - // Enter degraded state - tracker.record_failure(url); - assert_eq!(tracker.get_state(url), HealthState::Degraded); - - // Recover - tracker.record_success(url); - assert_eq!(tracker.get_state(url), HealthState::Healthy); - assert!(tracker.should_attempt_connection(url)); - assert!(tracker.get_remaining_backoff(url).is_none()); -} - -/// Test that remaining backoff is None after success -#[test] -fn test_no_remaining_backoff_after_success() { - let tracker = RelayHealthTracker::with_defaults(); - let url = "wss://test-relay.example.com"; - - // Fail to set backoff - tracker.record_failure(url); - assert!(tracker.get_remaining_backoff(url).is_some()); - - // Succeed to clear backoff - tracker.record_success(url); - assert!(tracker.get_remaining_backoff(url).is_none()); -} - -/// Integration test: simulate a realistic connection lifecycle -#[test] -fn test_realistic_connection_lifecycle() { - let tracker = RelayHealthTracker::with_max_backoff(60); // 1 minute max for test - let url = "wss://production-relay.example.com"; - - // Initial connection succeeds - tracker.record_success(url); - assert_eq!(tracker.get_state(url), HealthState::Healthy); - - // Connection drops - first failure - tracker.record_failure(url); - assert_eq!(tracker.get_state(url), HealthState::Degraded); - assert_eq!(tracker.get_failure_count(url), 1); - - // Second failure (retry failed) - tracker.record_failure(url); - assert_eq!(tracker.get_failure_count(url), 2); - - // Third failure - tracker.record_failure(url); - assert_eq!(tracker.get_failure_count(url), 3); - - // Connection finally succeeds - tracker.record_success(url); - assert_eq!(tracker.get_state(url), HealthState::Healthy); - assert_eq!(tracker.get_failure_count(url), 0); - assert!(tracker.should_attempt_connection(url)); -} - -/// Test backoff timing sequence -#[test] -fn test_backoff_timing_sequence() { - // With default max of 3600s (1 hour), verify the progression - let max = 3600u64; - - let expected = vec![ - (1, 5), // 5s - (2, 10), // 10s - (3, 20), // 20s - (4, 40), // 40s - (5, 80), // 80s - (6, 160), // 160s (~2.7 min) - (7, 320), // 320s (~5.3 min) - (8, 640), // 640s (~10.7 min) - (9, 1280), // 1280s (~21.3 min) - (10, 2560), // 2560s (~42.7 min) - (11, 3600), // capped at 3600s (1 hour) - (12, 3600), // still capped - ]; - - for (failures, expected_secs) in expected { - assert_eq!( - RelayHealthTracker::get_backoff_duration(failures, max), - Duration::from_secs(expected_secs), - "Failed for {} failures", - failures - ); - } -} - -/// Test that health info timestamp tracking works -#[test] -fn test_timestamp_tracking() { - let tracker = RelayHealthTracker::with_defaults(); - let url = "wss://test-relay.example.com"; - - // Record initial success - let before = Instant::now(); - tracker.record_success(url); - let after = Instant::now(); - - let health = tracker.get_health(url).unwrap(); - let success_time = health.last_success_time.unwrap(); - - // Success time should be between before and after - assert!(success_time >= before); - assert!(success_time <= after); - - // Record failure - let before_fail = Instant::now(); - tracker.record_failure(url); - let after_fail = Instant::now(); - - let health = tracker.get_health(url).unwrap(); - let failure_time = health.last_failure_time.unwrap(); - let first_failure = health.first_failure_time.unwrap(); - - // Failure times should be between before and after - assert!(failure_time >= before_fail); - assert!(failure_time <= after_fail); - assert!(first_failure >= before_fail); - assert!(first_failure <= after_fail); -} \ No newline at end of file -- cgit v1.2.3