use super::{CostEntry, CostFilter, CostSummary, CostTracker};
use crate::cli::cost::tracker::{
AdvancedCostTracker, Budget, BudgetScope, BudgetStatus, CostAlert,
};
use crate::cli::session::SessionId;
use chrono::{DateTime, Duration, Utc};
use proptest::prelude::*;
use proptest::strategy::Just;
use std::collections::HashMap;
use std::path::PathBuf;
use tempfile::tempdir;
use uuid::Uuid;
pub struct CostTestFixture {
pub session_id: SessionId,
pub entries: Vec<CostEntry>,
pub temp_dir: tempfile::TempDir,
pub storage_path: PathBuf,
}
impl CostTestFixture {
pub fn new() -> Self {
let temp_dir = tempdir().expect("Failed to create temp directory");
let storage_path = temp_dir.path().join("test_costs.json");
let session_id = Uuid::new_v4();
let mut entries = Vec::new();
entries.push(CostEntry::new(
session_id,
"analyze_code".to_string(),
0.050,
150,
300,
2500,
"claude-3-opus".to_string(),
));
entries.push(CostEntry::new(
session_id,
"generate_docs".to_string(),
0.025,
75,
150,
1800,
"claude-3-sonnet".to_string(),
));
entries.push(CostEntry::new(
session_id,
"review_pr".to_string(),
0.015,
50,
100,
1200,
"claude-3-haiku".to_string(),
));
entries.push(CostEntry::new(
session_id,
"analyze_code".to_string(), 0.040,
120,
240,
2000,
"claude-3-opus".to_string(),
));
Self {
session_id,
entries,
temp_dir,
storage_path,
}
}
pub fn with_large_dataset(entry_count: usize) -> Self {
let temp_dir = tempdir().expect("Failed to create temp directory");
let storage_path = temp_dir.path().join("large_test_costs.json");
let session_id = Uuid::new_v4();
let mut entries = Vec::new();
let commands = ["analyze", "generate", "review", "refactor", "test", "debug", "optimize"];
let models = ["claude-3-opus", "claude-3-sonnet", "claude-3-haiku"];
for i in 0..entry_count {
let command_idx = i % commands.len();
let model_idx = i % models.len();
entries.push(CostEntry::new(
session_id,
format!("{}_{}", commands[command_idx], i),
0.001 + (i as f64 * 0.001), 10 + (i as u32 * 5), 20 + (i as u32 * 10), 500 + (i as u64 * 100), models[model_idx].to_string(),
));
}
Self {
session_id,
entries,
temp_dir,
storage_path,
}
}
pub fn with_time_series_data(days: i64) -> Self {
let temp_dir = tempdir().expect("Failed to create temp directory");
let storage_path = temp_dir.path().join("timeseries_test_costs.json");
let session_id = Uuid::new_v4();
let mut entries = Vec::new();
let base_time = Utc::now() - Duration::days(days);
for day in 0..days {
for hour in [9, 14, 16] {
let mut entry = CostEntry::new(
session_id,
format!("daily_task_{}", day),
0.010 + (day as f64 * 0.005), 50 + (day as u32 * 2),
100 + (day as u32 * 4),
1000 + (day as u64 * 50),
"claude-3-opus".to_string(),
);
entry.timestamp = base_time + Duration::days(day) + Duration::hours(hour);
entries.push(entry);
}
}
Self {
session_id,
entries,
temp_dir,
storage_path,
}
}
pub async fn create_tracker(&self) -> CostTracker {
let mut tracker =
CostTracker::new(self.storage_path.clone()).expect("Failed to create test tracker");
for entry in &self.entries {
tracker
.record_cost(entry.clone())
.await
.expect("Failed to record test entry");
}
tracker
}
pub fn random_entry(session_id: SessionId) -> impl Strategy<Value = CostEntry> {
(
any::<String>().prop_filter("Non-empty command", |s| !s.is_empty()),
0.001f64..1.0f64, 1u32..10000u32, 1u32..20000u32, 100u64..60000u64, any::<String>().prop_filter("Non-empty model", |s| !s.is_empty()),
)
.prop_map(
move |(command, cost, input_tokens, output_tokens, duration, model)| {
CostEntry::new(
session_id,
command,
cost,
input_tokens,
output_tokens,
duration,
model,
)
},
)
}
pub fn random_filter() -> impl Strategy<Value = CostFilter> {
(
prop::option::of(Just(Uuid::new_v4())),
prop::option::of(any::<String>()),
prop::option::of(property_strategies::date_range().prop_map(|(start, _)| start)),
prop::option::of(property_strategies::date_range().prop_map(|(_, end)| end)),
prop::option::of(0.0f64..1000.0f64),
prop::option::of(0.0f64..1000.0f64),
prop::option::of(any::<String>()),
)
.prop_map(
|(session_id, command_pattern, since, until, min_cost, max_cost, model)| {
CostFilter {
session_id,
command_pattern,
since,
until,
min_cost,
max_cost,
model,
}
},
)
}
}
pub mod test_helpers {
use super::*;
pub fn minimal_cost_entry(session_id: SessionId) -> CostEntry {
CostEntry::new(
session_id,
"test_command".to_string(),
0.001,
1,
1,
100,
"test_model".to_string(),
)
}
pub fn maximal_cost_entry(session_id: SessionId) -> CostEntry {
CostEntry::new(
session_id,
"expensive_analysis_command_with_very_long_name".to_string(),
99.999,
100000,
200000,
3600000, "claude-3-opus-premium-ultra".to_string(),
)
}
pub fn edge_case_entries(session_id: SessionId) -> Vec<CostEntry> {
vec![
CostEntry::new(
session_id,
"free_command".to_string(),
0.0,
0,
0,
0,
"free_model".to_string(),
),
CostEntry::new(
session_id,
"tiny".to_string(),
0.00001,
1,
1,
1,
"tiny_model".to_string(),
),
CostEntry::new(
session_id,
"分析代码".to_string(),
0.01,
50,
100,
1000,
"claude-3-opus".to_string(),
),
CostEntry::new(
session_id,
"cmd-with_special.chars@test!".to_string(),
0.02,
60,
120,
1500,
"model-v2.1".to_string(),
),
]
}
pub fn concurrent_test_entries(session_id: SessionId, count: usize) -> Vec<CostEntry> {
(0..count)
.map(|i| {
CostEntry::new(
session_id,
format!("concurrent_cmd_{}", i),
0.01 * (i + 1) as f64,
10 * (i + 1) as u32,
20 * (i + 1) as u32,
100 * (i + 1) as u64,
format!("model_{}", i % 3),
)
})
.collect()
}
pub fn multi_session_entries() -> (Vec<SessionId>, Vec<CostEntry>) {
let sessions: Vec<SessionId> = (0..3).map(|_| Uuid::new_v4()).collect();
let mut entries = Vec::new();
for (i, &session_id) in sessions.iter().enumerate() {
for j in 0..5 {
entries.push(CostEntry::new(
session_id,
format!("session_{}_cmd_{}", i, j),
0.01 * ((i * 5 + j) + 1) as f64,
50 + (j * 10) as u32,
100 + (j * 20) as u32,
1000 + (j * 200) as u64,
"claude-3-opus".to_string(),
));
}
}
(sessions, entries)
}
pub fn time_distributed_entries(session_id: SessionId) -> Vec<CostEntry> {
let base_time = Utc::now();
let mut entries = Vec::new();
let time_offsets = [
Duration::days(-30), Duration::days(-7), Duration::days(-1), Duration::hours(-6), Duration::hours(-1), Duration::minutes(-10), ];
for (i, offset) in time_offsets.iter().enumerate() {
let mut entry = CostEntry::new(
session_id,
format!("time_cmd_{}", i),
0.01 * (i + 1) as f64,
50 + (i * 10) as u32,
100 + (i * 20) as u32,
1000 + (i * 200) as u64,
"claude-3-opus".to_string(),
);
entry.timestamp = base_time + *offset;
entries.push(entry);
}
entries
}
pub fn validate_cost_summary(entries: &[CostEntry], summary: &CostSummary) -> bool {
if entries.is_empty() {
return summary.total_cost == 0.0
&& summary.command_count == 0
&& summary.average_cost == 0.0
&& summary.total_tokens == 0;
}
let expected_total_cost: f64 = entries.iter().map(|e| e.cost_usd).sum();
let expected_command_count = entries.len();
let expected_average_cost = expected_total_cost / expected_command_count as f64;
let expected_total_tokens: u32 = entries
.iter()
.map(|e| e.input_tokens + e.output_tokens)
.sum();
let cost_match = (summary.total_cost - expected_total_cost).abs() < 0.00001;
let count_match = summary.command_count == expected_command_count;
let avg_match = (summary.average_cost - expected_average_cost).abs() < 0.00001;
let token_match = summary.total_tokens == expected_total_tokens;
cost_match && count_match && avg_match && token_match
}
pub fn benchmark_entries(count: usize) -> Vec<CostEntry> {
let session_id = Uuid::new_v4();
(0..count)
.map(|i| {
CostEntry::new(
session_id,
format!("benchmark_cmd_{:06}", i),
0.001 + (i as f64 / 1000000.0), 10 + (i % 100) as u32,
20 + (i % 200) as u32,
100 + (i % 5000) as u64,
match i % 3 {
0 => "claude-3-opus",
1 => "claude-3-sonnet",
_ => "claude-3-haiku",
}
.to_string(),
)
})
.collect()
}
}
pub mod property_strategies {
use super::*;
pub fn valid_cost() -> impl Strategy<Value = f64> {
(0.0..1000.0f64).prop_filter("Non-negative finite cost", |&cost| {
cost.is_finite() && cost >= 0.0
})
}
pub fn valid_tokens() -> impl Strategy<Value = u32> {
0u32..1000000u32
}
pub fn valid_duration() -> impl Strategy<Value = u64> {
1u64..3600000u64 }
pub fn command_name() -> impl Strategy<Value = String> {
prop::collection::vec("[a-z_]{1,20}", 1..4).prop_map(|parts| parts.join("_"))
}
pub fn model_name() -> impl Strategy<Value = String> {
prop_oneof![
Just("claude-3-opus".to_string()),
Just("claude-3-sonnet".to_string()),
Just("claude-3-haiku".to_string()),
"[a-z0-9-]{5,30}".prop_map(|s| s.to_string()),
]
}
pub fn date_range() -> impl Strategy<Value = (DateTime<Utc>, DateTime<Utc>)> {
(
-365i64..0i64, 0i64..365i64, )
.prop_map(|(start_days, end_days)| {
let base = Utc::now();
let start = base + Duration::days(start_days);
let end = base + Duration::days(end_days);
if start <= end {
(start, end)
} else {
(end, start)
}
})
}
}
#[cfg(test)]
mod test_infrastructure_tests {
use super::*;
#[test]
fn test_fixture_creation() {
let fixture = CostTestFixture::new();
assert!(!fixture.entries.is_empty());
assert!(!fixture.storage_path.exists()); assert_eq!(fixture.entries.len(), 4);
for entry in &fixture.entries {
assert_eq!(entry.session_id, fixture.session_id);
}
}
#[test]
fn test_large_dataset_fixture() {
let fixture = CostTestFixture::with_large_dataset(1000);
assert_eq!(fixture.entries.len(), 1000);
let mut unique_commands = std::collections::HashSet::new();
let mut unique_models = std::collections::HashSet::new();
for entry in &fixture.entries {
unique_commands.insert(&entry.command_name);
unique_models.insert(&entry.model);
}
assert!(unique_commands.len() > 1);
assert!(unique_models.len() > 1);
}
#[test]
fn test_time_series_fixture() {
let fixture = CostTestFixture::with_time_series_data(7);
assert_eq!(fixture.entries.len(), 7 * 3);
let mut timestamps: Vec<_> = fixture.entries.iter().map(|e| e.timestamp).collect();
timestamps.sort();
assert!(timestamps.first().unwrap() < timestamps.last().unwrap());
let costs: Vec<_> = fixture.entries.iter().map(|e| e.cost_usd).collect();
let first_day_cost = costs[0];
let last_day_cost = costs[costs.len() - 3]; assert!(last_day_cost > first_day_cost);
}
#[tokio::test]
async fn test_fixture_tracker_creation() {
let fixture = CostTestFixture::new();
let tracker = fixture.create_tracker().await;
let global_summary = tracker.get_global_summary().await.unwrap();
assert_eq!(global_summary.command_count, fixture.entries.len());
let expected_total: f64 = fixture.entries.iter().map(|e| e.cost_usd).sum();
assert!((global_summary.total_cost - expected_total).abs() < 0.00001);
}
#[test]
fn test_helper_functions() {
let session_id = Uuid::new_v4();
let minimal = test_helpers::minimal_cost_entry(session_id);
assert_eq!(minimal.session_id, session_id);
assert!(minimal.cost_usd > 0.0);
let maximal = test_helpers::maximal_cost_entry(session_id);
assert_eq!(maximal.session_id, session_id);
assert!(maximal.cost_usd > minimal.cost_usd);
let edge_cases = test_helpers::edge_case_entries(session_id);
assert!(!edge_cases.is_empty());
let concurrent = test_helpers::concurrent_test_entries(session_id, 10);
assert_eq!(concurrent.len(), 10);
let (sessions, entries) = test_helpers::multi_session_entries();
assert_eq!(sessions.len(), 3);
assert_eq!(entries.len(), 15);
let time_entries = test_helpers::time_distributed_entries(session_id);
assert_eq!(time_entries.len(), 6);
}
#[test]
fn test_summary_validation() {
let session_id = Uuid::new_v4();
let entries = vec![
CostEntry::new(
session_id,
"cmd1".to_string(),
0.10,
100,
200,
1000,
"model1".to_string(),
),
CostEntry::new(
session_id,
"cmd2".to_string(),
0.20,
150,
300,
1500,
"model2".to_string(),
),
];
let summary = CostSummary {
total_cost: 0.30,
command_count: 2,
average_cost: 0.15,
total_tokens: 750, date_range: (Utc::now(), Utc::now()),
by_command: HashMap::new(),
by_model: HashMap::new(),
};
assert!(test_helpers::validate_cost_summary(&entries, &summary));
let invalid_summary = CostSummary {
total_cost: 999.99, ..summary.clone()
};
assert!(!test_helpers::validate_cost_summary(
&entries,
&invalid_summary
));
}
#[test]
fn test_benchmark_entries() {
let entries = test_helpers::benchmark_entries(10000);
assert_eq!(entries.len(), 10000);
let start = std::time::Instant::now();
let _total_cost: f64 = entries.iter().map(|e| e.cost_usd).sum();
let duration = start.elapsed();
assert!(duration.as_millis() < 10);
}
proptest! {
#[test]
fn property_cost_entry_creation(
cost in property_strategies::valid_cost(),
input_tokens in property_strategies::valid_tokens(),
output_tokens in property_strategies::valid_tokens(),
duration in property_strategies::valid_duration(),
command in property_strategies::command_name(),
model in property_strategies::model_name(),
) {
let session_id = Uuid::new_v4();
let entry = CostEntry::new(
session_id,
command,
cost,
input_tokens,
output_tokens,
duration,
model.clone(),
);
prop_assert_eq!(entry.session_id, session_id);
prop_assert_eq!(entry.cost_usd, cost);
prop_assert_eq!(entry.input_tokens, input_tokens);
prop_assert_eq!(entry.output_tokens, output_tokens);
prop_assert_eq!(entry.duration_ms, duration);
prop_assert_eq!(entry.model, model);
prop_assert!(!entry.id.is_empty());
}
#[test]
fn property_cost_filter_behavior(
filter in CostTestFixture::random_filter(),
entry in CostTestFixture::random_entry(Uuid::new_v4()),
) {
let temp_dir = tempdir().unwrap();
let storage_path = temp_dir.path().join("prop_test.json");
let tracker = CostTracker::new(storage_path).unwrap();
let matches = tracker.matches_filter(&entry, &filter);
prop_assert!(matches == true || matches == false); }
#[test]
fn property_summary_calculation_consistency(
entries in prop::collection::vec(
CostTestFixture::random_entry(Uuid::new_v4()),
0..100
)
) {
if !entries.is_empty() {
let total_cost: f64 = entries.iter().map(|e| e.cost_usd).sum();
let command_count = entries.len();
let average_cost = total_cost / command_count as f64;
prop_assert!(total_cost >= 0.0);
prop_assert!(average_cost >= 0.0);
prop_assert!(command_count > 0);
if entries.iter().all(|e| e.cost_usd > 0.0) {
prop_assert!(average_cost > 0.0);
}
}
}
}
}
#[cfg(test)]
mod core_recording_tests {
use super::*;
use std::sync::Arc;
use tokio::sync::Mutex;
#[tokio::test]
async fn test_record_cost_valid_data() {
let fixture = CostTestFixture::new();
let mut tracker = CostTracker::new(fixture.storage_path.clone()).unwrap();
let session_id = Uuid::new_v4();
let entry = CostEntry::new(
session_id,
"test_command".to_string(),
0.025,
100,
200,
1500,
"claude-3-opus".to_string(),
);
let initial_count = tracker.entries.len();
tracker.record_cost(entry.clone()).await.unwrap();
assert_eq!(tracker.entries.len(), initial_count + 1);
let recorded_entry = &tracker.entries[tracker.entries.len() - 1];
assert_eq!(recorded_entry.session_id, entry.session_id);
assert_eq!(recorded_entry.command_name, entry.command_name);
assert_eq!(recorded_entry.cost_usd, entry.cost_usd);
assert_eq!(recorded_entry.input_tokens, entry.input_tokens);
assert_eq!(recorded_entry.output_tokens, entry.output_tokens);
assert_eq!(recorded_entry.duration_ms, entry.duration_ms);
assert_eq!(recorded_entry.model, entry.model);
}
#[tokio::test]
async fn test_record_cost_zero_values() {
let fixture = CostTestFixture::new();
let mut tracker = CostTracker::new(fixture.storage_path.clone()).unwrap();
let session_id = Uuid::new_v4();
let zero_cost_entry = CostEntry::new(
session_id,
"free_command".to_string(),
0.0, 0, 0, 0, "free_model".to_string(),
);
tracker.record_cost(zero_cost_entry.clone()).await.unwrap();
let summary = tracker.get_session_summary(session_id).await.unwrap();
assert_eq!(summary.total_cost, 0.0);
assert_eq!(summary.command_count, 1);
assert_eq!(summary.total_tokens, 0);
}
#[tokio::test]
async fn test_record_cost_negative_values() {
let fixture = CostTestFixture::new();
let mut tracker = CostTracker::new(fixture.storage_path.clone()).unwrap();
let session_id = Uuid::new_v4();
let negative_cost_entry = CostEntry::new(
session_id,
"refund_command".to_string(),
-0.05, 100,
200,
1000,
"claude-3-opus".to_string(),
);
tracker
.record_cost(negative_cost_entry.clone())
.await
.unwrap();
let summary = tracker.get_session_summary(session_id).await.unwrap();
assert_eq!(summary.total_cost, -0.05);
assert_eq!(summary.command_count, 1);
assert!(summary.average_cost < 0.0);
}
#[tokio::test]
async fn test_record_cost_different_types() {
let fixture = CostTestFixture::new();
let mut tracker = CostTracker::new(fixture.storage_path.clone()).unwrap();
let session_id = Uuid::new_v4();
let entries = vec![
CostEntry::new(
session_id,
"input_heavy".to_string(),
0.10,
1000,
100,
2000,
"claude-3-opus".to_string(),
),
CostEntry::new(
session_id,
"output_heavy".to_string(),
0.15,
100,
1000,
3000,
"claude-3-opus".to_string(),
),
CostEntry::new(
session_id,
"balanced".to_string(),
0.08,
500,
500,
1500,
"claude-3-sonnet".to_string(),
),
CostEntry::new(
session_id,
"quick_task".to_string(),
0.01,
50,
50,
200,
"claude-3-haiku".to_string(),
),
];
for entry in entries {
tracker.record_cost(entry).await.unwrap();
}
let summary = tracker.get_session_summary(session_id).await.unwrap();
assert_eq!(summary.command_count, 4);
assert_eq!(summary.by_model.len(), 3);
let total_input_tokens: u32 = tracker
.entries
.iter()
.filter(|e| e.session_id == session_id)
.map(|e| e.input_tokens)
.sum();
let total_output_tokens: u32 = tracker
.entries
.iter()
.filter(|e| e.session_id == session_id)
.map(|e| e.output_tokens)
.sum();
assert_eq!(total_input_tokens, 1650);
assert_eq!(total_output_tokens, 1650);
assert_eq!(summary.total_tokens, 3300);
}
#[tokio::test]
async fn test_concurrent_cost_recording() {
let fixture = CostTestFixture::new();
let tracker = Arc::new(Mutex::new(
CostTracker::new(fixture.storage_path.clone()).unwrap(),
));
let session_id = Uuid::new_v4();
let mut handles = vec![];
for i in 0..50 {
let tracker_clone = Arc::clone(&tracker);
let handle = tokio::spawn(async move {
let entry = CostEntry::new(
session_id,
format!("concurrent_cmd_{}", i),
0.01 * (i + 1) as f64,
10 + i as u32,
20 + i as u32,
100 + i as u64,
format!("model_{}", i % 3),
);
let mut tracker_lock = tracker_clone.lock().await;
tracker_lock.record_cost(entry).await.unwrap();
});
handles.push(handle);
}
for handle in handles {
handle.await.unwrap();
}
let tracker_lock = tracker.lock().await;
let summary = tracker_lock.get_session_summary(session_id).await.unwrap();
assert_eq!(summary.command_count, 50);
let expected_total: f64 = (1..=50).map(|i| 0.01 * i as f64).sum();
assert!((summary.total_cost - expected_total).abs() < 0.00001);
}
#[tokio::test]
async fn test_cost_recording_persistence() {
let fixture = CostTestFixture::new();
let session_id = Uuid::new_v4();
{
let mut tracker = CostTracker::new(fixture.storage_path.clone()).unwrap();
for i in 0..5 {
let entry = CostEntry::new(
session_id,
format!("persistent_cmd_{}", i),
0.01 * (i + 1) as f64,
50 + i as u32 * 10,
100 + i as u32 * 20,
1000 + i as u64 * 200,
"claude-3-opus".to_string(),
);
tracker.record_cost(entry).await.unwrap();
}
}
let tracker = CostTracker::new(fixture.storage_path.clone()).unwrap();
let summary = tracker.get_session_summary(session_id).await.unwrap();
assert_eq!(summary.command_count, 5);
let expected_total: f64 = (1..=5).map(|i| 0.01 * i as f64).sum();
assert!((summary.total_cost - expected_total).abs() < 0.00001);
}
#[tokio::test]
async fn test_record_cost_storage_failure_handling() {
let temp_dir = tempdir().unwrap();
let readonly_path = temp_dir.path().join("readonly");
std::fs::create_dir(&readonly_path).unwrap();
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let mut perms = std::fs::metadata(&readonly_path).unwrap().permissions();
perms.set_mode(0o444); std::fs::set_permissions(&readonly_path, perms).unwrap();
}
let storage_path = readonly_path.join("costs.json");
let mut tracker = CostTracker::new(storage_path).unwrap();
let session_id = Uuid::new_v4();
let entry = CostEntry::new(
session_id,
"test_command".to_string(),
0.025,
100,
200,
1500,
"claude-3-opus".to_string(),
);
#[cfg(unix)]
{
let result = tracker.record_cost(entry).await;
assert!(result.is_err());
}
#[cfg(not(unix))]
{
let _result = tracker.record_cost(entry).await;
}
}
#[tokio::test]
async fn test_record_cost_with_unicode_content() {
let fixture = CostTestFixture::new();
let mut tracker = CostTracker::new(fixture.storage_path.clone()).unwrap();
let session_id = Uuid::new_v4();
let entries = vec![
CostEntry::new(
session_id,
"分析代码".to_string(),
0.025,
100,
200,
1500,
"claude-3-opus".to_string(),
),
CostEntry::new(
session_id,
"анализ_кода".to_string(),
0.030,
150,
250,
2000,
"claude-3-sonnet".to_string(),
),
CostEntry::new(
session_id,
"コード解析".to_string(),
0.020,
80,
180,
1200,
"claude-3-haiku".to_string(),
),
CostEntry::new(
session_id,
"🔍analyze🚀".to_string(),
0.035,
200,
300,
2500,
"claude-3-opus".to_string(),
),
];
for entry in entries {
tracker.record_cost(entry).await.unwrap();
}
let summary = tracker.get_session_summary(session_id).await.unwrap();
assert_eq!(summary.command_count, 4);
let global_summary = tracker.get_global_summary().await.unwrap();
assert!(global_summary.by_command.contains_key("分析代码"));
assert!(global_summary.by_command.contains_key("анализ_кода"));
assert!(global_summary.by_command.contains_key("コード解析"));
assert!(global_summary.by_command.contains_key("🔍analyze🚀"));
}
#[tokio::test]
async fn test_record_cost_large_values() {
let fixture = CostTestFixture::new();
let mut tracker = CostTracker::new(fixture.storage_path.clone()).unwrap();
let session_id = Uuid::new_v4();
let large_entry = CostEntry::new(
session_id,
"extremely_expensive_analysis".to_string(),
999.999, 1000000, 2000000, 3600000, "claude-3-opus-premium".to_string(),
);
tracker.record_cost(large_entry).await.unwrap();
let summary = tracker.get_session_summary(session_id).await.unwrap();
assert_eq!(summary.command_count, 1);
assert!((summary.total_cost - 999.999).abs() < 0.00001);
assert_eq!(summary.total_tokens, 3000000);
assert!((summary.average_cost - 999.999).abs() < 0.00001);
}
#[tokio::test]
async fn test_record_cost_precision() {
let fixture = CostTestFixture::new();
let mut tracker = CostTracker::new(fixture.storage_path.clone()).unwrap();
let session_id = Uuid::new_v4();
let precise_entries = vec![0.00001, 0.00002, 0.00003, 0.00004, 0.00005];
for (i, cost) in precise_entries.iter().enumerate() {
let entry = CostEntry::new(
session_id,
format!("precise_cmd_{}", i),
*cost,
1,
1,
100,
"claude-3-haiku".to_string(),
);
tracker.record_cost(entry).await.unwrap();
}
let summary = tracker.get_session_summary(session_id).await.unwrap();
let expected_total: f64 = precise_entries.iter().sum();
assert!((summary.total_cost - expected_total).abs() < 0.0000001);
assert_eq!(summary.command_count, 5);
}
#[tokio::test]
async fn test_record_cost_non_deduplication() {
let fixture = CostTestFixture::new();
let mut tracker = CostTracker::new(fixture.storage_path.clone()).unwrap();
let session_id = Uuid::new_v4();
for _ in 0..3 {
let entry = CostEntry::new(
session_id,
"test_command".to_string(),
0.025,
100,
200,
1500,
"claude-3-opus".to_string(),
);
tracker.record_cost(entry).await.unwrap();
}
let summary = tracker.get_session_summary(session_id).await.unwrap();
assert_eq!(summary.command_count, 3);
assert!((summary.total_cost - 0.075).abs() < 0.00001);
let session_entries: Vec<_> = tracker
.entries
.iter()
.filter(|e| e.session_id == session_id)
.collect();
let entry_count = session_entries.len();
let mut ids: std::collections::HashSet<String> = std::collections::HashSet::new();
for entry in session_entries {
ids.insert(entry.id.clone());
}
assert_eq!(ids.len(), entry_count);
assert_eq!(ids.len(), 3);
}
proptest! {
#[test]
fn property_record_cost_maintains_invariants(
cost in property_strategies::valid_cost(),
input_tokens in property_strategies::valid_tokens(),
output_tokens in property_strategies::valid_tokens(),
duration in property_strategies::valid_duration(),
command in property_strategies::command_name(),
model in property_strategies::model_name(),
) {
tokio_test::block_on(async {
let fixture = CostTestFixture::new();
let mut tracker = CostTracker::new(fixture.storage_path.clone()).unwrap();
let session_id = Uuid::new_v4();
let initial_count = tracker.entries.len();
let entry = CostEntry::new(
session_id,
command,
cost,
input_tokens,
output_tokens,
duration,
model,
);
tracker.record_cost(entry.clone()).await.unwrap();
prop_assert_eq!(tracker.entries.len(), initial_count + 1);
let summary = tracker.get_session_summary(session_id).await.unwrap();
prop_assert_eq!(summary.command_count, 1);
prop_assert!((summary.total_cost - cost).abs() < 0.00001);
prop_assert_eq!(summary.total_tokens, input_tokens + output_tokens);
if cost > 0.0 {
prop_assert!(summary.average_cost > 0.0);
}
Ok(())
});
}
#[test]
fn property_concurrent_recording_consistency(
entries in prop::collection::vec(
(
property_strategies::valid_cost(),
property_strategies::valid_tokens(),
property_strategies::valid_tokens(),
property_strategies::valid_duration(),
property_strategies::command_name(),
property_strategies::model_name(),
),
1..20
)
) {
tokio_test::block_on(async {
let fixture = CostTestFixture::new();
let tracker = Arc::new(Mutex::new(CostTracker::new(fixture.storage_path.clone()).unwrap()));
let session_id = Uuid::new_v4();
let mut handles = vec![];
let expected_total: f64 = entries.iter().map(|(cost, _, _, _, _, _)| cost).sum();
for (i, (cost, input_tokens, output_tokens, duration, command, model)) in entries.into_iter().enumerate() {
let tracker_clone = Arc::clone(&tracker);
let handle = tokio::spawn(async move {
let entry = CostEntry::new(
session_id,
format!("{}_{}", command, i),
cost,
input_tokens,
output_tokens,
duration,
model,
);
let mut tracker_lock = tracker_clone.lock().await;
tracker_lock.record_cost(entry).await.unwrap();
});
handles.push(handle);
}
for handle in handles {
handle.await.unwrap();
}
let tracker_lock = tracker.lock().await;
let summary = tracker_lock.get_session_summary(session_id).await.unwrap();
prop_assert!((summary.total_cost - expected_total).abs() < 0.0001);
Ok(())
});
}
}
}
#[cfg(test)]
mod aggregation_analysis_tests {
use super::*;
use approx::assert_relative_eq;
#[tokio::test]
async fn test_cost_aggregation_by_session_multiple_entries() {
let fixture = CostTestFixture::new();
let mut tracker = CostTracker::new(fixture.storage_path.clone()).unwrap();
let session1 = Uuid::new_v4();
let session2 = Uuid::new_v4();
let session1_entries = vec![
CostEntry::new(
session1,
"analyze".to_string(),
0.10,
100,
200,
1000,
"claude-3-opus".to_string(),
),
CostEntry::new(
session1,
"generate".to_string(),
0.05,
50,
100,
500,
"claude-3-sonnet".to_string(),
),
CostEntry::new(
session1,
"review".to_string(),
0.03,
30,
60,
300,
"claude-3-haiku".to_string(),
),
];
let session2_entries = vec![
CostEntry::new(
session2,
"debug".to_string(),
0.08,
80,
160,
800,
"claude-3-opus".to_string(),
),
CostEntry::new(
session2,
"optimize".to_string(),
0.12,
120,
240,
1200,
"claude-3-sonnet".to_string(),
),
];
for entry in session1_entries.iter().chain(session2_entries.iter()) {
tracker.record_cost(entry.clone()).await.unwrap();
}
let summary1 = tracker.get_session_summary(session1).await.unwrap();
assert_eq!(summary1.command_count, 3);
assert_relative_eq!(summary1.total_cost, 0.18, epsilon = 0.00001); assert_relative_eq!(summary1.average_cost, 0.06, epsilon = 0.00001); assert_eq!(summary1.total_tokens, 540); assert_eq!(summary1.by_command.len(), 3);
assert_eq!(summary1.by_model.len(), 3);
let summary2 = tracker.get_session_summary(session2).await.unwrap();
assert_eq!(summary2.command_count, 2);
assert_relative_eq!(summary2.total_cost, 0.20, epsilon = 0.00001); assert_relative_eq!(summary2.average_cost, 0.10, epsilon = 0.00001); assert_eq!(summary2.total_tokens, 600); assert_eq!(summary2.by_command.len(), 2);
assert_eq!(summary2.by_model.len(), 2);
assert_relative_eq!(summary1.by_command["analyze"], 0.10, epsilon = 0.00001);
assert_relative_eq!(summary1.by_command["generate"], 0.05, epsilon = 0.00001);
assert_relative_eq!(summary1.by_command["review"], 0.03, epsilon = 0.00001);
assert_relative_eq!(summary2.by_command["debug"], 0.08, epsilon = 0.00001);
assert_relative_eq!(summary2.by_command["optimize"], 0.12, epsilon = 0.00001);
}
#[tokio::test]
async fn test_cost_aggregation_by_time_periods() {
let fixture = CostTestFixture::with_time_series_data(30); let tracker = fixture.create_tracker().await;
let now = Utc::now();
let daily_filter = CostFilter {
since: Some(now - Duration::days(1)),
until: Some(now),
..Default::default()
};
let daily_summary = tracker.get_filtered_summary(&daily_filter).await.unwrap();
let weekly_filter = CostFilter {
since: Some(now - Duration::days(7)),
until: Some(now),
..Default::default()
};
let weekly_summary = tracker.get_filtered_summary(&weekly_filter).await.unwrap();
let monthly_filter = CostFilter {
since: Some(now - Duration::days(30)),
until: Some(now),
..Default::default()
};
let monthly_summary = tracker.get_filtered_summary(&monthly_filter).await.unwrap();
assert!(daily_summary.command_count <= weekly_summary.command_count);
assert!(weekly_summary.command_count <= monthly_summary.command_count);
assert!(daily_summary.total_cost <= weekly_summary.total_cost);
assert!(weekly_summary.total_cost <= monthly_summary.total_cost);
assert_eq!(monthly_summary.command_count, 90);
assert_eq!(weekly_summary.command_count, 21);
assert!(daily_summary.command_count <= 3);
}
#[tokio::test]
async fn test_cost_filtering_by_date_ranges() {
let fixture = CostTestFixture::new();
let mut tracker = CostTracker::new(fixture.storage_path.clone()).unwrap();
let session_id = Uuid::new_v4();
let base_time = Utc::now() - Duration::days(10);
let mut entries = vec![];
for i in 0..10 {
let mut entry = CostEntry::new(
session_id,
format!("day_{}_cmd", i),
0.01 * (i + 1) as f64,
10 * (i + 1) as u32,
20 * (i + 1) as u32,
100 * (i + 1) as u64,
"claude-3-opus".to_string(),
);
entry.timestamp = base_time + Duration::days(i as i64);
entries.push(entry);
}
for entry in entries {
tracker.record_cost(entry).await.unwrap();
}
let mid_start = base_time + Duration::days(3);
let mid_end = base_time + Duration::days(7);
let mid_filter = CostFilter {
since: Some(mid_start),
until: Some(mid_end),
..Default::default()
};
let mid_summary = tracker.get_filtered_summary(&mid_filter).await.unwrap();
assert_eq!(mid_summary.command_count, 5); let expected_mid_cost: f64 = (4..=8).map(|i| 0.01 * i as f64).sum(); assert_relative_eq!(mid_summary.total_cost, expected_mid_cost, epsilon = 0.00001);
let exact_day_start = base_time + Duration::days(5);
let exact_day_end = exact_day_start + Duration::hours(23) + Duration::minutes(59);
let exact_filter = CostFilter {
since: Some(exact_day_start),
until: Some(exact_day_end),
..Default::default()
};
let exact_summary = tracker.get_filtered_summary(&exact_filter).await.unwrap();
assert_eq!(exact_summary.command_count, 1);
assert_relative_eq!(exact_summary.total_cost, 0.06, epsilon = 0.00001);
let inclusive_filter = CostFilter {
since: Some(base_time),
until: Some(base_time + Duration::days(9)),
..Default::default()
};
let inclusive_summary = tracker
.get_filtered_summary(&inclusive_filter)
.await
.unwrap();
assert_eq!(inclusive_summary.command_count, 10); let expected_total: f64 = (1..=10).map(|i| 0.01 * i as f64).sum();
assert_relative_eq!(
inclusive_summary.total_cost,
expected_total,
epsilon = 0.00001
);
}
#[tokio::test]
async fn test_cost_filtering_edge_cases() {
let fixture = CostTestFixture::new();
let mut tracker = CostTracker::new(fixture.storage_path.clone()).unwrap();
let session_id = Uuid::new_v4();
let now = Utc::now();
for i in 0..5 {
let mut entry = CostEntry::new(
session_id,
format!("test_cmd_{}", i),
0.01 * (i + 1) as f64,
10 * (i + 1) as u32,
20 * (i + 1) as u32,
100 * (i + 1) as u64,
"claude-3-opus".to_string(),
);
entry.timestamp = now - Duration::hours(i as i64);
tracker.record_cost(entry).await.unwrap();
}
let invalid_filter = CostFilter {
since: Some(now),
until: Some(now - Duration::hours(1)),
..Default::default()
};
let invalid_summary = tracker.get_filtered_summary(&invalid_filter).await.unwrap();
assert_eq!(invalid_summary.command_count, 0);
assert_eq!(invalid_summary.total_cost, 0.0);
let future_filter = CostFilter {
since: Some(now + Duration::hours(1)),
until: Some(now + Duration::hours(2)),
..Default::default()
};
let future_summary = tracker.get_filtered_summary(&future_filter).await.unwrap();
assert_eq!(future_summary.command_count, 0);
let old_filter = CostFilter {
since: Some(now - Duration::days(365)),
until: Some(now - Duration::days(364)),
..Default::default()
};
let old_summary = tracker.get_filtered_summary(&old_filter).await.unwrap();
assert_eq!(old_summary.command_count, 0);
let first_entry_time = now;
let boundary_filter = CostFilter {
since: Some(first_entry_time),
until: Some(first_entry_time),
..Default::default()
};
let boundary_summary = tracker
.get_filtered_summary(&boundary_filter)
.await
.unwrap();
assert_eq!(boundary_summary.command_count, 1);
let precise_start = now - Duration::microseconds(1);
let precise_end = now + Duration::microseconds(1);
let precise_filter = CostFilter {
since: Some(precise_start),
until: Some(precise_end),
..Default::default()
};
let precise_summary = tracker.get_filtered_summary(&precise_filter).await.unwrap();
assert_eq!(precise_summary.command_count, 1);
}
#[tokio::test]
async fn test_cost_aggregation_by_command_name() {
let fixture = CostTestFixture::new();
let mut tracker = CostTracker::new(fixture.storage_path.clone()).unwrap();
let session_id = Uuid::new_v4();
let entries = vec![
CostEntry::new(
session_id,
"analyze".to_string(),
0.10,
100,
200,
1000,
"claude-3-opus".to_string(),
),
CostEntry::new(
session_id,
"analyze".to_string(),
0.15,
150,
300,
1500,
"claude-3-opus".to_string(),
),
CostEntry::new(
session_id,
"analyze".to_string(),
0.08,
80,
160,
800,
"claude-3-sonnet".to_string(),
),
CostEntry::new(
session_id,
"generate".to_string(),
0.05,
50,
100,
500,
"claude-3-haiku".to_string(),
),
CostEntry::new(
session_id,
"generate".to_string(),
0.07,
70,
140,
700,
"claude-3-sonnet".to_string(),
),
CostEntry::new(
session_id,
"review".to_string(),
0.03,
30,
60,
300,
"claude-3-haiku".to_string(),
),
];
for entry in entries {
tracker.record_cost(entry).await.unwrap();
}
let summary = tracker.get_session_summary(session_id).await.unwrap();
assert_eq!(summary.by_command.len(), 3);
assert_relative_eq!(summary.by_command["analyze"], 0.33, epsilon = 0.00001); assert_relative_eq!(summary.by_command["generate"], 0.12, epsilon = 0.00001); assert_relative_eq!(summary.by_command["review"], 0.03, epsilon = 0.00001);
assert_eq!(summary.by_model.len(), 3);
assert_relative_eq!(summary.by_model["claude-3-opus"], 0.25, epsilon = 0.00001); assert_relative_eq!(summary.by_model["claude-3-sonnet"], 0.15, epsilon = 0.00001); assert_relative_eq!(summary.by_model["claude-3-haiku"], 0.08, epsilon = 0.00001);
let top_commands = tracker.get_top_commands(5).await.unwrap();
assert_eq!(top_commands.len(), 3);
assert_eq!(top_commands[0].0, "analyze"); assert_relative_eq!(top_commands[0].1, 0.33, epsilon = 0.00001);
assert_eq!(top_commands[1].0, "generate");
assert_relative_eq!(top_commands[1].1, 0.12, epsilon = 0.00001);
assert_eq!(top_commands[2].0, "review"); assert_relative_eq!(top_commands[2].1, 0.03, epsilon = 0.00001);
}
#[tokio::test]
async fn test_cost_aggregation_by_model_types() {
let fixture = CostTestFixture::new();
let mut tracker = CostTracker::new(fixture.storage_path.clone()).unwrap();
let session_id = Uuid::new_v4();
let entries = vec![
CostEntry::new(
session_id,
"complex_analysis".to_string(),
0.50,
500,
1000,
5000,
"claude-3-opus".to_string(),
),
CostEntry::new(
session_id,
"deep_review".to_string(),
0.60,
600,
1200,
6000,
"claude-3-opus".to_string(),
),
CostEntry::new(
session_id,
"standard_task".to_string(),
0.20,
200,
400,
2000,
"claude-3-sonnet".to_string(),
),
CostEntry::new(
session_id,
"balanced_work".to_string(),
0.25,
250,
500,
2500,
"claude-3-sonnet".to_string(),
),
CostEntry::new(
session_id,
"routine_check".to_string(),
0.15,
150,
300,
1500,
"claude-3-sonnet".to_string(),
),
CostEntry::new(
session_id,
"quick_task".to_string(),
0.05,
50,
100,
500,
"claude-3-haiku".to_string(),
),
CostEntry::new(
session_id,
"simple_query".to_string(),
0.03,
30,
60,
300,
"claude-3-haiku".to_string(),
),
CostEntry::new(
session_id,
"fast_check".to_string(),
0.04,
40,
80,
400,
"claude-3-haiku".to_string(),
),
CostEntry::new(
session_id,
"basic_task".to_string(),
0.02,
20,
40,
200,
"claude-3-haiku".to_string(),
),
];
for entry in entries {
tracker.record_cost(entry).await.unwrap();
}
let summary = tracker.get_session_summary(session_id).await.unwrap();
assert_eq!(summary.by_model.len(), 3);
assert_relative_eq!(summary.by_model["claude-3-opus"], 1.10, epsilon = 0.00001); assert_relative_eq!(summary.by_model["claude-3-sonnet"], 0.60, epsilon = 0.00001); assert_relative_eq!(summary.by_model["claude-3-haiku"], 0.14, epsilon = 0.00001);
assert_relative_eq!(summary.total_cost, 1.84, epsilon = 0.00001);
assert_eq!(summary.command_count, 9);
let opus_ratio = summary.by_model["claude-3-opus"] / summary.total_cost;
let sonnet_ratio = summary.by_model["claude-3-sonnet"] / summary.total_cost;
let haiku_ratio = summary.by_model["claude-3-haiku"] / summary.total_cost;
assert!(opus_ratio > sonnet_ratio); assert!(sonnet_ratio > haiku_ratio); assert_relative_eq!(
opus_ratio + sonnet_ratio + haiku_ratio,
1.0,
epsilon = 0.00001
);
}
#[tokio::test]
async fn test_global_vs_session_aggregation() {
let fixture = CostTestFixture::new();
let mut tracker = CostTracker::new(fixture.storage_path.clone()).unwrap();
let sessions = vec![Uuid::new_v4(), Uuid::new_v4(), Uuid::new_v4()];
let mut total_expected_cost = 0.0;
let mut total_expected_commands = 0;
for (session_index, &session_id) in sessions.iter().enumerate() {
for cmd_index in 0..5 {
let cost = 0.01 * ((session_index * 5 + cmd_index) + 1) as f64;
let entry = CostEntry::new(
session_id,
format!("s{}_cmd_{}", session_index, cmd_index),
cost,
10 * (cmd_index + 1) as u32,
20 * (cmd_index + 1) as u32,
100 * (cmd_index + 1) as u64,
format!("model_{}", cmd_index % 3),
);
tracker.record_cost(entry).await.unwrap();
total_expected_cost += cost;
total_expected_commands += 1;
}
}
let global_summary = tracker.get_global_summary().await.unwrap();
assert_eq!(global_summary.command_count, total_expected_commands);
assert_relative_eq!(
global_summary.total_cost,
total_expected_cost,
epsilon = 0.00001
);
assert_relative_eq!(
global_summary.average_cost,
total_expected_cost / total_expected_commands as f64,
epsilon = 0.00001
);
let mut sum_of_session_costs = 0.0;
let mut sum_of_session_commands = 0;
for (session_index, &session_id) in sessions.iter().enumerate() {
let session_summary = tracker.get_session_summary(session_id).await.unwrap();
assert_eq!(session_summary.command_count, 5);
let session_expected_cost: f64 = (0..5)
.map(|cmd_index| 0.01 * ((session_index * 5 + cmd_index) + 1) as f64)
.sum();
assert_relative_eq!(
session_summary.total_cost,
session_expected_cost,
epsilon = 0.00001
);
sum_of_session_costs += session_summary.total_cost;
sum_of_session_commands += session_summary.command_count;
}
assert_relative_eq!(
sum_of_session_costs,
global_summary.total_cost,
epsilon = 0.00001
);
assert_eq!(sum_of_session_commands, global_summary.command_count);
assert_eq!(global_summary.by_command.len(), 15); }
#[tokio::test]
async fn test_cost_aggregation_with_empty_results() {
let fixture = CostTestFixture::new();
let tracker = CostTracker::new(fixture.storage_path.clone()).unwrap();
let empty_session_id = Uuid::new_v4();
let empty_summary = tracker.get_session_summary(empty_session_id).await.unwrap();
assert_eq!(empty_summary.command_count, 0);
assert_eq!(empty_summary.total_cost, 0.0);
assert_eq!(empty_summary.average_cost, 0.0);
assert_eq!(empty_summary.total_tokens, 0);
assert!(empty_summary.by_command.is_empty());
assert!(empty_summary.by_model.is_empty());
let global_empty = tracker.get_global_summary().await.unwrap();
assert_eq!(global_empty.command_count, 0);
assert_eq!(global_empty.total_cost, 0.0);
assert_eq!(global_empty.average_cost, 0.0);
assert_eq!(global_empty.total_tokens, 0);
let no_match_filter = CostFilter {
command_pattern: Some("nonexistent_command".to_string()),
..Default::default()
};
let no_match_summary = tracker
.get_filtered_summary(&no_match_filter)
.await
.unwrap();
assert_eq!(no_match_summary.command_count, 0);
assert_eq!(no_match_summary.total_cost, 0.0);
let top_commands = tracker.get_top_commands(10).await.unwrap();
assert!(top_commands.is_empty());
}
#[tokio::test]
async fn test_cost_aggregation_precision_with_large_datasets() {
let fixture = CostTestFixture::with_large_dataset(1000);
let tracker = fixture.create_tracker().await;
let global_summary = tracker.get_global_summary().await.unwrap();
assert_eq!(global_summary.command_count, 1000);
let expected_total: f64 = (0..1000).map(|i| 0.001 + (i as f64 * 0.001)).sum();
assert_relative_eq!(global_summary.total_cost, expected_total, epsilon = 0.001);
let expected_average = expected_total / 1000.0;
assert_relative_eq!(
global_summary.average_cost,
expected_average,
epsilon = 0.001
);
let expected_tokens: u32 = (0..1000)
.map(|i| (10 + (i as u32 * 5)) + (20 + (i as u32 * 10)))
.sum();
assert_eq!(global_summary.total_tokens, expected_tokens);
let start = std::time::Instant::now();
let _summary = tracker.get_global_summary().await.unwrap();
let duration = start.elapsed();
assert!(duration.as_millis() < 100);
}
proptest! {
#[test]
fn property_aggregation_consistency(
entries in prop::collection::vec(
(
property_strategies::valid_cost(),
property_strategies::valid_tokens(),
property_strategies::valid_tokens(),
property_strategies::command_name(),
property_strategies::model_name(),
),
1..50
)
) {
let _ = tokio_test::block_on(async {
let fixture = CostTestFixture::new();
let mut tracker = CostTracker::new(fixture.storage_path.clone()).unwrap();
let session_id = Uuid::new_v4();
let mut expected_total_cost = 0.0;
let mut expected_total_tokens = 0u32;
for (i, (cost, input_tokens, output_tokens, command, model)) in entries.into_iter().enumerate() {
let entry = CostEntry::new(
session_id,
format!("{}_{}", command, i),
cost,
input_tokens,
output_tokens,
1000,
model,
);
tracker.record_cost(entry).await.unwrap();
expected_total_cost += cost;
expected_total_tokens += input_tokens + output_tokens;
}
let summary = tracker.get_session_summary(session_id).await.unwrap();
prop_assert!((summary.total_cost - expected_total_cost).abs() < 0.001);
prop_assert_eq!(summary.total_tokens, expected_total_tokens);
if summary.command_count > 0 {
prop_assert!((summary.average_cost - (expected_total_cost / summary.command_count as f64)).abs() < 0.001);
}
let command_total: f64 = summary.by_command.values().sum();
prop_assert!((command_total - summary.total_cost).abs() < 0.001);
let model_total: f64 = summary.by_model.values().sum();
prop_assert!((model_total - summary.total_cost).abs() < 0.001);
Ok(())
});
}
#[test]
fn property_filtering_subset_consistency(
cost_filter in 0.001f64..1.0f64,
token_filter in 1u32..1000u32,
) {
let _ = tokio_test::block_on(async {
let fixture = CostTestFixture::new();
let mut tracker = CostTracker::new(fixture.storage_path.clone()).unwrap();
let session_id = Uuid::new_v4();
for i in 0..20 {
let entry = CostEntry::new(
session_id,
format!("cmd_{}", i),
0.01 * (i + 1) as f64,
10 * (i + 1) as u32,
20 * (i + 1) as u32,
1000,
"claude-3-opus".to_string(),
);
tracker.record_cost(entry).await.unwrap();
}
let global_summary = tracker.get_global_summary().await.unwrap();
let cost_filtered = CostFilter {
min_cost: Some(cost_filter),
..Default::default()
};
let filtered_summary = tracker.get_filtered_summary(&cost_filtered).await.unwrap();
prop_assert!(filtered_summary.command_count <= global_summary.command_count);
prop_assert!(filtered_summary.total_cost <= global_summary.total_cost);
prop_assert!(filtered_summary.total_tokens <= global_summary.total_tokens);
for entry in tracker.get_entries(&cost_filtered).await.unwrap() {
prop_assert!(entry.cost_usd >= cost_filter);
}
Ok(())
});
}
}
}