vipune 0.20.1

A minimal memory layer for AI agents
Documentation
//! Environment variable overrides for configuration.

use crate::config::Config;
use crate::errors::Error;

#[cfg(test)]
use crate::config::tests_utils::ConfigEnvGuard;
#[cfg(test)]
use std::path::PathBuf;

use super::env_parser;

/// Apply environment variable overrides to configuration.
pub fn apply_env_overrides(config: &mut Config) -> Result<(), Error> {
    env_parser::apply_database_path_override(&mut config.database_path)?;
    env_parser::apply_embedding_model_override(&mut config.embedding_model)?;
    env_parser::apply_similarity_threshold_override(&mut config.similarity_threshold)?;
    env_parser::apply_recency_weight_override(&mut config.recency_weight)?;
    env_parser::apply_hybrid_override(&mut config.hybrid)?;
    env_parser::apply_decay_refresh_days_override(&mut config.decay_refresh_days)?;
    env_parser::apply_promotion_threshold_override(&mut config.promotion_threshold)?;
    env_parser::apply_prune_retrieval_limit_override(&mut config.prune_retrieval_limit)?;
    env_parser::apply_prune_min_age_days_override(&mut config.prune_min_age_days)?;
    Ok(())
}

#[cfg(test)]
mod tests {
    use super::*;

    // Every test in this module writes knobs that `Config::load()` re-reads, so
    // each takes the one env mutex this target shares via `ConfigEnvGuard` and
    // holds it for the whole body (not a file-local mutex). The guard's `Drop`
    // clears every knob in `CONFIG_ENV_VARS` on exit — a trailing `clean_env()`
    // is skipped by a panicking assertion, which is how one real failure here
    // used to cascade into unrelated `Config::load().unwrap()` panics in
    // `config::tests`. See `docs/testing.md`.

    #[test]
    fn test_env_var_overrides_config() {
        let _guard = ConfigEnvGuard::acquire();

        unsafe {
            std::env::set_var("VIPUNE_DATABASE_PATH", "/custom/path/db.db");
            std::env::set_var("VIPUNE_EMBEDDING_MODEL", "env/model");
            std::env::set_var("VIPUNE_SIMILARITY_THRESHOLD", "0.95");
        }

        let mut config = Config {
            database_path: PathBuf::from("/default"),
            embedding_model: "default/model".to_string(),
            similarity_threshold: 0.85,
            recency_weight: 0.3,
            hybrid: false,
            decay_refresh_days: 30.0,
            promotion_threshold: 5,
            prune_retrieval_limit: 5,
            prune_min_age_days: 30.0,
        };

        apply_env_overrides(&mut config).unwrap();

        assert_eq!(config.database_path, PathBuf::from("/custom/path/db.db"));
        assert_eq!(config.embedding_model, "env/model");
        assert_eq!(config.similarity_threshold, 0.95);
    }

    #[test]
    fn test_invalid_similarity_threshold() {
        let _guard = ConfigEnvGuard::acquire();

        unsafe {
            std::env::set_var("VIPUNE_SIMILARITY_THRESHOLD", "invalid");
        }

        let mut config = Config {
            database_path: PathBuf::from("/default"),
            embedding_model: "default/model".to_string(),
            similarity_threshold: 0.85,
            recency_weight: 0.3,
            hybrid: false,
            decay_refresh_days: 30.0,
            promotion_threshold: 5,
            prune_retrieval_limit: 5,
            prune_min_age_days: 30.0,
        };

        let result = apply_env_overrides(&mut config);

        assert!(matches!(result, Err(Error::Config(_))));
    }

    #[test]
    fn test_empty_env_var_rejected() {
        let _guard = ConfigEnvGuard::acquire();

        unsafe {
            std::env::set_var("VIPUNE_DATABASE_PATH", "");
        }

        let mut config = Config {
            database_path: PathBuf::from("/default"),
            embedding_model: "default/model".to_string(),
            similarity_threshold: 0.85,
            recency_weight: 0.3,
            hybrid: false,
            decay_refresh_days: 30.0,
            promotion_threshold: 5,
            prune_retrieval_limit: 5,
            prune_min_age_days: 30.0,
        };

        let result = apply_env_overrides(&mut config);

        assert!(matches!(result, Err(Error::Config(_))));
    }

    #[test]
    fn test_whitespace_env_var_rejected() {
        let _guard = ConfigEnvGuard::acquire();

        unsafe {
            std::env::set_var("VIPUNE_EMBEDDING_MODEL", "   ");
        }

        let mut config = Config {
            database_path: PathBuf::from("/default"),
            embedding_model: "default/model".to_string(),
            similarity_threshold: 0.85,
            recency_weight: 0.3,
            hybrid: false,
            decay_refresh_days: 30.0,
            promotion_threshold: 5,
            prune_retrieval_limit: 5,
            prune_min_age_days: 30.0,
        };

        let result = apply_env_overrides(&mut config);

        assert!(matches!(result, Err(Error::Config(_))));
    }

    #[test]
    fn test_recency_weight_env_var_override() {
        let _guard = ConfigEnvGuard::acquire();

        unsafe {
            std::env::set_var("VIPUNE_RECENCY_WEIGHT", "0.5");
        }

        let mut config = Config {
            database_path: PathBuf::from("/default"),
            embedding_model: "default/model".to_string(),
            similarity_threshold: 0.85,
            recency_weight: 0.3,
            hybrid: false,
            decay_refresh_days: 30.0,
            promotion_threshold: 5,
            prune_retrieval_limit: 5,
            prune_min_age_days: 30.0,
        };

        apply_env_overrides(&mut config).unwrap();

        assert_eq!(config.recency_weight, 0.5);
    }

    #[test]
    fn test_hybrid_env_var_override() {
        let _guard = ConfigEnvGuard::acquire();

        unsafe {
            std::env::set_var("VIPUNE_HYBRID", "true");
        }

        let mut config = Config {
            database_path: PathBuf::from("/default"),
            embedding_model: "default/model".to_string(),
            similarity_threshold: 0.85,
            recency_weight: 0.3,
            hybrid: false,
            decay_refresh_days: 30.0,
            promotion_threshold: 5,
            prune_retrieval_limit: 5,
            prune_min_age_days: 30.0,
        };

        apply_env_overrides(&mut config).unwrap();

        assert!(config.hybrid);
    }

    #[test]
    fn test_hybrid_env_var_override_false() {
        let _guard = ConfigEnvGuard::acquire();

        unsafe {
            std::env::set_var("VIPUNE_HYBRID", "false");
        }

        let mut config = Config {
            database_path: PathBuf::from("/default"),
            embedding_model: "default/model".to_string(),
            similarity_threshold: 0.85,
            recency_weight: 0.3,
            hybrid: true,
            decay_refresh_days: 30.0,
            promotion_threshold: 5,
            prune_retrieval_limit: 5,
            prune_min_age_days: 30.0,
        };

        apply_env_overrides(&mut config).unwrap();

        assert!(!config.hybrid);
    }

    #[test]
    fn test_invalid_recency_weight_format() {
        let _guard = ConfigEnvGuard::acquire();

        unsafe {
            std::env::set_var("VIPUNE_RECENCY_WEIGHT", "invalid");
        }

        let mut config = Config {
            database_path: PathBuf::from("/default"),
            embedding_model: "default/model".to_string(),
            similarity_threshold: 0.85,
            recency_weight: 0.3,
            hybrid: false,
            decay_refresh_days: 30.0,
            promotion_threshold: 5,
            prune_retrieval_limit: 5,
            prune_min_age_days: 30.0,
        };

        let result = apply_env_overrides(&mut config);

        assert!(matches!(result, Err(Error::Config(_))));
    }

    #[test]
    fn test_lifecycle_knob_env_overrides() {
        let _guard = ConfigEnvGuard::acquire();

        unsafe {
            std::env::set_var("VIPUNE_DECAY_REFRESH_DAYS", "14.0");
            std::env::set_var("VIPUNE_PROMOTION_THRESHOLD", "10");
            std::env::set_var("VIPUNE_PRUNE_RETRIEVAL_LIMIT", "3");
            std::env::set_var("VIPUNE_PRUNE_MIN_AGE_DAYS", "7");
        }

        let mut config = Config {
            database_path: PathBuf::from("/default"),
            embedding_model: "default/model".to_string(),
            similarity_threshold: 0.85,
            recency_weight: 0.3,
            hybrid: false,
            decay_refresh_days: 30.0,
            promotion_threshold: 5,
            prune_retrieval_limit: 5,
            prune_min_age_days: 30.0,
        };

        apply_env_overrides(&mut config).unwrap();

        assert_eq!(config.decay_refresh_days, 14.0);
        assert_eq!(config.promotion_threshold, 10);
        assert_eq!(config.prune_retrieval_limit, 3);
        assert_eq!(config.prune_min_age_days, 7.0);
    }

    #[test]
    fn test_invalid_lifecycle_knob_values() {
        let _guard = ConfigEnvGuard::acquire();

        unsafe {
            std::env::set_var("VIPUNE_PROMOTION_THRESHOLD", "-1");
            std::env::set_var("VIPUNE_PRUNE_MIN_AGE_DAYS", "invalid");
        }

        let mut config = Config {
            database_path: PathBuf::from("/default"),
            embedding_model: "default/model".to_string(),
            similarity_threshold: 0.85,
            recency_weight: 0.3,
            hybrid: false,
            decay_refresh_days: 30.0,
            promotion_threshold: 5,
            prune_retrieval_limit: 5,
            prune_min_age_days: 30.0,
        };

        let result = apply_env_overrides(&mut config);

        assert!(matches!(result, Err(Error::Config(_))));
    }
}