#![ cfg( feature = "failover" ) ]
use api_xai::{ FailoverManager, FailoverConfig, EndpointHealth };
use core::time::Duration;
#[ test ]
fn failover_manager_starts_with_first_endpoint()
{
let manager = FailoverManager::new(
vec![
"https://api1.x.ai/v1/".to_string(),
"https://api2.x.ai/v1/".to_string(),
],
FailoverConfig::default()
);
assert_eq!( manager.current_endpoint(), "https://api1.x.ai/v1/" );
assert_eq!( manager.current_index(), 0 );
assert_eq!( manager.endpoint_count(), 2 );
}
#[ test ]
fn failover_tracks_endpoint_health()
{
let config = FailoverConfig::default()
.with_max_failures( 3 )
.with_auto_rotate( false );
let manager = FailoverManager::new(
vec![
"https://api1.x.ai/v1/".to_string(),
"https://api2.x.ai/v1/".to_string(),
],
config
);
let health = manager.endpoint_health();
assert_eq!( health.len(), 2 );
assert_eq!( health[ 0 ].1, EndpointHealth::Healthy );
manager.record_failure();
let health = manager.endpoint_health();
assert_eq!( health[ 0 ].1, EndpointHealth::Degraded );
manager.record_failure();
let health = manager.endpoint_health();
assert_eq!( health[ 0 ].1, EndpointHealth::Degraded );
manager.record_failure();
let health = manager.endpoint_health();
assert_eq!( health[ 0 ].1, EndpointHealth::Unhealthy );
}
#[ test ]
fn failover_rotates_automatically_on_unhealthy()
{
let config = FailoverConfig::default()
.with_max_failures( 2 )
.with_auto_rotate( true );
let manager = FailoverManager::new(
vec![
"https://api1.x.ai/v1/".to_string(),
"https://api2.x.ai/v1/".to_string(),
],
config
);
assert_eq!( manager.current_endpoint(), "https://api1.x.ai/v1/" );
let rotated = manager.record_failure();
assert!( !rotated, "Should not rotate on first failure" );
let rotated = manager.record_failure();
assert!( rotated, "Should rotate after reaching threshold" );
assert_eq!( manager.current_endpoint(), "https://api2.x.ai/v1/" );
assert_eq!( manager.current_index(), 1 );
}
#[ test ]
fn failover_success_resets_failure_count()
{
let config = FailoverConfig::default()
.with_max_failures( 3 )
.with_auto_rotate( false );
let manager = FailoverManager::new(
vec![ "https://api1.x.ai/v1/".to_string() ],
config
);
manager.record_failure();
manager.record_failure();
let health = manager.endpoint_health();
assert_eq!( health[ 0 ].1, EndpointHealth::Degraded );
manager.record_success();
let health = manager.endpoint_health();
assert_eq!( health[ 0 ].1, EndpointHealth::Healthy );
manager.record_failure();
manager.record_failure();
let health = manager.endpoint_health();
assert_eq!( health[ 0 ].1, EndpointHealth::Degraded );
manager.record_failure();
let health = manager.endpoint_health();
assert_eq!( health[ 0 ].1, EndpointHealth::Unhealthy );
}
#[ test ]
fn failover_manual_rotation_works()
{
let manager = FailoverManager::new(
vec![
"https://api1.x.ai/v1/".to_string(),
"https://api2.x.ai/v1/".to_string(),
"https://api3.x.ai/v1/".to_string(),
],
FailoverConfig::default()
);
assert_eq!( manager.current_index(), 0 );
manager.rotate();
assert_eq!( manager.current_index(), 1 );
manager.rotate();
assert_eq!( manager.current_index(), 2 );
manager.rotate();
assert_eq!( manager.current_index(), 0 ); }
#[ test ]
fn failover_skips_unhealthy_endpoints_on_rotation()
{
let config = FailoverConfig::default()
.with_max_failures( 1 );
let manager = FailoverManager::new(
vec![
"https://api1.x.ai/v1/".to_string(),
"https://api2.x.ai/v1/".to_string(),
"https://api3.x.ai/v1/".to_string(),
],
config
);
manager.record_failure();
manager.rotate();
assert_eq!( manager.current_index(), 2, "Should skip index 1 (unhealthy)" );
}
#[ test ]
fn failover_respects_retry_cooldown()
{
let config = FailoverConfig::default()
.with_max_failures( 1 )
.with_retry_after( Duration::from_millis( 100 ) );
let manager = FailoverManager::new(
vec![
"https://api1.x.ai/v1/".to_string(),
"https://api2.x.ai/v1/".to_string(),
],
config
);
manager.record_failure();
let health = manager.endpoint_health();
assert_eq!( health[ 0 ].1, EndpointHealth::Unhealthy );
manager.rotate();
assert_eq!( manager.current_index(), 1 );
std::thread::sleep( Duration::from_millis( 150 ) );
manager.rotate();
assert_eq!( manager.current_index(), 0 );
let health = manager.endpoint_health();
assert_eq!( health[ 0 ].1, EndpointHealth::Degraded );
}
#[ test ]
fn failover_reset_restores_all_endpoints()
{
let config = FailoverConfig::default()
.with_max_failures( 1 );
let manager = FailoverManager::new(
vec![
"https://api1.x.ai/v1/".to_string(),
"https://api2.x.ai/v1/".to_string(),
],
config
);
manager.record_failure();
manager.rotate();
manager.record_failure();
let health = manager.endpoint_health();
assert_eq!( health[ 0 ].1, EndpointHealth::Unhealthy );
assert_eq!( health[ 1 ].1, EndpointHealth::Unhealthy );
manager.reset();
let health = manager.endpoint_health();
assert_eq!( health[ 0 ].1, EndpointHealth::Healthy );
assert_eq!( health[ 1 ].1, EndpointHealth::Healthy );
assert_eq!( manager.current_index(), 0 );
}
#[ test ]
fn failover_config_builder_works()
{
let config = FailoverConfig::default()
.with_max_failures( 10 )
.with_retry_after( Duration::from_secs( 120 ) )
.with_auto_rotate( false );
assert_eq!( config.max_failures, 10 );
assert_eq!( config.retry_after, Duration::from_secs( 120 ) );
assert!( !config.auto_rotate );
}
#[ test ]
#[ should_panic( expected = "Must provide at least one endpoint" ) ]
fn failover_panics_on_empty_endpoints()
{
FailoverManager::new( vec![], FailoverConfig::default() );
}
#[ test ]
fn failover_handles_single_endpoint()
{
let manager = FailoverManager::new(
vec![ "https://api1.x.ai/v1/".to_string() ],
FailoverConfig::default()
);
manager.record_failure();
manager.record_failure();
manager.record_failure();
manager.rotate();
assert_eq!( manager.current_index(), 0 );
}
#[ test ]
fn failover_all_unhealthy_uses_next_anyway()
{
let config = FailoverConfig::default()
.with_max_failures( 1 )
.with_auto_rotate( false );
let manager = FailoverManager::new(
vec![
"https://api1.x.ai/v1/".to_string(),
"https://api2.x.ai/v1/".to_string(),
],
config
);
manager.record_failure();
manager.rotate();
manager.record_failure();
manager.rotate();
assert_eq!( manager.current_index(), 0 );
}