zeroclaw 0.1.7

Zero overhead. Zero compromise. 100% Rust. The fastest, smallest AI assistant.
Documentation
//! TG2: Config Load/Save Round-Trip Tests
//!
//! Prevents: Pattern 2 — Config persistence & workspace discovery bugs (13% of user bugs).
//! Issues: #547, #417, #621, #802
//!
//! Tests Config::load_or_init() with isolated temp directories, env var overrides,
//! and config file round-trips to verify workspace discovery and persistence.

use std::fs;
use zeroclaw::config::{AgentConfig, Config, MemoryConfig};

// ─────────────────────────────────────────────────────────────────────────────
// Config default construction
// ─────────────────────────────────────────────────────────────────────────────

#[test]
fn config_default_has_expected_provider() {
    let config = Config::default();
    assert!(
        config.default_provider.is_some(),
        "default config should have a default_provider"
    );
}

#[test]
fn config_default_has_expected_model() {
    let config = Config::default();
    assert!(
        config.default_model.is_some(),
        "default config should have a default_model"
    );
}

#[test]
fn config_default_temperature_positive() {
    let config = Config::default();
    assert!(
        config.default_temperature > 0.0,
        "default temperature should be positive"
    );
}

// ─────────────────────────────────────────────────────────────────────────────
// AgentConfig defaults
// ─────────────────────────────────────────────────────────────────────────────

#[test]
fn agent_config_default_max_tool_iterations() {
    let agent = AgentConfig::default();
    assert_eq!(
        agent.max_tool_iterations, 10,
        "default max_tool_iterations should be 10"
    );
}

#[test]
fn agent_config_default_max_history_messages() {
    let agent = AgentConfig::default();
    assert_eq!(
        agent.max_history_messages, 50,
        "default max_history_messages should be 50"
    );
}

#[test]
fn agent_config_default_tool_dispatcher() {
    let agent = AgentConfig::default();
    assert_eq!(
        agent.tool_dispatcher, "auto",
        "default tool_dispatcher should be 'auto'"
    );
}

#[test]
fn agent_config_default_compact_context_off() {
    let agent = AgentConfig::default();
    assert!(
        !agent.compact_context,
        "compact_context should default to false"
    );
}

// ─────────────────────────────────────────────────────────────────────────────
// MemoryConfig defaults
// ─────────────────────────────────────────────────────────────────────────────

#[test]
fn memory_config_default_backend() {
    let memory = MemoryConfig::default();
    assert!(
        !memory.backend.is_empty(),
        "memory backend should have a default value"
    );
}

#[test]
fn memory_config_default_embedding_provider() {
    let memory = MemoryConfig::default();
    // Default embedding_provider should be set (even if "none")
    assert!(
        !memory.embedding_provider.is_empty(),
        "embedding_provider should have a default value"
    );
}

#[test]
fn memory_config_default_vector_keyword_weights_sum_to_one() {
    let memory = MemoryConfig::default();
    let sum = memory.vector_weight + memory.keyword_weight;
    assert!(
        (sum - 1.0).abs() < 0.01,
        "vector_weight + keyword_weight should sum to ~1.0, got {sum}"
    );
}

// ─────────────────────────────────────────────────────────────────────────────
// Config TOML serialization round-trip
// ─────────────────────────────────────────────────────────────────────────────

#[test]
fn config_toml_roundtrip_preserves_provider() {
    let mut config = Config::default();
    config.default_provider = Some("deepseek".into());
    config.default_model = Some("deepseek-chat".into());
    config.default_temperature = 0.5;

    let toml_str = toml::to_string(&config).expect("config should serialize to TOML");
    let parsed: Config = toml::from_str(&toml_str).expect("TOML should deserialize back");

    assert_eq!(parsed.default_provider.as_deref(), Some("deepseek"));
    assert_eq!(parsed.default_model.as_deref(), Some("deepseek-chat"));
    assert!((parsed.default_temperature - 0.5).abs() < f64::EPSILON);
}

#[test]
fn config_toml_roundtrip_preserves_agent_config() {
    let mut config = Config::default();
    config.agent.max_tool_iterations = 5;
    config.agent.max_history_messages = 25;
    config.agent.compact_context = true;

    let toml_str = toml::to_string(&config).expect("config should serialize to TOML");
    let parsed: Config = toml::from_str(&toml_str).expect("TOML should deserialize back");

    assert_eq!(parsed.agent.max_tool_iterations, 5);
    assert_eq!(parsed.agent.max_history_messages, 25);
    assert!(parsed.agent.compact_context);
}

#[test]
fn config_toml_roundtrip_preserves_memory_config() {
    let mut config = Config::default();
    config.memory.embedding_provider = "openai".into();
    config.memory.embedding_model = "text-embedding-3-small".into();
    config.memory.vector_weight = 0.8;
    config.memory.keyword_weight = 0.2;

    let toml_str = toml::to_string(&config).expect("config should serialize to TOML");
    let parsed: Config = toml::from_str(&toml_str).expect("TOML should deserialize back");

    assert_eq!(parsed.memory.embedding_provider, "openai");
    assert_eq!(parsed.memory.embedding_model, "text-embedding-3-small");
    assert!((parsed.memory.vector_weight - 0.8).abs() < f64::EPSILON);
    assert!((parsed.memory.keyword_weight - 0.2).abs() < f64::EPSILON);
}

// ─────────────────────────────────────────────────────────────────────────────
// Config file write/read round-trip with tempdir
// ─────────────────────────────────────────────────────────────────────────────

#[test]
fn config_file_write_read_roundtrip() {
    let tmp = tempfile::TempDir::new().expect("tempdir creation should succeed");
    let config_path = tmp.path().join("config.toml");

    let mut config = Config::default();
    config.default_provider = Some("mistral".into());
    config.default_model = Some("mistral-large".into());
    config.agent.max_tool_iterations = 15;

    let toml_str = toml::to_string(&config).expect("config should serialize");
    fs::write(&config_path, &toml_str).expect("config file write should succeed");

    let read_back = fs::read_to_string(&config_path).expect("config file read should succeed");
    let parsed: Config = toml::from_str(&read_back).expect("TOML should parse back");

    assert_eq!(parsed.default_provider.as_deref(), Some("mistral"));
    assert_eq!(parsed.default_model.as_deref(), Some("mistral-large"));
    assert_eq!(parsed.agent.max_tool_iterations, 15);
}

#[test]
fn config_file_with_missing_optional_fields_uses_defaults() {
    // Simulate a minimal config TOML that omits optional sections
    let minimal_toml = r#"
default_temperature = 0.7
"#;
    let parsed: Config = toml::from_str(minimal_toml).expect("minimal TOML should parse");

    // Agent config should use defaults
    assert_eq!(parsed.agent.max_tool_iterations, 10);
    assert_eq!(parsed.agent.max_history_messages, 50);
    assert!(!parsed.agent.compact_context);
}

#[test]
fn config_file_with_custom_agent_section() {
    let toml_with_agent = r#"
default_temperature = 0.7

[agent]
max_tool_iterations = 3
compact_context = true
"#;
    let parsed: Config =
        toml::from_str(toml_with_agent).expect("TOML with agent section should parse");

    assert_eq!(parsed.agent.max_tool_iterations, 3);
    assert!(parsed.agent.compact_context);
    // max_history_messages should still use default
    assert_eq!(parsed.agent.max_history_messages, 50);
}

// ─────────────────────────────────────────────────────────────────────────────
// Workspace directory creation
// ─────────────────────────────────────────────────────────────────────────────

#[test]
fn workspace_dir_creation_in_tempdir() {
    let tmp = tempfile::TempDir::new().expect("tempdir creation should succeed");
    let workspace_dir = tmp.path().join("workspace");

    fs::create_dir_all(&workspace_dir).expect("workspace dir creation should succeed");
    assert!(workspace_dir.exists(), "workspace dir should exist");
    assert!(
        workspace_dir.is_dir(),
        "workspace path should be a directory"
    );
}

#[test]
fn nested_workspace_dir_creation() {
    let tmp = tempfile::TempDir::new().expect("tempdir creation should succeed");
    let nested_dir = tmp.path().join("deep").join("nested").join("workspace");

    fs::create_dir_all(&nested_dir).expect("nested dir creation should succeed");
    assert!(nested_dir.exists(), "nested workspace dir should exist");
}