use super::*;
use tempfile::TempDir;
use crate::paths::AidHomeGuard;
fn isolated() -> (TempDir, AidHomeGuard) {
let temp = tempfile::tempdir().unwrap();
let guard = AidHomeGuard::set(temp.path());
clear_feed_for_tests();
(temp, guard)
}
#[test]
fn kilo_and_free_models_zero_cost() {
let _guard = isolated();
assert_eq!(
estimate_cost(
100_000,
Some("opencode/mimo-v2-flash-free"),
AgentKind::OpenCode
),
Some(0.0)
);
assert_eq!(estimate_cost(100_000, None, AgentKind::Kilo), Some(0.0));
assert_eq!(estimate_cost(100_000, None, AgentKind::MiMoCode), Some(0.0));
assert_eq!(
estimate_cost(100_000, Some("kilo/kilo/auto-free"), AgentKind::Kilo),
Some(0.0)
);
assert_eq!(
estimate_cost(100_000, Some("mimo/mimo-auto"), AgentKind::MiMoCode),
Some(0.0)
);
}
#[test]
fn subscription_agents_stay_included_not_unknown() {
let _guard = isolated();
assert_eq!(
estimate_cost(100_000, Some("composer-2"), AgentKind::Cursor),
Some(0.0)
);
assert_eq!(
estimate_cost(100_000, Some("some-copilot-model"), AgentKind::Copilot),
Some(0.0)
);
}
#[test]
fn gpt41_cost_estimate() {
let _guard = isolated();
let cost = estimate_cost(1_000_000, Some("gpt-4.1"), AgentKind::Codex).unwrap();
assert!((cost - 3.8).abs() < 0.01);
}
#[test]
fn codex_fallback_uses_standard_tier_or_first_catalog_model() {
let _guard = isolated();
let cost = estimate_cost(1_000_000, None, AgentKind::Codex).unwrap();
let models = model_catalog::static_models_for_agent(&AgentKind::Codex);
let fallback = models
.iter()
.find(|m| m.tier == "standard")
.or_else(|| models.first())
.unwrap();
let blended = fallback.input_per_m * 0.7 + fallback.output_per_m * 0.3;
assert!((cost - blended).abs() < 0.01);
}
#[test]
fn composer2_cursor_subscription_is_included() {
let _guard = isolated();
let cost = estimate_cost(1_000_000, Some("composer-2"), AgentKind::Cursor).unwrap();
assert_eq!(cost, 0.0);
}
#[test]
fn unknown_model_returns_none() {
let _guard = isolated();
let cost = estimate_cost(1000, Some("unknown-model"), AgentKind::OpenCode);
assert!(cost.is_none());
}
#[test]
fn discovered_agy_model_does_not_inherit_similar_model_pricing() {
let _guard = isolated();
clear_feed_for_tests();
let cost = estimate_cost(
1_000_000,
Some("gemini-3.7-flash-high"),
AgentKind::Antigravity,
);
assert_eq!(cost, None);
assert_eq!(format_cost(cost), "unknown");
}
#[test]
fn discovered_opencode_model_does_not_inherit_similar_model_pricing() {
let _guard = isolated();
clear_feed_for_tests();
let cost = estimate_cost(
1_000_000,
Some("opencode-go/glm-5.2"),
AgentKind::OpenCode,
);
assert_eq!(cost, None);
assert_eq!(format_cost(cost), "unknown");
}
#[test]
fn commandcode_unknown_model_stays_unknown() {
let _guard = isolated();
assert_eq!(
estimate_cost(100_000, Some("nobody/has-this-model"), AgentKind::CommandCode),
None
);
}
#[test]
fn commandcode_reuses_known_vendor_pricing_without_zero_fallback() {
let _guard = isolated();
let cost = estimate_cost(1_000_000, Some("gpt-5.6-sol"), AgentKind::CommandCode).unwrap();
assert!((cost - 6.25).abs() < 0.01);
}
#[test]
fn format_cost_variants() {
assert_eq!(format_cost(Some(0.0)), "free");
assert_eq!(format_cost(Some(0.0038)), "$0.0038");
assert_eq!(format_cost(Some(1.23)), "$1.23");
assert_eq!(format_cost(None), "unknown");
}
#[test]
fn gpt56_matches_flagship_premium_rates() {
let _guard = isolated();
let sol = model_pricing("gpt-5.6-sol", AgentKind::Codex).unwrap();
assert_eq!(sol.input_per_m, 2.5);
assert_eq!(sol.output_per_m, 15.0);
let luna = model_pricing("gpt-5.6-luna", AgentKind::Codex).unwrap();
assert_eq!(luna.input_per_m, 0.4);
assert_eq!(luna.output_per_m, 1.6);
}
#[test]
fn format_cost_label_special_cases() {
assert_eq!(format_cost_label(Some(1.0), AgentKind::Cursor), "$1.00");
assert_eq!(format_cost_label(None, AgentKind::Cursor), "subscription");
assert_eq!(format_cost_label(None, AgentKind::Copilot), "subscription");
assert_eq!(format_cost_label(Some(0.0), AgentKind::Kilo), "included");
assert_eq!(format_cost_label(Some(0.0), AgentKind::MiMoCode), "included");
}
#[test]
fn gemini_estimate_fallback_without_explicit_model_matches_gemini_three_flash_blend() {
let _guard = isolated();
let blended =
estimate_cost(1_000_000, None, AgentKind::Gemini).expect("gemini default pricing present");
let expected = model_pricing("gemini-3-flash-preview", AgentKind::Gemini).unwrap();
let blended_per_m = expected.input_per_m * 0.7 + expected.output_per_m * 0.3;
assert!((blended - blended_per_m).abs() < 0.001);
}
#[test]
fn gemini_3_preview_model_pricing() {
let _guard = isolated();
let p = model_pricing("gemini-3.1-pro-preview", AgentKind::Gemini).unwrap();
assert_eq!(p.input_per_m, 1.25);
assert_eq!(p.output_per_m, 10.0);
let p = model_pricing("gemini-3-flash-preview", AgentKind::Gemini).unwrap();
assert_eq!(p.input_per_m, 0.30);
assert_eq!(p.output_per_m, 2.50);
let p = model_pricing("gemini-3-flash-lite-preview", AgentKind::Gemini).unwrap();
assert_eq!(p.input_per_m, 0.10);
assert_eq!(p.output_per_m, 0.40);
}
#[test]
fn new_model_pricing_entries() {
let _guard = isolated();
let pricing = model_pricing("claude-sonnet-4", AgentKind::Custom).unwrap();
assert_eq!(pricing.input_per_m, 3.0);
assert_eq!(pricing.output_per_m, 15.0);
let pricing = model_pricing("gpt-5", AgentKind::Codex).unwrap();
assert_eq!(pricing.input_per_m, 1.25);
assert_eq!(pricing.output_per_m, 10.0);
let pricing = model_pricing("gpt-4.1", AgentKind::Codex).unwrap();
assert_eq!(pricing.input_per_m, 2.0);
assert_eq!(pricing.output_per_m, 8.0);
let pricing = model_pricing("gpt-5.4", AgentKind::Codex).unwrap();
assert_eq!(pricing.input_per_m, 2.5);
assert_eq!(pricing.output_per_m, 15.0);
let pricing = model_pricing("gpt-5.4-mini", AgentKind::Codex).unwrap();
assert_eq!(pricing.input_per_m, 0.4);
assert_eq!(pricing.output_per_m, 1.6);
let pricing = model_pricing("gpt-5.5", AgentKind::Codex).unwrap();
assert_eq!(pricing.input_per_m, 2.5);
assert_eq!(pricing.output_per_m, 15.0);
let pricing = model_pricing("gpt-5.5-mini", AgentKind::Codex).unwrap();
assert_eq!(pricing.input_per_m, 0.4);
assert_eq!(pricing.output_per_m, 1.6);
let pricing = model_pricing("gpt-5-mini", AgentKind::Codex).unwrap();
assert_eq!(pricing.input_per_m, 0.25);
assert_eq!(pricing.output_per_m, 2.0);
let pricing = model_pricing("o3-mini", AgentKind::Custom).unwrap();
assert_eq!(pricing.input_per_m, 1.10);
assert_eq!(pricing.output_per_m, 4.40);
}
#[test]
fn unknown_model_yields_none_not_zero() {
let _guard = isolated();
let cost = estimate_cost(1000, Some("unknown-model"), AgentKind::OpenCode);
assert_eq!(cost, None);
assert_eq!(format_cost(cost), "unknown");
let cost = estimate_cost(1000, Some("nobody/has-this-model"), AgentKind::OpenCode);
assert_eq!(cost, None);
assert_eq!(format_cost(cost), "unknown");
}
#[test]
fn feed_precedence_and_builtin_fallback() {
let _guard = isolated();
use crate::cost::price_feed::{Feed, FeedModel};
set_feed_for_tests(Feed {
built_at: chrono::Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Secs, true),
age_seconds: Some(60),
stale: Some(false),
count: Some(1),
models: vec![FeedModel {
id: "gpt-5.9-mini".to_string(),
aliases: vec![],
input_per_mtok: 0.75,
output_per_mtok: 4.5,
cached_input_per_mtok: None,
context_length: None,
source: Some("openrouter".to_string()),
}],
});
let p = model_pricing("gpt-5.9-mini", AgentKind::Codex).unwrap();
assert_eq!(p.input_per_m, 0.75);
assert_eq!(p.output_per_m, 4.5);
let p = model_pricing("composer-2", AgentKind::Codex).unwrap();
assert_eq!(p.input_per_m, 0.50);
assert_eq!(p.output_per_m, 2.50);
clear_feed_for_tests();
}
#[test]
fn feed_reads_from_isolated_cache_file() {
let (temp, _guard) = isolated();
let feed = price_feed::Feed {
built_at: chrono::Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Secs, true),
age_seconds: Some(10),
stale: Some(false),
count: Some(1),
models: vec![price_feed::FeedModel {
id: "custom/feed-model-x".to_string(),
aliases: vec![],
input_per_mtok: 1.0,
output_per_mtok: 2.0,
cached_input_per_mtok: None,
context_length: None,
source: None,
}],
};
let json = serde_json::to_vec(&feed).unwrap();
std::fs::write(temp.path().join("prices.json"), json).unwrap();
clear_feed_for_tests();
let pricing = model_pricing("custom/feed-model-x", AgentKind::Codex).unwrap();
assert_eq!(pricing.input_per_m, 1.0);
assert_eq!(pricing.output_per_m, 2.0);
clear_feed_for_tests();
}