pictor-runtime 0.1.0

Inference runtime, sampling, tokenizer, and server for Pictor
Documentation
//! Tests for the layered configuration system.

use pictor_runtime::config::{
    ModelConfig, ObservabilityConfig, PictorConfig, SamplingConfig, ServerConfig,
};

// ═══════════════���════════════════��═════════════════════════════
// Default values
// ════��══════════════════════════════════════════════════��══════

#[test]
fn default_config_has_expected_values() {
    let cfg = PictorConfig::default();
    assert_eq!(cfg.server.host, "0.0.0.0");
    assert_eq!(cfg.server.port, 8080);
    assert!((cfg.sampling.temperature - 0.7).abs() < f32::EPSILON);
    assert_eq!(cfg.sampling.top_k, 40);
    assert!((cfg.sampling.top_p - 0.9).abs() < f32::EPSILON);
    assert!((cfg.sampling.repetition_penalty - 1.1).abs() < f32::EPSILON);
    assert_eq!(cfg.sampling.max_tokens, 512);
    assert!(cfg.model.model_path.is_none());
    assert!(cfg.model.tokenizer_path.is_none());
    assert_eq!(cfg.model.max_seq_len, 4096);
    assert_eq!(cfg.observability.log_level, "info");
    assert!(!cfg.observability.json_logs);
}

#[test]
fn server_config_defaults() {
    let cfg = ServerConfig::default();
    assert_eq!(cfg.host, "0.0.0.0");
    assert_eq!(cfg.port, 8080);
}

#[test]
fn sampling_config_defaults_match_params() {
    let cfg = SamplingConfig::default();
    assert!((cfg.temperature - 0.7).abs() < f32::EPSILON);
    assert_eq!(cfg.top_k, 40);
    assert!((cfg.top_p - 0.9).abs() < f32::EPSILON);
    assert!((cfg.repetition_penalty - 1.1).abs() < f32::EPSILON);
    assert_eq!(cfg.max_tokens, 512);
}

#[test]
fn model_config_defaults() {
    let cfg = ModelConfig::default();
    assert!(cfg.model_path.is_none());
    assert!(cfg.tokenizer_path.is_none());
    assert_eq!(cfg.max_seq_len, 4096);
}

#[test]
fn observability_config_default_log_level_is_info() {
    let cfg = ObservabilityConfig::default();
    assert_eq!(cfg.log_level, "info");
    assert!(!cfg.json_logs);
}

// ══════════════════════════════════════════════════════════════
// TOML parsing
// ═══════════��══════════════════════════════════════════════════

#[test]
fn toml_with_all_fields_parses_correctly() {
    let model_path = std::env::temp_dir().join("model.gguf");
    let tokenizer_path = std::env::temp_dir().join("tokenizer.json");
    let toml_str = format!(
        r#"
[server]
host = "127.0.0.1"
port = 3000

[sampling]
temperature = 0.5
top_k = 50
top_p = 0.95
repetition_penalty = 1.2
max_tokens = 1024

[model]
model_path = "{}"
tokenizer_path = "{}"
max_seq_len = 8192

[observability]
log_level = "debug"
json_logs = true
"#,
        model_path.display(),
        tokenizer_path.display()
    );
    let cfg: PictorConfig = toml::from_str(&toml_str).expect("should parse complete TOML");
    assert_eq!(cfg.server.host, "127.0.0.1");
    assert_eq!(cfg.server.port, 3000);
    assert!((cfg.sampling.temperature - 0.5).abs() < f32::EPSILON);
    assert_eq!(cfg.sampling.top_k, 50);
    assert!((cfg.sampling.top_p - 0.95).abs() < f32::EPSILON);
    assert!((cfg.sampling.repetition_penalty - 1.2).abs() < f32::EPSILON);
    assert_eq!(cfg.sampling.max_tokens, 1024);
    assert_eq!(
        cfg.model.model_path.as_deref(),
        Some(model_path.to_str().expect("path is valid UTF-8"))
    );
    assert_eq!(
        cfg.model.tokenizer_path.as_deref(),
        Some(tokenizer_path.to_str().expect("path is valid UTF-8"))
    );
    assert_eq!(cfg.model.max_seq_len, 8192);
    assert_eq!(cfg.observability.log_level, "debug");
    assert!(cfg.observability.json_logs);
}

#[test]
fn toml_with_partial_fields_uses_defaults() {
    let toml_str = r#"
[server]
port = 9090
"#;
    let cfg: PictorConfig = toml::from_str(toml_str).expect("should parse partial TOML");
    assert_eq!(cfg.server.port, 9090);
    assert_eq!(cfg.server.host, "0.0.0.0"); // default
    assert!((cfg.sampling.temperature - 0.7).abs() < f32::EPSILON); // default
    assert_eq!(cfg.sampling.top_k, 40); // default
    assert_eq!(cfg.model.max_seq_len, 4096); // default
    assert_eq!(cfg.observability.log_level, "info"); // default
}

#[test]
fn toml_with_only_sampling_section() {
    let toml_str = r#"
[sampling]
temperature = 0.0
top_k = 1
"#;
    let cfg: PictorConfig = toml::from_str(toml_str).expect("should parse sampling-only TOML");
    assert!(cfg.sampling.temperature.abs() < f32::EPSILON);
    assert_eq!(cfg.sampling.top_k, 1);
    // Others remain default
    assert_eq!(cfg.server.port, 8080);
}

#[test]
fn invalid_toml_returns_error() {
    let bad_toml = "this is not valid [[[toml";
    let result: Result<PictorConfig, _> = toml::from_str(bad_toml);
    assert!(result.is_err(), "invalid TOML should fail to parse");
}

#[test]
fn empty_toml_string_parses_to_defaults() {
    let cfg: PictorConfig = toml::from_str("").expect("empty string should parse to defaults");
    assert_eq!(cfg.server.host, "0.0.0.0");
    assert_eq!(cfg.server.port, 8080);
    assert!((cfg.sampling.temperature - 0.7).abs() < f32::EPSILON);
    assert_eq!(cfg.observability.log_level, "info");
}

// ═��════════════════���═══════════════════════════════════════════
// File loading
// ════════════════════════��═════════════════════════════════════

#[test]
fn missing_file_path_returns_default_via_load_or_default() {
    let cfg = PictorConfig::load_or_default(None);
    assert_eq!(cfg.server.host, "0.0.0.0");
    assert_eq!(cfg.server.port, 8080);
}

#[test]
fn nonexistent_file_returns_default_via_load_or_default() {
    let path = std::env::temp_dir().join("nonexistent_pictor_test_99999.toml");
    let cfg = PictorConfig::load_or_default(Some(&path));
    assert_eq!(cfg.server.port, 8080);
}

#[test]
fn load_from_temp_file() {
    let dir = std::env::temp_dir();
    let path = dir.join("pictor_config_test_load.toml");
    std::fs::write(
        &path,
        r#"
[server]
host = "10.0.0.1"
port = 5555

[sampling]
temperature = 0.3
"#,
    )
    .expect("write temp config");

    let cfg = PictorConfig::load(&path).expect("should load temp config");
    assert_eq!(cfg.server.host, "10.0.0.1");
    assert_eq!(cfg.server.port, 5555);
    assert!((cfg.sampling.temperature - 0.3).abs() < f32::EPSILON);
    // Defaults for unspecified
    assert_eq!(cfg.sampling.top_k, 40);

    let _ = std::fs::remove_file(&path);
}

#[test]
fn load_invalid_file_returns_error() {
    let dir = std::env::temp_dir();
    let path = dir.join("pictor_config_test_invalid.toml");
    std::fs::write(&path, "not valid toml {{{").expect("write invalid config");

    let result = PictorConfig::load(&path);
    assert!(result.is_err(), "invalid file should return error");

    let _ = std::fs::remove_file(&path);
}

// ═════��════════════════════════════════════════════════════════
// Round-trip: serialize then deserialize
// ════════════════════��════════════════════════════════════��════

#[test]
fn config_roundtrip_serialize_deserialize() {
    let original = PictorConfig {
        server: ServerConfig {
            host: "192.168.1.1".to_string(),
            port: 7777,
        },
        sampling: SamplingConfig {
            temperature: 0.42,
            top_k: 30,
            top_p: 0.85,
            repetition_penalty: 1.3,
            max_tokens: 2048,
        },
        model: ModelConfig {
            model_path: Some("/path/to/model.gguf".to_string()),
            tokenizer_path: Some("/path/to/tokenizer.json".to_string()),
            max_seq_len: 16384,
        },
        observability: ObservabilityConfig {
            log_level: "trace".to_string(),
            json_logs: true,
        },
    };

    let toml_str = toml::to_string(&original).expect("serialize");
    let parsed: PictorConfig = toml::from_str(&toml_str).expect("deserialize");

    assert_eq!(parsed.server.host, original.server.host);
    assert_eq!(parsed.server.port, original.server.port);
    assert!((parsed.sampling.temperature - original.sampling.temperature).abs() < f32::EPSILON);
    assert_eq!(parsed.sampling.top_k, original.sampling.top_k);
    assert!((parsed.sampling.top_p - original.sampling.top_p).abs() < f32::EPSILON);
    assert!(
        (parsed.sampling.repetition_penalty - original.sampling.repetition_penalty).abs()
            < f32::EPSILON
    );
    assert_eq!(parsed.sampling.max_tokens, original.sampling.max_tokens);
    assert_eq!(parsed.model.model_path, original.model.model_path);
    assert_eq!(parsed.model.tokenizer_path, original.model.tokenizer_path);
    assert_eq!(parsed.model.max_seq_len, original.model.max_seq_len);
    assert_eq!(
        parsed.observability.log_level,
        original.observability.log_level
    );
    assert_eq!(
        parsed.observability.json_logs,
        original.observability.json_logs
    );
}