//! Runtime-tunable timing/threshold config for the workspace tool server. //! //! All values are read once at startup from `KIGI_WORKSPACE_*` environment //! variables via [`StatusConfig::from_env`]. Unset or unparseable variables //! fall back to the documented defaults (with a `warn!` on parse failure), so //! construction never fails. use std::str::FromStr; use std::time::Duration; // ── Default timing/threshold values ────────────────────────────────────── // Single source of truth for the `StatusConfig::default()` values and the // documented fallbacks for each `KIGI_WORKSPACE_*` env var. /// Default interval between status/heartbeat emissions. const DEFAULT_HEARTBEAT_SECS: u64 = 30; /// Default transport keepalive interval. Should exceed `heartbeat`. const DEFAULT_KEEPALIVE_SECS: u64 = 60; /// Default WebSocket keepalive ping cadence for the server SDK connection. const DEFAULT_WS_PING_SECS: u64 = 30; /// Default number of consecutive hub reconnect failures before warning. const DEFAULT_HUB_WARN_THRESHOLD: u32 = 5; /// Default base delay (ms) for exponential backoff on failed hub sends. const DEFAULT_HUB_BACKOFF_BASE_MS: u64 = 100; /// Default idle window (s) after which an inactive session is pruned. Kept /// aligned with the sandbox service's idle-hibernate grace. const DEFAULT_SESSION_IDLE_PRUNE_SECS: u64 = 1800; /// Default max time (s) to wait for in-flight work to drain on shutdown. const DEFAULT_DRAIN_TIMEOUT_SECS: u64 = 30; /// Default per-call timeout (s) for agent gRPC RPCs. const DEFAULT_AGENT_RPC_TIMEOUT_SECS: u64 = 30; /// Default timeout (s) for establishing an agent connection. const DEFAULT_AGENT_CONNECT_TIMEOUT_SECS: u64 = 5; /// Default preview-activity withhold window. Sourced from the tracker's /// `PREVIEW_ACTIVITY_WINDOW_MS` so the two can't drift. const DEFAULT_PREVIEW_ACTIVITY_WINDOW_MS: u64 = crate::activity::PREVIEW_ACTIVITY_WINDOW_MS; /// Default preview-activity scrape cadence. Must stay below the withhold window. const DEFAULT_PREVIEW_ACTIVITY_SCRAPE_INTERVAL_MS: u64 = 10_000; /// Smallest window that still leaves room for a strictly-smaller scrape; only a /// broken config reaches it (the normal window is 60s). const MIN_PREVIEW_ACTIVITY_WINDOW_MS: u64 = 2; /// Scrape-interval floor; `0` would busy-loop the scraper. const MIN_PREVIEW_ACTIVITY_SCRAPE_INTERVAL_MS: u64 = 1; /// Tunable timing/threshold constants for the workspace tool server. #[derive(Debug, Clone)] pub struct StatusConfig { /// Interval between status/heartbeat emissions. pub heartbeat: Duration, /// Transport keepalive interval. Should exceed `heartbeat`. pub keepalive: Duration, /// WebSocket keepalive ping cadence for the server SDK connection. pub ws_ping: Duration, /// Reconnect backoff schedule for the server SDK connection. `None` leaves /// the SDK's built-in default exponential schedule in place. pub ws_reconnect_backoff: Option>, /// Number of consecutive server reconnect failures before warning. pub hub_warn_threshold: u32, /// Base delay for exponential backoff on failed server event-notification sends. pub hub_backoff_base: Duration, /// Idle duration after which an inactive session is pruned. pub session_idle_prune: Duration, /// Legacy single-phase drain timeout (`KIGI_WORKSPACE_DRAIN_TIMEOUT_SECS`), /// retained for compatibility; the SIGTERM and server-evict paths now use the /// two-phase drain bounded by `KIGI_WORKSPACE_TERMINATION_GRACE_MS`. pub drain_timeout: Duration, /// Per-call timeout for agent RPCs. pub agent_rpc_timeout: Duration, /// Timeout for establishing an agent connection. pub agent_connect_timeout: Duration, /// Opt-in foreground-only idle (`KIGI_WORKSPACE_IDLE_IGNORE_BACKGROUND_TASKS`); /// requires the literal `"true"` — other spellings fall back to this default. pub idle_ignores_background: bool, /// Recent preview-proxy traffic withholds idle for this window /// (`KIGI_WORKSPACE_PREVIEW_ACTIVITY_WINDOW_MS`). pub preview_activity_window: Duration, /// Cadence at which the preview-activity scraper polls the proxy /// (`KIGI_WORKSPACE_PREVIEW_ACTIVITY_SCRAPE_INTERVAL_MS`); kept strictly /// below `preview_activity_window` by [`validate`](Self::validate). pub preview_activity_scrape_interval: Duration, /// True when this container booted via the sandbox restore path, which /// injects `KIGI_SESSION_RESTORED=true`; a first boot never does. pub session_restored: bool, /// True when restore injects `KIGI_REVIVE_SCRIPT_CONFIGURED=true` (launchable /// revive configured); unset on first boot and non-launchable restores. pub revive_script_configured: bool, } impl Default for StatusConfig { fn default() -> Self { Self { heartbeat: Duration::from_secs(DEFAULT_HEARTBEAT_SECS), keepalive: Duration::from_secs(DEFAULT_KEEPALIVE_SECS), ws_ping: Duration::from_secs(DEFAULT_WS_PING_SECS), ws_reconnect_backoff: None, hub_warn_threshold: DEFAULT_HUB_WARN_THRESHOLD, hub_backoff_base: Duration::from_millis(DEFAULT_HUB_BACKOFF_BASE_MS), session_idle_prune: Duration::from_secs(DEFAULT_SESSION_IDLE_PRUNE_SECS), drain_timeout: Duration::from_secs(DEFAULT_DRAIN_TIMEOUT_SECS), agent_rpc_timeout: Duration::from_secs(DEFAULT_AGENT_RPC_TIMEOUT_SECS), agent_connect_timeout: Duration::from_secs(DEFAULT_AGENT_CONNECT_TIMEOUT_SECS), idle_ignores_background: false, preview_activity_window: Duration::from_millis(DEFAULT_PREVIEW_ACTIVITY_WINDOW_MS), preview_activity_scrape_interval: Duration::from_millis( DEFAULT_PREVIEW_ACTIVITY_SCRAPE_INTERVAL_MS, ), session_restored: false, revive_script_configured: false, } } } impl StatusConfig { /// Populate from `KIGI_WORKSPACE_*`. Unset or unparseable vars fall /// back to the default with a `warn!`. Never fails. pub fn from_env() -> Self { let defaults = Self::default(); let (agent_rpc, agent_connect) = Self::agent_timeouts_from_env(); let mut cfg = Self { heartbeat: secs_or("KIGI_WORKSPACE_HEARTBEAT_SECS", defaults.heartbeat), keepalive: secs_or("KIGI_WORKSPACE_KEEPALIVE_SECS", defaults.keepalive), ws_ping: secs_or("KIGI_WORKSPACE_WS_PING_SECS", defaults.ws_ping), ws_reconnect_backoff: backoff_schedule_from_env( "KIGI_WORKSPACE_WS_RECONNECT_BACKOFF_MS", ), hub_warn_threshold: parse_or( "KIGI_WORKSPACE_HUB_WARN_THRESHOLD", defaults.hub_warn_threshold, ), hub_backoff_base: ms_or( "KIGI_WORKSPACE_HUB_BACKOFF_BASE_MS", defaults.hub_backoff_base, ), session_idle_prune: secs_or( "KIGI_WORKSPACE_SESSION_IDLE_PRUNE_SECS", defaults.session_idle_prune, ), drain_timeout: secs_or("KIGI_WORKSPACE_DRAIN_TIMEOUT_SECS", defaults.drain_timeout), agent_rpc_timeout: agent_rpc, agent_connect_timeout: agent_connect, idle_ignores_background: parse_or( "KIGI_WORKSPACE_IDLE_IGNORE_BACKGROUND_TASKS", defaults.idle_ignores_background, ), preview_activity_window: ms_or( "KIGI_WORKSPACE_PREVIEW_ACTIVITY_WINDOW_MS", defaults.preview_activity_window, ), preview_activity_scrape_interval: ms_or( "KIGI_WORKSPACE_PREVIEW_ACTIVITY_SCRAPE_INTERVAL_MS", defaults.preview_activity_scrape_interval, ), session_restored: std::env::var("KIGI_SESSION_RESTORED").as_deref() == Ok("true"), revive_script_configured: std::env::var("KIGI_REVIVE_SCRIPT_CONFIGURED").as_deref() == Ok("true"), }; cfg.validate(); cfg } /// Read only the agent gRPC `(request, connect)` timeouts from the /// environment, without parsing or validating the rest of the config. /// /// Used by the btrfs delegate's env-based construction path, which has no /// `StatusConfig` in scope; reading just these two vars avoids re-running /// [`validate`](Self::validate) (and its possible duplicate `warn!`). pub fn agent_timeouts_from_env() -> (Duration, Duration) { let defaults = Self::default(); const RPC_VAR: &str = "KIGI_WORKSPACE_AGENT_RPC_TIMEOUT_SECS"; const CONNECT_VAR: &str = "KIGI_WORKSPACE_AGENT_CONNECT_TIMEOUT_SECS"; ( nonzero_secs_or( RPC_VAR, parse_or(RPC_VAR, defaults.agent_rpc_timeout.as_secs()), defaults.agent_rpc_timeout, ), nonzero_secs_or( CONNECT_VAR, parse_or(CONNECT_VAR, defaults.agent_connect_timeout.as_secs()), defaults.agent_connect_timeout, ), ) } /// Warn on (and, where load-bearing, repair) inconsistent values. /// /// `keepalive` can't be validated against the server's idle window (unknown /// here), so it only warns. The preview scraper, however, must run strictly /// more often than the withhold window (else the withhold lapses between /// scrapes) and never at a zero interval (which would busy-loop it), so any /// misconfiguration is repaired into `1ms <= scrape < window`. fn validate(&mut self) { if self.keepalive <= self.heartbeat { tracing::warn!( keepalive = ?self.keepalive, heartbeat = ?self.heartbeat, "KIGI_WORKSPACE keepalive <= heartbeat; transport may time out between heartbeats" ); } let min_scrape = Duration::from_millis(MIN_PREVIEW_ACTIVITY_SCRAPE_INTERVAL_MS); let window = self .preview_activity_window .max(Duration::from_millis(MIN_PREVIEW_ACTIVITY_WINDOW_MS)); let scrape = self .preview_activity_scrape_interval .clamp(min_scrape, window - min_scrape); if window != self.preview_activity_window || scrape != self.preview_activity_scrape_interval { tracing::warn!( scrape_interval = ?self.preview_activity_scrape_interval, window = ?self.preview_activity_window, clamped_scrape = ?scrape, clamped_window = ?window, "KIGI_WORKSPACE preview scrape interval/window out of range; clamped to 1ms <= scrape < window" ); self.preview_activity_window = window; self.preview_activity_scrape_interval = scrape; } } } /// Read `var` and parse it as `T`. Returns `default` when unset; warns and /// returns `default` when present but unparseable. fn parse_or(var: &str, default: T) -> T { match std::env::var(var) { Err(_) => default, Ok(raw) => match raw.parse::() { Ok(value) => value, Err(_) => { tracing::warn!(var, value = %raw, "Unparseable KIGI_WORKSPACE value; using default"); default } }, } } /// Parse `var` as a `u64` number of seconds into a [`Duration`]. fn secs_or(var: &str, default: Duration) -> Duration { Duration::from_secs(parse_or(var, default.as_secs())) } /// Parse `var` as a `u64` number of milliseconds into a [`Duration`]. fn ms_or(var: &str, default: Duration) -> Duration { Duration::from_millis(parse_or(var, default.as_millis() as u64)) } /// Convert a parsed seconds value into a [`Duration`], rejecting `0`. A zero /// gRPC timeout makes every RPC fail immediately, so a configured `0` warns /// and falls back to `default` instead of being applied verbatim. fn nonzero_secs_or(var: &str, secs: u64, default: Duration) -> Duration { if secs == 0 { tracing::warn!( var, default = ?default, "KIGI_WORKSPACE agent timeout of 0s is invalid; using default" ); return default; } Duration::from_secs(secs) } /// Parse `var` as a comma-separated list of `u64` milliseconds into a reconnect /// backoff schedule. Returns `None` (keep the SDK's built-in default schedule) /// when the var is unset or yields no values, and warns + returns `None` when /// any element fails to parse. fn backoff_schedule_from_env(var: &str) -> Option> { let raw = std::env::var(var).ok()?; let mut schedule = Vec::new(); for part in raw.split(',').map(str::trim).filter(|p| !p.is_empty()) { match part.parse::() { Ok(ms) => schedule.push(Duration::from_millis(ms)), Err(_) => { tracing::warn!( var, value = %raw, "Unparseable KIGI_WORKSPACE backoff schedule; using SDK default" ); return None; } } } (!schedule.is_empty()).then_some(schedule) } #[cfg(test)] mod tests { use super::*; // Crate-shared env lock: every test that mutates the process environment // holds it for its full duration. ONE lock for the whole crate because the // hazard is the global `environ` array under `unsafe set_var` (not the // variable's value), so even disjoint vars must serialize. use crate::ENV_TEST_LOCK as ENV_LOCK; #[test] fn defaults_match_documented_values() { let cfg = StatusConfig::default(); assert_eq!(cfg.heartbeat, Duration::from_secs(30)); assert_eq!(cfg.keepalive, Duration::from_secs(60)); assert_eq!(cfg.ws_ping, Duration::from_secs(30)); assert_eq!(cfg.ws_reconnect_backoff, None); assert_eq!(cfg.hub_warn_threshold, 5); assert_eq!(cfg.hub_backoff_base, Duration::from_millis(100)); assert_eq!(cfg.session_idle_prune, Duration::from_secs(1800)); assert_eq!(cfg.drain_timeout, Duration::from_secs(30)); assert_eq!(cfg.agent_rpc_timeout, Duration::from_secs(30)); assert_eq!(cfg.agent_connect_timeout, Duration::from_secs(5)); assert!(!cfg.idle_ignores_background); assert_eq!(cfg.preview_activity_window, Duration::from_secs(60)); assert_eq!( cfg.preview_activity_scrape_interval, Duration::from_secs(10) ); assert!(!cfg.session_restored); assert!(!cfg.revive_script_configured); } /// `parse_or` returns the default when the variable is unset. Uses a /// uniquely-named var so it never collides with other tests' env writes. #[test] fn parse_or_unset_returns_default() { let _guard = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner()); let var = "KIGI_WORKSPACE_TEST_PARSE_OR_UNSET"; unsafe { std::env::remove_var(var) }; assert_eq!(parse_or::(var, 5), 5); } #[test] fn parse_or_valid_parses() { let _guard = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner()); let var = "KIGI_WORKSPACE_TEST_PARSE_OR_VALID"; unsafe { std::env::set_var(var, "42") }; assert_eq!(parse_or::(var, 5), 42); unsafe { std::env::remove_var(var) }; } #[test] fn parse_or_invalid_falls_back_without_panic() { let _guard = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner()); let var = "KIGI_WORKSPACE_TEST_PARSE_OR_INVALID"; unsafe { std::env::set_var(var, "not-a-number") }; assert_eq!(parse_or::(var, 5), 5); unsafe { std::env::remove_var(var) }; } #[test] fn secs_or_parses_into_duration() { let _guard = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner()); let var = "KIGI_WORKSPACE_TEST_SECS_OR_VALID"; unsafe { std::env::set_var(var, "120") }; assert_eq!( secs_or(var, Duration::from_secs(30)), Duration::from_secs(120) ); unsafe { std::env::remove_var(var) }; } #[test] fn secs_or_unset_returns_default() { let _guard = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner()); let var = "KIGI_WORKSPACE_TEST_SECS_OR_UNSET"; unsafe { std::env::remove_var(var) }; assert_eq!( secs_or(var, Duration::from_secs(30)), Duration::from_secs(30) ); } #[test] fn secs_or_invalid_falls_back_without_panic() { let _guard = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner()); let var = "KIGI_WORKSPACE_TEST_SECS_OR_INVALID"; unsafe { std::env::set_var(var, "12.5") }; assert_eq!( secs_or(var, Duration::from_secs(30)), Duration::from_secs(30) ); unsafe { std::env::remove_var(var) }; } #[test] fn ms_or_parses_into_duration() { let _guard = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner()); let var = "KIGI_WORKSPACE_TEST_MS_OR_VALID"; unsafe { std::env::set_var(var, "250") }; assert_eq!( ms_or(var, Duration::from_millis(100)), Duration::from_millis(250) ); unsafe { std::env::remove_var(var) }; } #[test] fn ms_or_invalid_falls_back_without_panic() { let _guard = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner()); let var = "KIGI_WORKSPACE_TEST_MS_OR_INVALID"; unsafe { std::env::set_var(var, "abc") }; assert_eq!( ms_or(var, Duration::from_millis(100)), Duration::from_millis(100) ); unsafe { std::env::remove_var(var) }; } /// With none of the `KIGI_WORKSPACE_*` vars set, `from_env` reproduces /// `StatusConfig::default()` field-for-field. /// /// This is the one test that touches the real (non-`_TEST_`-prefixed) /// var names: it `remove_var`s all of them before reading them. If a future /// test sets these shared names, run them serialized to avoid a race. #[test] fn from_env_clean_matches_default() { let _guard = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner()); for var in [ "KIGI_WORKSPACE_HEARTBEAT_SECS", "KIGI_WORKSPACE_KEEPALIVE_SECS", "KIGI_WORKSPACE_WS_PING_SECS", "KIGI_WORKSPACE_WS_RECONNECT_BACKOFF_MS", "KIGI_WORKSPACE_HUB_WARN_THRESHOLD", "KIGI_WORKSPACE_HUB_BACKOFF_BASE_MS", "KIGI_WORKSPACE_SESSION_IDLE_PRUNE_SECS", "KIGI_WORKSPACE_DRAIN_TIMEOUT_SECS", "KIGI_WORKSPACE_AGENT_RPC_TIMEOUT_SECS", "KIGI_WORKSPACE_AGENT_CONNECT_TIMEOUT_SECS", "KIGI_WORKSPACE_IDLE_IGNORE_BACKGROUND_TASKS", "KIGI_WORKSPACE_PREVIEW_ACTIVITY_WINDOW_MS", "KIGI_WORKSPACE_PREVIEW_ACTIVITY_SCRAPE_INTERVAL_MS", "KIGI_SESSION_RESTORED", "KIGI_REVIVE_SCRIPT_CONFIGURED", ] { unsafe { std::env::remove_var(var) }; } let cfg = StatusConfig::from_env(); let default = StatusConfig::default(); assert_eq!(cfg.heartbeat, default.heartbeat); assert_eq!(cfg.keepalive, default.keepalive); assert_eq!(cfg.ws_ping, default.ws_ping); assert_eq!(cfg.ws_reconnect_backoff, default.ws_reconnect_backoff); assert_eq!(cfg.hub_warn_threshold, default.hub_warn_threshold); assert_eq!(cfg.hub_backoff_base, default.hub_backoff_base); assert_eq!(cfg.session_idle_prune, default.session_idle_prune); assert_eq!(cfg.drain_timeout, default.drain_timeout); assert_eq!(cfg.agent_rpc_timeout, default.agent_rpc_timeout); assert_eq!(cfg.agent_connect_timeout, default.agent_connect_timeout); assert_eq!(cfg.idle_ignores_background, default.idle_ignores_background); assert_eq!(cfg.preview_activity_window, default.preview_activity_window); assert_eq!( cfg.preview_activity_scrape_interval, default.preview_activity_scrape_interval ); assert_eq!(cfg.session_restored, default.session_restored); assert_eq!( cfg.revive_script_configured, default.revive_script_configured ); } #[test] fn from_env_reads_session_restored_true_only() { let _guard = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner()); unsafe { std::env::set_var("KIGI_SESSION_RESTORED", "true") }; let restored = StatusConfig::from_env().session_restored; unsafe { std::env::set_var("KIGI_SESSION_RESTORED", "1") }; let non_canonical = StatusConfig::from_env().session_restored; unsafe { std::env::remove_var("KIGI_SESSION_RESTORED") }; assert!(restored); assert!(!non_canonical); } #[test] fn from_env_reads_revive_script_configured_true_only() { let _guard = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner()); unsafe { std::env::set_var("KIGI_REVIVE_SCRIPT_CONFIGURED", "true") }; let configured = StatusConfig::from_env().revive_script_configured; unsafe { std::env::set_var("KIGI_REVIVE_SCRIPT_CONFIGURED", "1") }; let non_canonical = StatusConfig::from_env().revive_script_configured; unsafe { std::env::remove_var("KIGI_REVIVE_SCRIPT_CONFIGURED") }; assert!(configured); assert!(!non_canonical); } #[test] fn from_env_reads_idle_ignore_background_true() { let _guard = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner()); unsafe { std::env::set_var("KIGI_WORKSPACE_IDLE_IGNORE_BACKGROUND_TASKS", "true") }; let cfg = StatusConfig::from_env(); unsafe { std::env::remove_var("KIGI_WORKSPACE_IDLE_IGNORE_BACKGROUND_TASKS") }; assert!(cfg.idle_ignores_background); } #[test] fn from_env_reads_preview_activity_window() { let _guard = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner()); unsafe { std::env::set_var("KIGI_WORKSPACE_PREVIEW_ACTIVITY_WINDOW_MS", "120000") }; let cfg = StatusConfig::from_env(); unsafe { std::env::remove_var("KIGI_WORKSPACE_PREVIEW_ACTIVITY_WINDOW_MS") }; assert_eq!(cfg.preview_activity_window, Duration::from_millis(120_000)); } #[test] fn from_env_reads_preview_activity_scrape_interval() { let _guard = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner()); unsafe { std::env::set_var("KIGI_WORKSPACE_PREVIEW_ACTIVITY_SCRAPE_INTERVAL_MS", "5000") }; let cfg = StatusConfig::from_env(); unsafe { std::env::remove_var("KIGI_WORKSPACE_PREVIEW_ACTIVITY_SCRAPE_INTERVAL_MS") }; assert_eq!( cfg.preview_activity_scrape_interval, Duration::from_millis(5_000) ); } /// `validate` must not panic regardless of the relative ordering of /// `keepalive` and `heartbeat`. #[test] fn validate_does_not_panic_when_keepalive_le_heartbeat() { let mut cfg = StatusConfig { keepalive: Duration::from_secs(10), heartbeat: Duration::from_secs(30), ..StatusConfig::default() }; cfg.validate(); } #[test] fn validate_clamps_preview_scrape_into_valid_range() { for (window_ms, scrape_ms, exp_window_ms, exp_scrape_ms) in [ (1_000u64, 4_000u64, 1_000u64, 999u64), (1_000, 1_000, 1_000, 999), (1_000, 200, 1_000, 200), (60_000, 0, 60_000, 1), (0, 0, 2, 1), (1, 5, 2, 1), ] { let mut cfg = StatusConfig { preview_activity_window: Duration::from_millis(window_ms), preview_activity_scrape_interval: Duration::from_millis(scrape_ms), ..StatusConfig::default() }; cfg.validate(); assert_eq!( cfg.preview_activity_window, Duration::from_millis(exp_window_ms) ); assert_eq!( cfg.preview_activity_scrape_interval, Duration::from_millis(exp_scrape_ms) ); assert!(cfg.preview_activity_scrape_interval >= Duration::from_millis(1)); assert!(cfg.preview_activity_scrape_interval < cfg.preview_activity_window); } } /// A configured agent timeout of `0` seconds is invalid (it would make /// every gRPC call fail immediately), so `nonzero_secs_or` falls back to /// the supplied default instead of returning `Duration::ZERO`. #[test] fn nonzero_secs_or_zero_falls_back_to_default() { assert_eq!( nonzero_secs_or( "KIGI_WORKSPACE_AGENT_RPC_TIMEOUT_SECS", 0, Duration::from_secs(30) ), Duration::from_secs(30) ); assert_eq!( nonzero_secs_or( "KIGI_WORKSPACE_AGENT_CONNECT_TIMEOUT_SECS", 0, Duration::from_secs(5) ), Duration::from_secs(5) ); } /// A positive value is honored verbatim. #[test] fn nonzero_secs_or_positive_is_passed_through() { assert_eq!( nonzero_secs_or( "KIGI_WORKSPACE_AGENT_RPC_TIMEOUT_SECS", 12, Duration::from_secs(30) ), Duration::from_secs(12) ); } /// An unset backoff var leaves the schedule unconfigured (`None`), so the /// SDK keeps its built-in default. #[test] fn backoff_schedule_unset_returns_none() { let _guard = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner()); let var = "KIGI_WORKSPACE_TEST_BACKOFF_UNSET"; unsafe { std::env::remove_var(var) }; assert_eq!(backoff_schedule_from_env(var), None); } /// A valid comma-separated list parses into millisecond `Duration`s in /// order, tolerating surrounding whitespace. #[test] fn backoff_schedule_valid_list_parses_in_order() { let _guard = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner()); let var = "KIGI_WORKSPACE_TEST_BACKOFF_VALID"; unsafe { std::env::set_var(var, "100, 200,500,1000") }; assert_eq!( backoff_schedule_from_env(var), Some(vec![ Duration::from_millis(100), Duration::from_millis(200), Duration::from_millis(500), Duration::from_millis(1000), ]) ); unsafe { std::env::remove_var(var) }; } /// A malformed element makes the whole schedule fall back to `None` (and /// warns) rather than silently dropping entries. #[test] fn backoff_schedule_malformed_returns_none() { let _guard = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner()); let var = "KIGI_WORKSPACE_TEST_BACKOFF_MALFORMED"; unsafe { std::env::set_var(var, "100,not-a-number,500") }; assert_eq!(backoff_schedule_from_env(var), None); unsafe { std::env::remove_var(var) }; } }