#[cfg(feature = "aur")]
use std::sync::{LazyLock, Mutex};
#[cfg(feature = "aur")]
use std::time::{Duration, Instant};
#[cfg(feature = "aur")]
use rand::Rng;
#[cfg(feature = "aur")]
use tracing::{debug, warn};
#[cfg(feature = "aur")]
use crate::aur::validation::ValidationConfig;
#[cfg(feature = "aur")]
use crate::cache::{CacheConfig, CacheWrapper};
#[cfg(feature = "aur")]
use crate::env;
#[cfg(feature = "aur")]
use crate::error::{ArchToolkitError, Result};
#[cfg(feature = "aur")]
use reqwest::Client as ReqwestClient;
#[cfg(feature = "aur")]
struct ArchLinuxRateLimiter {
last_request: Instant,
current_backoff_ms: u64,
consecutive_failures: u32,
}
#[cfg(feature = "aur")]
static ARCHLINUX_RATE_LIMITER: LazyLock<Mutex<ArchLinuxRateLimiter>> = LazyLock::new(|| {
Mutex::new(ArchLinuxRateLimiter {
last_request: Instant::now(),
current_backoff_ms: 500, consecutive_failures: 0,
})
});
#[cfg(feature = "aur")]
static ARCHLINUX_REQUEST_SEMAPHORE: LazyLock<std::sync::Arc<tokio::sync::Semaphore>> =
LazyLock::new(|| std::sync::Arc::new(tokio::sync::Semaphore::new(1)));
#[cfg(feature = "aur")]
const ARCHLINUX_BASE_DELAY_MS: u64 = 500;
#[cfg(feature = "aur")]
const ARCHLINUX_MAX_BACKOFF_MS: u64 = 60_000;
#[cfg(feature = "aur")]
const JITTER_MAX_MS: u64 = 500;
#[cfg(feature = "aur")]
pub async fn rate_limit_archlinux() -> tokio::sync::OwnedSemaphorePermit {
let permit = ARCHLINUX_REQUEST_SEMAPHORE
.clone()
.acquire_owned()
.await
.expect("archlinux.org request semaphore should never be closed");
let delay_needed = {
let mut limiter = match ARCHLINUX_RATE_LIMITER.lock() {
Ok(guard) => guard,
Err(poisoned) => poisoned.into_inner(),
};
let elapsed = limiter.last_request.elapsed();
let min_delay = Duration::from_millis(limiter.current_backoff_ms);
let delay = if elapsed < min_delay {
min_delay.checked_sub(elapsed).unwrap_or(Duration::ZERO)
} else {
Duration::ZERO
};
limiter.last_request = Instant::now();
delay
};
if !delay_needed.is_zero() {
let jitter_ms = rand::rng().random_range(0..=JITTER_MAX_MS);
let delay_with_jitter = delay_needed + Duration::from_millis(jitter_ms);
#[allow(clippy::cast_possible_truncation)] let delay_ms = delay_needed.as_millis() as u64;
debug!(
delay_ms,
jitter_ms,
total_ms = delay_with_jitter.as_millis(),
"rate limiting archlinux.org request with jitter"
);
tokio::time::sleep(delay_with_jitter).await;
}
permit
}
#[cfg(feature = "aur")]
pub fn increase_archlinux_backoff(retry_after_seconds: Option<u64>) {
let mut limiter = match ARCHLINUX_RATE_LIMITER.lock() {
Ok(guard) => guard,
Err(poisoned) => poisoned.into_inner(),
};
limiter.consecutive_failures += 1;
if let Some(retry_after) = retry_after_seconds {
let retry_after_ms = (retry_after * 1000).min(ARCHLINUX_MAX_BACKOFF_MS);
limiter.current_backoff_ms = retry_after_ms;
warn!(
consecutive_failures = limiter.consecutive_failures,
retry_after_seconds = retry_after,
backoff_ms = limiter.current_backoff_ms,
"increased archlinux.org backoff delay using Retry-After header"
);
} else {
limiter.current_backoff_ms = (limiter.current_backoff_ms * 2).min(ARCHLINUX_MAX_BACKOFF_MS);
warn!(
consecutive_failures = limiter.consecutive_failures,
backoff_ms = limiter.current_backoff_ms,
"increased archlinux.org backoff delay"
);
}
}
#[cfg(feature = "aur")]
pub fn reset_archlinux_backoff() {
let mut limiter = match ARCHLINUX_RATE_LIMITER.lock() {
Ok(guard) => guard,
Err(poisoned) => poisoned.into_inner(),
};
if limiter.consecutive_failures > 0 {
debug!(
previous_failures = limiter.consecutive_failures,
previous_backoff_ms = limiter.current_backoff_ms,
"resetting archlinux.org backoff after successful request"
);
}
limiter.current_backoff_ms = ARCHLINUX_BASE_DELAY_MS;
limiter.consecutive_failures = 0;
}
#[cfg(feature = "aur")]
#[must_use]
pub fn is_archlinux_url(url: &str) -> bool {
url.contains("archlinux.org")
}
#[cfg(feature = "aur")]
#[must_use]
pub fn is_retryable_error(error: &reqwest::Error) -> (bool, Option<u64>) {
if error.is_timeout() {
return (true, None);
}
if error.is_connect() || error.is_request() {
return (true, None);
}
if let Some(status) = error.status() {
let code = status.as_u16();
if (500..600).contains(&code) {
return (true, None);
}
if code == 429 {
return (true, None);
}
if (400..500).contains(&code) {
return (false, None);
}
if (300..400).contains(&code) {
return (false, None);
}
}
(false, None)
}
#[cfg(feature = "aur")]
#[must_use]
pub fn extract_retry_after(response: &reqwest::Response) -> Option<u64> {
response
.headers()
.get(reqwest::header::RETRY_AFTER)
.and_then(|value| value.to_str().ok())
.and_then(|s| s.parse::<u64>().ok())
}
#[cfg(feature = "aur")]
pub async fn retry_with_policy<F, Fut, T>(
policy: &RetryPolicy,
operation_name: &str,
context: &str,
mut operation: F,
) -> Result<T>
where
F: FnMut() -> Fut,
Fut: std::future::Future<Output = Result<T>>,
{
if !policy.enabled {
return operation().await;
}
let mut last_error: Option<ArchToolkitError> = None;
let mut retry_after_seconds: Option<u64> = None;
for attempt in 0..=policy.max_retries {
let result = operation().await;
match result {
Ok(value) => {
if attempt > 0 {
debug!(
operation = operation_name,
context = %context,
attempt = attempt + 1,
"operation succeeded after retries"
);
}
return Ok(value);
}
Err(
ArchToolkitError::Network(ref e)
| ArchToolkitError::SearchFailed { source: ref e, .. }
| ArchToolkitError::InfoFailed { source: ref e, .. }
| ArchToolkitError::CommentsFailed { source: ref e, .. }
| ArchToolkitError::PkgbuildFailed { source: ref e, .. },
) => {
let (is_retryable, _) = is_retryable_error(e);
let Err(error) = result else {
unreachable!();
};
if !is_retryable {
return Err(error);
}
if attempt >= policy.max_retries {
warn!(
operation = operation_name,
context = %context,
max_retries = policy.max_retries,
"max retries exhausted"
);
return Err(error);
}
last_error = Some(error);
let base_delay_ms = retry_after_seconds.map_or_else(
|| {
let delay = policy.initial_delay_ms * (1u64 << attempt.min(20)); delay.min(policy.max_delay_ms)
},
|retry_after| {
(retry_after * 1000).min(policy.max_delay_ms)
},
);
let jitter_ms = rand::rng().random_range(0..=policy.jitter_max_ms);
let total_delay_ms = base_delay_ms + jitter_ms;
let delay = Duration::from_millis(total_delay_ms);
warn!(
operation = operation_name,
context = %context,
attempt = attempt + 1,
max_retries = policy.max_retries,
delay_ms = total_delay_ms,
base_delay_ms,
jitter_ms,
"retrying operation after error"
);
tokio::time::sleep(delay).await;
retry_after_seconds = None; }
Err(e) => {
return Err(e);
}
}
}
Err(last_error.unwrap_or_else(|| {
ArchToolkitError::Parse(format!(
"retry exhausted without error for {operation_name} (context: {context})"
))
}))
}
#[cfg(feature = "aur")]
const DEFAULT_TIMEOUT_SECS: u64 = 30;
#[cfg(feature = "aur")]
const DEFAULT_USER_AGENT: &str = "arch-toolkit/0.1.0";
#[cfg(feature = "aur")]
const DEFAULT_HEALTH_CHECK_TIMEOUT_SECS: u64 = 5;
#[cfg(feature = "aur")]
#[derive(Debug, Clone)]
#[allow(clippy::struct_excessive_bools)] pub struct RetryPolicy {
pub max_retries: u32,
pub initial_delay_ms: u64,
pub max_delay_ms: u64,
pub jitter_max_ms: u64,
pub enabled: bool,
pub retry_search: bool,
pub retry_info: bool,
pub retry_comments: bool,
pub retry_pkgbuild: bool,
}
#[cfg(feature = "aur")]
impl Default for RetryPolicy {
fn default() -> Self {
Self {
max_retries: 3,
initial_delay_ms: 1000,
max_delay_ms: 30_000,
jitter_max_ms: 500,
enabled: true,
retry_search: true,
retry_info: true,
retry_comments: true,
retry_pkgbuild: true,
}
}
}
#[cfg(feature = "aur")]
#[derive(Debug)]
pub struct ArchClient {
http_client: ReqwestClient,
#[allow(dead_code)]
user_agent: String,
#[allow(dead_code)]
timeout: Duration,
retry_policy: RetryPolicy,
cache: Option<CacheWrapper>,
cache_config: Option<CacheConfig>,
validation_config: ValidationConfig,
health_check_timeout: Duration,
}
#[cfg(feature = "aur")]
impl ArchClient {
pub fn new() -> Result<Self> {
Self::builder().build()
}
#[must_use]
pub const fn builder() -> ArchClientBuilder {
ArchClientBuilder::new()
}
#[must_use]
pub const fn aur(&self) -> crate::aur::Aur<'_> {
crate::aur::Aur::new(self)
}
pub(crate) const fn http_client(&self) -> &ReqwestClient {
&self.http_client
}
pub(crate) const fn retry_policy(&self) -> &RetryPolicy {
&self.retry_policy
}
pub(crate) const fn cache(&self) -> Option<&CacheWrapper> {
self.cache.as_ref()
}
pub(crate) const fn cache_config(&self) -> Option<&CacheConfig> {
self.cache_config.as_ref()
}
pub(crate) const fn validation_config(&self) -> &ValidationConfig {
&self.validation_config
}
#[must_use]
pub const fn invalidate_cache(&self) -> CacheInvalidator<'_> {
CacheInvalidator::new(self)
}
pub async fn health_check(&self) -> Result<bool> {
let status = self.health_status().await?;
Ok(status.is_healthy())
}
pub async fn health_status(&self) -> Result<crate::types::HealthStatus> {
crate::health::check_health(&self.http_client, Some(self.health_check_timeout)).await
}
}
#[cfg(feature = "aur")]
pub struct CacheInvalidator<'a> {
client: &'a ArchClient,
}
#[cfg(feature = "aur")]
impl<'a> CacheInvalidator<'a> {
const fn new(client: &'a ArchClient) -> Self {
Self { client }
}
#[must_use]
pub fn search(&self, query: &str) -> &Self {
if let Some(cache) = self.client.cache() {
let key = crate::cache::cache_key_search(query);
let _ = cache.invalidate(&key);
}
self
}
#[must_use]
pub fn info(&self, names: &[&str]) -> &Self {
if let Some(cache) = self.client.cache() {
let key = crate::cache::cache_key_info(names);
let _ = cache.invalidate(&key);
}
self
}
#[must_use]
pub fn comments(&self, pkgname: &str) -> &Self {
if let Some(cache) = self.client.cache() {
let key = crate::cache::cache_key_comments(pkgname);
let _ = cache.invalidate(&key);
}
self
}
#[must_use]
pub fn pkgbuild(&self, package: &str) -> &Self {
if let Some(cache) = self.client.cache() {
let key = crate::cache::cache_key_pkgbuild(package);
let _ = cache.invalidate(&key);
}
self
}
#[must_use]
pub fn package(&self, package: &str) -> &Self {
let _ = self.comments(package).pkgbuild(package);
self
}
#[must_use]
pub fn all(&self) -> &Self {
if let Some(cache) = self.client.cache() {
let _ = cache.clear();
}
self
}
}
#[cfg(feature = "aur")]
#[derive(Debug, Clone)]
pub struct ArchClientBuilder {
timeout: Option<Duration>,
user_agent: Option<String>,
retry_policy: Option<RetryPolicy>,
cache_config: Option<CacheConfig>,
validation_config: Option<ValidationConfig>,
health_check_timeout: Option<Duration>,
}
#[cfg(feature = "aur")]
impl ArchClientBuilder {
#[must_use]
pub const fn new() -> Self {
Self {
timeout: None,
user_agent: None,
retry_policy: None,
cache_config: None,
validation_config: None,
health_check_timeout: None,
}
}
#[must_use]
#[allow(clippy::missing_const_for_fn)] pub fn from_env() -> Self {
Self::new().with_env()
}
#[must_use]
#[allow(clippy::missing_const_for_fn)] pub fn from_env_chain(self) -> Self {
self.with_env()
}
#[must_use]
#[allow(clippy::missing_const_for_fn)] pub fn with_env(mut self) -> Self {
if let Some(timeout) = env::env_timeout() {
self.timeout = Some(timeout);
}
if let Some(user_agent) = env::env_user_agent() {
self.user_agent = Some(user_agent);
}
if let Some(health_timeout) = env::env_health_check_timeout() {
self.health_check_timeout = Some(health_timeout);
}
let mut retry_policy_modified = false;
let mut retry_policy = self.retry_policy.clone().unwrap_or_default();
if let Some(max_retries) = env::env_max_retries() {
retry_policy.max_retries = max_retries;
retry_policy_modified = true;
}
if let Some(enabled) = env::env_retry_enabled() {
retry_policy.enabled = enabled;
retry_policy_modified = true;
}
if let Some(initial_delay) = env::env_retry_initial_delay_ms() {
retry_policy.initial_delay_ms = initial_delay;
retry_policy_modified = true;
}
if let Some(max_delay) = env::env_retry_max_delay_ms() {
retry_policy.max_delay_ms = max_delay;
retry_policy_modified = true;
}
if retry_policy_modified {
self.retry_policy = Some(retry_policy);
}
if let Some(strict) = env::env_validation_strict() {
let mut validation_config = self.validation_config.clone().unwrap_or_default();
validation_config.strict_empty = strict;
self.validation_config = Some(validation_config);
}
if let Some(cache_size) = env::env_cache_size() {
let mut cache_config = self.cache_config.clone().unwrap_or_default();
cache_config.memory_cache_size = cache_size;
self.cache_config = Some(cache_config);
}
self
}
#[must_use]
#[allow(clippy::missing_const_for_fn)] pub fn timeout(mut self, timeout: Duration) -> Self {
self.timeout = Some(timeout);
self
}
#[must_use]
#[allow(clippy::missing_const_for_fn)] pub fn user_agent(mut self, user_agent: impl Into<String>) -> Self {
self.user_agent = Some(user_agent.into());
self
}
#[must_use]
#[allow(clippy::missing_const_for_fn)] pub fn retry_policy(mut self, policy: RetryPolicy) -> Self {
self.retry_policy = Some(policy);
self
}
#[must_use]
#[allow(clippy::missing_const_for_fn)] pub fn max_retries(mut self, max_retries: u32) -> Self {
let mut policy = self.retry_policy.unwrap_or_default();
policy.max_retries = max_retries;
self.retry_policy = Some(policy);
self
}
#[must_use]
#[allow(clippy::missing_const_for_fn)] pub fn retry_enabled(mut self, enabled: bool) -> Self {
let mut policy = self.retry_policy.unwrap_or_default();
policy.enabled = enabled;
self.retry_policy = Some(policy);
self
}
#[must_use]
#[allow(clippy::missing_const_for_fn)] pub fn retry_operation(mut self, operation: &str, enabled: bool) -> Self {
let mut policy = self.retry_policy.unwrap_or_default();
match operation {
"search" => policy.retry_search = enabled,
"info" => policy.retry_info = enabled,
"comments" => policy.retry_comments = enabled,
"pkgbuild" => policy.retry_pkgbuild = enabled,
_ => {} }
self.retry_policy = Some(policy);
self
}
#[must_use]
#[allow(clippy::missing_const_for_fn)] pub fn cache_config(mut self, config: CacheConfig) -> Self {
self.cache_config = Some(config);
self
}
#[must_use]
#[allow(clippy::missing_const_for_fn)] pub fn validation_config(mut self, config: ValidationConfig) -> Self {
self.validation_config = Some(config);
self
}
#[must_use]
#[allow(clippy::missing_const_for_fn)] pub fn health_check_timeout(mut self, timeout: Duration) -> Self {
self.health_check_timeout = Some(timeout);
self
}
pub fn build(self) -> Result<ArchClient> {
let timeout = self
.timeout
.unwrap_or_else(|| Duration::from_secs(DEFAULT_TIMEOUT_SECS));
let user_agent = self
.user_agent
.unwrap_or_else(|| DEFAULT_USER_AGENT.to_string());
let retry_policy = self.retry_policy.unwrap_or_default();
let validation_config = self.validation_config.unwrap_or_default();
let health_check_timeout = self
.health_check_timeout
.unwrap_or_else(|| Duration::from_secs(DEFAULT_HEALTH_CHECK_TIMEOUT_SECS));
let http_client = ReqwestClient::builder()
.timeout(timeout)
.user_agent(&user_agent)
.build()
.map_err(ArchToolkitError::Network)?;
let cache = self
.cache_config
.as_ref()
.map(CacheWrapper::new)
.transpose()
.map_err(|e| ArchToolkitError::Parse(format!("Failed to create cache: {e}")))?;
Ok(ArchClient {
http_client,
user_agent,
timeout,
retry_policy,
cache,
cache_config: self.cache_config,
validation_config,
health_check_timeout,
})
}
}
#[cfg(feature = "aur")]
impl Default for ArchClientBuilder {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
#[cfg(feature = "aur")]
mod tests {
use super::*;
#[test]
fn test_arch_client_new() {
let client = ArchClient::new();
assert!(client.is_ok(), "ArchClient::new() should succeed");
}
#[test]
fn test_arch_client_builder_defaults() {
let client = ArchClient::builder().build();
assert!(
client.is_ok(),
"ArchClientBuilder with defaults should succeed"
);
}
#[test]
fn test_arch_client_builder_custom_timeout() {
let client = ArchClient::builder()
.timeout(Duration::from_secs(60))
.build();
assert!(
client.is_ok(),
"ArchClientBuilder with custom timeout should succeed"
);
}
#[test]
fn test_arch_client_builder_custom_user_agent() {
let client = ArchClient::builder().user_agent("test-agent/1.0").build();
assert!(
client.is_ok(),
"ArchClientBuilder with custom user agent should succeed"
);
}
#[test]
fn test_arch_client_builder_all_options() {
let client = ArchClient::builder()
.timeout(Duration::from_secs(45))
.user_agent("my-app/2.0")
.build();
assert!(
client.is_ok(),
"ArchClientBuilder with all options should succeed"
);
}
#[test]
fn test_arch_client_aur_access() {
let client = ArchClient::new().expect("client creation should succeed");
let _aur = client.aur();
}
#[test]
fn test_retry_policy_default() {
let policy = RetryPolicy::default();
assert_eq!(policy.max_retries, 3);
assert_eq!(policy.initial_delay_ms, 1000);
assert_eq!(policy.max_delay_ms, 30_000);
assert_eq!(policy.jitter_max_ms, 500);
assert!(policy.enabled);
assert!(policy.retry_search);
assert!(policy.retry_info);
assert!(policy.retry_comments);
assert!(policy.retry_pkgbuild);
}
#[test]
fn test_arch_client_builder_retry_policy() {
let policy = RetryPolicy {
max_retries: 5,
..Default::default()
};
let client = ArchClient::builder().retry_policy(policy).build();
assert!(
client.is_ok(),
"ArchClientBuilder with retry policy should succeed"
);
}
#[test]
fn test_arch_client_builder_max_retries() {
let client = ArchClient::builder().max_retries(5).build();
assert!(
client.is_ok(),
"ArchClientBuilder with max_retries should succeed"
);
let client = client.expect("client creation should succeed");
assert_eq!(client.retry_policy().max_retries, 5);
}
#[test]
fn test_arch_client_builder_retry_enabled() {
let client = ArchClient::builder().retry_enabled(false).build();
assert!(
client.is_ok(),
"ArchClientBuilder with retry_enabled should succeed"
);
let client = client.expect("client creation should succeed");
assert!(!client.retry_policy().enabled);
}
#[test]
fn test_arch_client_builder_retry_operation() {
let client = ArchClient::builder()
.retry_operation("pkgbuild", false)
.build();
assert!(
client.is_ok(),
"ArchClientBuilder with retry_operation should succeed"
);
let client = client.expect("client creation should succeed");
assert!(!client.retry_policy().retry_pkgbuild);
assert!(client.retry_policy().retry_search); }
#[test]
fn test_is_retryable_error_timeout() {
let result = std::panic::catch_unwind(|| {
true
});
assert!(result.is_ok());
}
#[test]
fn test_retry_policy_clone() {
let policy1 = RetryPolicy::default();
let policy2 = policy1.clone();
assert_eq!(policy1.max_retries, policy2.max_retries);
assert_eq!(policy1.enabled, policy2.enabled);
}
#[tokio::test]
async fn test_health_check_integration() {
let client = ArchClient::new().expect("client creation should succeed");
let result = client.health_check().await;
if result.is_ok() {
} else {
}
}
#[tokio::test]
async fn test_health_status_integration() {
let client = ArchClient::new().expect("client creation should succeed");
let result = client.health_status().await;
if let Ok(status) = result {
let _ = status.aur_api;
let _ = status.latency;
let _ = status.checked_at;
} else {
}
}
#[test]
fn test_arch_client_builder_health_check_timeout() {
let client = ArchClient::builder()
.health_check_timeout(Duration::from_secs(10))
.build();
assert!(
client.is_ok(),
"ArchClientBuilder with health_check_timeout should succeed"
);
}
#[test]
fn test_arch_client_builder_from_env_timeout() {
unsafe {
std::env::set_var("ARCH_TOOLKIT_TIMEOUT", "60");
}
let client = ArchClientBuilder::from_env().build();
assert!(
client.is_ok(),
"ArchClientBuilder::from_env() with timeout should succeed"
);
let client = client.expect("client creation should succeed");
assert_eq!(client.timeout, Duration::from_secs(60));
unsafe {
std::env::remove_var("ARCH_TOOLKIT_TIMEOUT");
}
}
#[test]
fn test_arch_client_builder_from_env_user_agent() {
unsafe {
std::env::set_var("ARCH_TOOLKIT_USER_AGENT", "test-env-agent/1.0");
}
let client = ArchClientBuilder::from_env().build();
assert!(
client.is_ok(),
"ArchClientBuilder::from_env() with user agent should succeed"
);
let client = client.expect("client creation should succeed");
assert_eq!(client.user_agent, "test-env-agent/1.0");
unsafe {
std::env::remove_var("ARCH_TOOLKIT_USER_AGENT");
}
}
#[test]
fn test_arch_client_builder_from_env_health_check_timeout() {
unsafe {
std::env::set_var("ARCH_TOOLKIT_HEALTH_CHECK_TIMEOUT", "10");
}
let client = ArchClientBuilder::from_env().build();
assert!(
client.is_ok(),
"ArchClientBuilder::from_env() with health check timeout should succeed"
);
let client = client.expect("client creation should succeed");
assert_eq!(client.health_check_timeout, Duration::from_secs(10));
unsafe {
std::env::remove_var("ARCH_TOOLKIT_HEALTH_CHECK_TIMEOUT");
}
}
#[test]
fn test_arch_client_builder_from_env_max_retries() {
unsafe {
std::env::set_var("ARCH_TOOLKIT_MAX_RETRIES", "5");
}
let client = ArchClientBuilder::from_env().build();
assert!(
client.is_ok(),
"ArchClientBuilder::from_env() with max retries should succeed"
);
let client = client.expect("client creation should succeed");
assert_eq!(client.retry_policy().max_retries, 5);
unsafe {
std::env::remove_var("ARCH_TOOLKIT_MAX_RETRIES");
}
}
#[test]
fn test_arch_client_builder_from_env_retry_enabled() {
unsafe {
std::env::set_var("ARCH_TOOLKIT_RETRY_ENABLED", "false");
}
let client = ArchClientBuilder::from_env().build();
assert!(
client.is_ok(),
"ArchClientBuilder::from_env() with retry enabled should succeed"
);
let client = client.expect("client creation should succeed");
assert!(!client.retry_policy().enabled);
unsafe {
std::env::remove_var("ARCH_TOOLKIT_RETRY_ENABLED");
}
}
#[test]
fn test_arch_client_builder_from_env_retry_delays() {
unsafe {
std::env::set_var("ARCH_TOOLKIT_RETRY_INITIAL_DELAY_MS", "2000");
std::env::set_var("ARCH_TOOLKIT_RETRY_MAX_DELAY_MS", "60000");
}
let client = ArchClientBuilder::from_env().build();
assert!(
client.is_ok(),
"ArchClientBuilder::from_env() with retry delays should succeed"
);
let client = client.expect("client creation should succeed");
assert_eq!(client.retry_policy().initial_delay_ms, 2000);
assert_eq!(client.retry_policy().max_delay_ms, 60000);
unsafe {
std::env::remove_var("ARCH_TOOLKIT_RETRY_INITIAL_DELAY_MS");
std::env::remove_var("ARCH_TOOLKIT_RETRY_MAX_DELAY_MS");
}
}
#[test]
fn test_arch_client_builder_from_env_validation_strict() {
unsafe {
std::env::set_var("ARCH_TOOLKIT_VALIDATION_STRICT", "false");
}
let client = ArchClientBuilder::from_env().build();
assert!(
client.is_ok(),
"ArchClientBuilder::from_env() with validation strict should succeed"
);
let client = client.expect("client creation should succeed");
assert!(!client.validation_config().strict_empty);
unsafe {
std::env::remove_var("ARCH_TOOLKIT_VALIDATION_STRICT");
}
}
#[test]
fn test_arch_client_builder_from_env_cache_size() {
unsafe {
std::env::set_var("ARCH_TOOLKIT_CACHE_SIZE", "200");
}
let cache_config = crate::cache::CacheConfigBuilder::new()
.enable_search(true)
.build();
let client = ArchClientBuilder::from_env()
.cache_config(cache_config)
.build();
assert!(
client.is_ok(),
"ArchClientBuilder::from_env() with cache size should succeed"
);
unsafe {
std::env::remove_var("ARCH_TOOLKIT_CACHE_SIZE");
}
}
#[test]
fn test_arch_client_builder_from_env_missing_vars() {
unsafe {
std::env::remove_var("ARCH_TOOLKIT_TIMEOUT");
}
unsafe {
std::env::remove_var("ARCH_TOOLKIT_USER_AGENT");
}
unsafe {
std::env::remove_var("ARCH_TOOLKIT_HEALTH_CHECK_TIMEOUT");
}
unsafe {
std::env::remove_var("ARCH_TOOLKIT_MAX_RETRIES");
}
unsafe {
std::env::remove_var("ARCH_TOOLKIT_RETRY_ENABLED");
std::env::remove_var("ARCH_TOOLKIT_RETRY_INITIAL_DELAY_MS");
std::env::remove_var("ARCH_TOOLKIT_RETRY_MAX_DELAY_MS");
std::env::remove_var("ARCH_TOOLKIT_VALIDATION_STRICT");
}
unsafe {
std::env::remove_var("ARCH_TOOLKIT_CACHE_SIZE");
}
let client = ArchClientBuilder::from_env().build();
assert!(
client.is_ok(),
"ArchClientBuilder::from_env() with missing vars should use defaults"
);
let client = client.expect("client creation should succeed");
assert_eq!(client.timeout, Duration::from_secs(DEFAULT_TIMEOUT_SECS));
assert_eq!(client.user_agent, DEFAULT_USER_AGENT);
assert_eq!(
client.health_check_timeout,
Duration::from_secs(DEFAULT_HEALTH_CHECK_TIMEOUT_SECS)
);
}
#[test]
fn test_arch_client_builder_with_env_overrides() {
let client = ArchClient::builder()
.timeout(Duration::from_secs(30))
.user_agent("code-agent/1.0")
.build();
assert!(client.is_ok());
unsafe {
std::env::set_var("ARCH_TOOLKIT_TIMEOUT", "60");
std::env::set_var("ARCH_TOOLKIT_USER_AGENT", "env-agent/1.0");
}
let client = ArchClient::builder()
.timeout(Duration::from_secs(30))
.user_agent("code-agent/1.0")
.with_env() .build();
assert!(
client.is_ok(),
"ArchClientBuilder::with_env() should override code values"
);
let client = client.expect("client creation should succeed");
assert_eq!(client.timeout, Duration::from_secs(60));
assert_eq!(client.user_agent, "env-agent/1.0");
unsafe {
std::env::remove_var("ARCH_TOOLKIT_TIMEOUT");
}
unsafe {
std::env::remove_var("ARCH_TOOLKIT_USER_AGENT");
}
}
#[test]
fn test_arch_client_builder_with_env_partial_override() {
unsafe {
std::env::set_var("ARCH_TOOLKIT_TIMEOUT", "90");
}
let client = ArchClient::builder()
.timeout(Duration::from_secs(30))
.user_agent("code-agent/1.0")
.with_env() .build();
assert!(
client.is_ok(),
"ArchClientBuilder::with_env() should partially override"
);
let client = client.expect("client creation should succeed");
assert_eq!(client.timeout, Duration::from_secs(90)); assert_eq!(client.user_agent, "code-agent/1.0"); unsafe {
std::env::remove_var("ARCH_TOOLKIT_TIMEOUT");
}
}
#[test]
fn test_arch_client_builder_from_env_invalid_values() {
unsafe {
std::env::set_var("ARCH_TOOLKIT_TIMEOUT", "invalid");
std::env::set_var("ARCH_TOOLKIT_MAX_RETRIES", "not-a-number");
std::env::set_var("ARCH_TOOLKIT_RETRY_ENABLED", "maybe");
}
let client = ArchClientBuilder::from_env().build();
assert!(
client.is_ok(),
"ArchClientBuilder::from_env() should ignore invalid values"
);
let client = client.expect("client creation should succeed");
assert_eq!(client.timeout, Duration::from_secs(DEFAULT_TIMEOUT_SECS));
assert_eq!(client.retry_policy().max_retries, 3); assert!(client.retry_policy().enabled);
unsafe {
std::env::remove_var("ARCH_TOOLKIT_TIMEOUT");
}
unsafe {
std::env::remove_var("ARCH_TOOLKIT_MAX_RETRIES");
}
unsafe {
std::env::remove_var("ARCH_TOOLKIT_RETRY_ENABLED");
}
}
}