#![cfg(feature = "std")]
use oxirouter::prelude::*;
use oxirouter::{CircuitBreakerConfig, OxiRouterError, QueryLog, RouterConfig, RouterState};
const V1: u32 = 1;
const V2: u32 = 2;
#[test]
fn test_router_config_roundtrip_json() {
let cfg = RouterConfig {
max_sources: 12,
min_confidence: 0.42,
use_ml: false,
use_context: false,
timeout_us: 99_999,
history_weight: 0.11,
vocab_weight: 0.22,
geo_weight: 0.33,
circuit_breaker: CircuitBreakerConfig {
failure_threshold: 3,
cooldown_ms: 15_000,
now_ms: None,
},
max_response_bytes: 64 * 1024 * 1024,
#[cfg(feature = "cache")]
cache_enabled: false,
#[cfg(feature = "cache")]
cache_ttl_ms: 60_000,
#[cfg(feature = "cache")]
cache_max_entries: 500,
};
let json = serde_json::to_string(&cfg).expect("serialize failed");
let restored: RouterConfig = serde_json::from_str(&json).expect("deserialize failed");
assert_eq!(restored.max_sources, 12);
assert!((restored.min_confidence - 0.42).abs() < 1e-5);
assert!(!restored.use_ml);
assert!(!restored.use_context);
assert_eq!(restored.timeout_us, 99_999);
assert!((restored.history_weight - 0.11).abs() < 1e-5);
assert!((restored.vocab_weight - 0.22).abs() < 1e-5);
assert!((restored.geo_weight - 0.33).abs() < 1e-5);
assert_eq!(restored.circuit_breaker.failure_threshold, 3);
assert_eq!(restored.circuit_breaker.cooldown_ms, 15_000);
let _ = restored.circuit_breaker.now_ms;
}
#[test]
fn test_router_with_config_file() {
let cfg = RouterConfig {
max_sources: 7,
min_confidence: 0.25,
use_ml: true,
use_context: false,
timeout_us: 50_000,
history_weight: 0.5,
vocab_weight: 0.3,
geo_weight: 0.2,
circuit_breaker: CircuitBreakerConfig {
failure_threshold: 2,
cooldown_ms: 5_000,
now_ms: None,
},
max_response_bytes: 64 * 1024 * 1024,
#[cfg(feature = "cache")]
cache_enabled: true,
#[cfg(feature = "cache")]
cache_ttl_ms: 120_000,
#[cfg(feature = "cache")]
cache_max_entries: 200,
};
let json = serde_json::to_string(&cfg).expect("serialize failed");
let path = std::env::temp_dir().join("oxi_test_config_file.json");
std::fs::write(&path, &json).expect("write temp file failed");
let loaded = RouterConfig::from_config_file(&path).expect("from_config_file failed");
assert_eq!(loaded.max_sources, 7);
assert!((loaded.min_confidence - 0.25).abs() < 1e-5);
assert!(!loaded.use_context);
assert_eq!(loaded.circuit_breaker.failure_threshold, 2);
let router = Router::with_config_file(&path).expect("Router::with_config_file failed");
assert_eq!(router.source_count(), 0);
let _ = std::fs::remove_file(&path);
}
#[test]
fn test_timeout_error_carries_context() {
let err = OxiRouterError::Timeout {
source_id: "test-src".into(),
operation: "execute".into(),
elapsed_ms: 500,
deadline_ms: 100,
};
match &err {
OxiRouterError::Timeout {
source_id,
operation,
elapsed_ms,
deadline_ms,
} => {
assert_eq!(source_id, "test-src");
assert_eq!(operation, "execute");
assert_eq!(*elapsed_ms, 500);
assert_eq!(*deadline_ms, 100);
}
_ => panic!("expected Timeout variant"),
}
}
#[test]
fn test_timeout_display_message() {
let err = OxiRouterError::Timeout {
source_id: "test-src".into(),
operation: "execute".into(),
elapsed_ms: 500,
deadline_ms: 100,
};
let msg = err.to_string();
assert!(msg.contains("test-src"), "expected source_id in: {msg}");
assert!(msg.contains("execute"), "expected operation in: {msg}");
assert!(msg.contains("500"), "expected elapsed_ms in: {msg}");
assert!(msg.contains("100"), "expected deadline_ms in: {msg}");
}
#[test]
fn test_state_v1_loads_with_default_config() {
#[derive(serde::Serialize)]
struct V1Body<'a> {
version: u32,
sources: Vec<oxirouter::DataSource>,
model_bytes: Option<Vec<u8>>,
rl_bytes: Option<Vec<u8>>,
query_log: &'a QueryLog,
}
let ql = QueryLog::new();
let body = V1Body {
version: V1,
sources: Vec::new(),
model_bytes: None,
rl_bytes: None,
query_log: &ql,
};
let body_json = serde_json::to_vec(&body).expect("serialize v1 body failed");
let mut blob = Vec::with_capacity(8 + body_json.len());
blob.extend_from_slice(b"OXIR");
blob.extend_from_slice(&V1.to_le_bytes());
blob.extend_from_slice(&body_json);
let state = RouterState::from_bytes(&blob).expect("v1 blob should load cleanly");
assert!(
state.config().is_none(),
"v1 blob should produce config == None"
);
let custom_cfg = RouterConfig {
max_sources: 13,
min_confidence: 0.77,
use_ml: false,
use_context: false,
timeout_us: 42_000,
history_weight: 0.1,
vocab_weight: 0.5,
geo_weight: 0.4,
circuit_breaker: CircuitBreakerConfig {
failure_threshold: 7,
cooldown_ms: 12_345,
now_ms: None,
},
max_response_bytes: 64 * 1024 * 1024,
#[cfg(feature = "cache")]
cache_enabled: false,
#[cfg(feature = "cache")]
cache_ttl_ms: 10_000,
#[cfg(feature = "cache")]
cache_max_entries: 50,
};
let mut router = Router::with_config(custom_cfg, oxirouter::DefaultContextProvider);
router
.load_state(&blob)
.expect("load_state from v1 blob failed");
assert_eq!(
router.config().max_sources,
13,
"v1 load must not clobber router config (max_sources)"
);
assert!(
(router.config().min_confidence - 0.77).abs() < 1e-5,
"v1 load must not clobber router config (min_confidence)"
);
assert_eq!(
router.source_count(),
0,
"no sources in v1 blob, so count should be 0"
);
}
#[test]
fn test_state_v2_roundtrip_with_config() {
let custom_cfg = RouterConfig {
max_sources: 9,
min_confidence: 0.33,
use_ml: false,
use_context: true,
timeout_us: 200_000,
history_weight: 0.6,
vocab_weight: 0.2,
geo_weight: 0.2,
circuit_breaker: CircuitBreakerConfig {
failure_threshold: 10,
cooldown_ms: 60_000,
now_ms: None,
},
max_response_bytes: 64 * 1024 * 1024,
#[cfg(feature = "cache")]
cache_enabled: false,
#[cfg(feature = "cache")]
cache_ttl_ms: 30_000,
#[cfg(feature = "cache")]
cache_max_entries: 100,
};
let mut original_router = Router::with_config(custom_cfg, oxirouter::DefaultContextProvider);
original_router.add_source(DataSource::new("src-x", "https://x.example.org/sparql"));
let bytes = original_router.save_state().expect("save_state failed");
let version_in_blob = u32::from_le_bytes([bytes[4], bytes[5], bytes[6], bytes[7]]);
assert_eq!(
version_in_blob, V2,
"saved blob must carry version 2, got {version_in_blob}"
);
let mut restored_router = Router::new();
restored_router
.load_state(&bytes)
.expect("load_state v2 failed");
let state = RouterState::from_bytes(&bytes).expect("from_bytes failed");
let restored_cfg = state.config().expect("v2 state must carry a config");
assert_eq!(restored_cfg.max_sources, 9);
assert!((restored_cfg.min_confidence - 0.33).abs() < 1e-5);
assert!(!restored_cfg.use_ml);
assert_eq!(restored_cfg.circuit_breaker.failure_threshold, 10);
assert_eq!(restored_cfg.circuit_breaker.cooldown_ms, 60_000);
assert_eq!(
restored_router.config().max_sources,
9,
"Router::load_state must apply the config from the v2 blob"
);
assert!(
(restored_router.config().min_confidence - 0.33).abs() < 1e-5,
"Router::load_state must restore min_confidence"
);
assert!(
!restored_router.config().use_ml,
"Router::load_state must restore use_ml"
);
assert_eq!(restored_router.source_count(), 1);
assert!(restored_router.get_source("src-x").is_some());
}