malvin 0.2.4

Non-interactive research and coding agent
use super::super::RunTiming;
use crate::malvin_config_file::TokenCostRates;

#[test]
#[allow(clippy::float_cmp)]
fn codex_additive_reasoning_folds_into_tokens_out_and_cost() {
    let mut r = RunTiming {
        token_cost_rates: TokenCostRates {
            usd_per_microtoken_in: 0.0,
            usd_per_microtoken_out: 1000.0,
            usd_per_microtoken_cache_read: 0.0,
            usd_per_microtoken_cache_write: 0.0,
        },
        ..Default::default()
    };
    r.record_acp_usage_if_present(&serde_json::json!({
        "inputTokens": 4,
        "outputTokens": 2,
        "cacheReadTokens": 1,
        "cacheWriteTokens": 0,
        "reasoningTokens": 3,
        "totalTokens": 10
    }));
    assert_eq!(r.tokens_out, Some(5));
    assert_eq!(r.reasoning_tokens, Some(3));
    let stats = super::super::cost::cost_stats(&r).expect("stats");
    assert!((stats["cost_out"].as_f64().unwrap() - 0.005).abs() < 1e-12);
}

#[test]
fn cursor_subset_reasoning_does_not_inflate_tokens_out() {
    let mut r = RunTiming::default();
    r.record_acp_usage_if_present(&serde_json::json!({
        "inputTokens": 100,
        "outputTokens": 50,
        "cacheReadTokens": 0,
        "cacheWriteTokens": 0,
        "reasoningTokens": 10,
        "totalTokens": 150
    }));
    assert_eq!(r.tokens_out, Some(50));
    assert_eq!(r.reasoning_tokens, Some(10));
}

#[test]
#[allow(clippy::float_cmp)]
fn reported_cost_usd_skips_rate_estimate() {
    let mut r = RunTiming {
        token_cost_rates: TokenCostRates {
            usd_per_microtoken_in: 1_000_000.0,
            usd_per_microtoken_out: 1_000_000.0,
            usd_per_microtoken_cache_read: 0.0,
            usd_per_microtoken_cache_write: 0.0,
        },
        ..Default::default()
    };
    r.record_acp_usage_if_present(&serde_json::json!({
        "inputTokens": 10,
        "outputTokens": 2,
        "costUsd": {
            "input": 0.01,
            "output": 0.02,
            "cacheRead": 0.0,
            "cacheWrite": 0.0,
            "total": 0.03
        }
    }));
    assert_eq!(r.tx_costs, vec![0.03]);
    let stats = super::super::cost::cost_stats(&r).expect("stats");
    assert_eq!(stats["source"], "reported");
    assert!((stats["cost_tot"].as_f64().unwrap() - 0.03).abs() < 1e-12);
}

#[test]
#[allow(clippy::float_cmp)]
fn mixed_reported_and_estimated_turns_merge_cost_components() {
    let mut r = RunTiming {
        token_cost_rates: TokenCostRates {
            usd_per_microtoken_in: 1_000_000.0,
            usd_per_microtoken_out: 0.0,
            usd_per_microtoken_cache_read: 0.0,
            usd_per_microtoken_cache_write: 0.0,
        },
        ..Default::default()
    };
    r.record_acp_usage_if_present(&serde_json::json!({
        "inputTokens": 10,
        "outputTokens": 0,
        "costUsd": { "input": 0.01, "output": 0.0, "total": 0.01 }
    }));
    r.record_acp_usage_if_present(&serde_json::json!({
        "inputTokens": 10,
        "outputTokens": 0
    }));
    assert_eq!(r.tx_costs, vec![0.01, 10.0]);
    let stats = super::super::cost::cost_stats(&r).expect("stats");
    assert_eq!(stats["source"], "mixed");
    assert!((stats["cost_in"].as_f64().unwrap() - 10.01).abs() < 1e-12);
    assert!((stats["cost_tot"].as_f64().unwrap() - 10.01).abs() < 1e-12);
}