use distri_types::{
ContextBudget,
events::{AgentEventType, RunUsage},
};
use crate::printer::ContextHealth;
fn make_budget(window: usize) -> ContextBudget {
ContextBudget {
system_prompt_static_tokens: 500,
system_prompt_dynamic_tokens: 100,
tool_schema_tokens: 200,
deferred_tool_tokens: 50,
skill_listing_tokens: 0,
conversation_tokens: 150,
tool_result_tokens: 0,
context_window_size: window,
static_prefix_cache_hit: false,
static_prefix_hash: None,
}
}
fn make_usage(input: u32, output: u32) -> RunUsage {
RunUsage {
input_tokens: input,
output_tokens: output,
total_tokens: input + output,
cached_tokens: 0,
estimated_tokens: 0,
model: Some("gpt-5.1".to_string()),
cost_usd: None,
}
}
#[test]
fn update_from_budget_sets_last_budget() {
let mut h = ContextHealth::default();
assert!(h.last_budget.is_none());
let budget = make_budget(20_000);
h.update_from_budget(&budget);
assert!(h.last_budget.is_some());
assert_eq!(h.tokens_limit, 20_000);
assert_eq!(h.tokens_used, budget.total_tokens());
}
#[test]
fn update_from_budget_computes_utilization() {
let mut h = ContextHealth::default();
let budget = make_budget(20_000); h.update_from_budget(&budget);
assert!((h.utilization - 0.05).abs() < 0.001);
assert!(!h.is_warning);
assert!(!h.is_critical);
}
#[test]
fn update_from_budget_with_zero_window_still_stores() {
let mut h = ContextHealth::default();
let budget = ContextBudget::default(); h.update_from_budget(&budget);
assert!(h.last_budget.is_some());
assert_eq!(h.tokens_limit, 0);
}
#[test]
fn reset_run_tokens_clears_api_counters() {
let mut h = ContextHealth::default();
h.api_input_tokens = 5_000;
h.api_output_tokens = 300;
h.api_cached_tokens = 1_000;
h.cost_usd = Some(0.42);
h.reset_run_tokens();
assert_eq!(h.api_input_tokens, 0);
assert_eq!(h.api_output_tokens, 0);
assert_eq!(h.api_cached_tokens, 0);
assert!(h.cost_usd.is_none());
}
#[test]
fn reset_run_tokens_preserves_budget_and_context() {
let mut h = ContextHealth::default();
let budget = make_budget(80_000);
h.update_from_budget(&budget);
h.api_input_tokens = 9_000;
h.reset_run_tokens();
assert!(h.last_budget.is_some());
assert_eq!(h.tokens_limit, 80_000);
assert_eq!(h.api_input_tokens, 0);
}
#[test]
fn update_from_usage_accumulates_tokens() {
let mut h = ContextHealth::default();
h.update_from_usage(&make_usage(1_000, 200));
h.update_from_usage(&make_usage(2_000, 400));
assert_eq!(h.api_input_tokens, 3_000);
assert_eq!(h.api_output_tokens, 600);
}
#[test]
fn update_from_usage_sets_model() {
let mut h = ContextHealth::default();
let u = RunUsage {
model: Some("claude-sonnet-4".to_string()),
input_tokens: 100,
output_tokens: 50,
total_tokens: 150,
cached_tokens: 0,
estimated_tokens: 0,
cost_usd: None,
};
h.update_from_usage(&u);
assert_eq!(h.model.as_deref(), Some("claude-sonnet-4"));
}
#[test]
fn tokens_reset_between_simulated_runs() {
let mut h = ContextHealth::default();
for _ in 0..3 {
h.update_from_usage(&make_usage(1_000, 100));
}
assert_eq!(h.api_input_tokens, 3_000);
h.reset_run_tokens();
assert_eq!(h.api_input_tokens, 0);
for _ in 0..2 {
h.update_from_usage(&make_usage(1_500, 200));
}
assert_eq!(
h.api_input_tokens, 3_000,
"run 2 total should be 3K, not 6K"
);
}
#[test]
fn context_budget_utilization_zero_window() {
let budget = ContextBudget::default();
assert_eq!(budget.utilization(), 0.0);
assert_eq!(budget.remaining_tokens(), 0);
assert!(!budget.is_warning());
assert!(!budget.is_critical());
}
#[test]
fn context_budget_total_tokens_correct() {
let budget = make_budget(20_000);
assert_eq!(budget.total_tokens(), 1_000);
assert_eq!(budget.remaining_tokens(), 19_000);
assert!((budget.utilization() - 0.05).abs() < 0.001);
}
#[test]
fn print_context_breakdown_no_data() {
let h = ContextHealth::default();
h.print_context_breakdown();
}
#[test]
fn print_context_breakdown_with_zero_window() {
let mut h = ContextHealth::default();
h.update_from_budget(&ContextBudget::default());
h.print_context_breakdown();
}
#[test]
fn print_context_breakdown_with_real_budget() {
let mut h = ContextHealth::default();
h.update_from_budget(&make_budget(20_000));
h.print_context_breakdown();
}
#[test]
fn context_budget_update_event_round_trips() {
let budget = make_budget(80_000);
let event = AgentEventType::ContextBudgetUpdate {
budget: budget.clone(),
is_warning: false,
is_critical: false,
};
let json = serde_json::to_value(&event).unwrap();
let decoded: AgentEventType = serde_json::from_value(json).unwrap();
match decoded {
AgentEventType::ContextBudgetUpdate { budget: b, .. } => {
assert_eq!(b.context_window_size, 80_000);
assert_eq!(b.system_prompt_static_tokens, 500);
assert_eq!(b.conversation_tokens, 150);
}
_ => panic!("expected ContextBudgetUpdate"),
}
}