use koda_core::engine::EngineEvent;
use koda_test_utils::{Env, MockProvider, MockResponse};
#[tokio::test]
async fn corrective_context_usage_uses_actual_prompt_tokens() {
let env = Env::builder().max_context_tokens(200_000).build().await;
env.insert_user_message("hello").await;
let provider = MockProvider::new(vec![MockResponse::Text("hi".into())]);
let (result, events) = env.run_inference_result(&provider).await;
assert!(result.is_ok(), "inference must succeed: {:?}", result.err());
let context_events: Vec<(usize, usize)> = events
.iter()
.filter_map(|e| match e {
EngineEvent::ContextUsage { used, max } => Some((*used, *max)),
_ => None,
})
.collect();
assert!(
context_events.len() >= 2,
"expected at least 2 ContextUsage events (heuristic + corrective), \
got {}: {context_events:?}",
context_events.len()
);
let (last_used, last_max) = *context_events.last().unwrap();
assert_eq!(
last_used, 10,
"last ContextUsage.used must equal actual prompt_tokens (10 from mock), \
got {last_used}"
);
assert_eq!(
last_max, 200_000,
"max must match configured context window"
);
let (first_used, _) = context_events[0];
assert_ne!(
first_used, last_used,
"heuristic and corrective ContextUsage.used should differ, \
both were {first_used} — corrective event may not be firing"
);
}
#[tokio::test]
async fn context_percentage_reflects_actual_tokens_after_turn() {
let env = Env::builder().max_context_tokens(100_000).build().await;
env.insert_user_message("test message for context accuracy")
.await;
let provider = MockProvider::new(vec![MockResponse::Text("response".into())]);
let (result, events) = env.run_inference_result(&provider).await;
assert!(result.is_ok());
let footer = events.iter().find_map(|e| match e {
EngineEvent::Footer { prompt_tokens, .. } => Some(*prompt_tokens),
_ => None,
});
assert!(footer.is_some(), "Footer event must be emitted");
let footer_tokens = footer.unwrap();
let last_context_used = events
.iter()
.filter_map(|e| match e {
EngineEvent::ContextUsage { used, .. } => Some(*used),
_ => None,
})
.next_back()
.expect("at least one ContextUsage event required");
assert_eq!(
last_context_used as i64, footer_tokens,
"last ContextUsage.used ({last_context_used}) must match \
Footer.prompt_tokens ({footer_tokens})"
);
}
#[tokio::test]
async fn context_meter_uses_last_iteration_not_sum() {
let env = Env::builder().max_context_tokens(25).build().await;
env.insert_user_message("do two things then summarise")
.await;
let provider = MockProvider::new(vec![
MockResponse::tool_call("Bash", serde_json::json!({"command": "echo one"})),
MockResponse::tool_call("Bash", serde_json::json!({"command": "echo two"})),
MockResponse::Text("Both done.".into()),
]);
let (result, events) = env.run_inference_result(&provider).await;
assert!(result.is_ok(), "inference must succeed: {:?}", result.err());
let last_context_used = events
.iter()
.filter_map(|e| match e {
EngineEvent::ContextUsage { used, .. } => Some(*used),
_ => None,
})
.next_back()
.expect("at least one ContextUsage event required");
let footer_tokens = events
.iter()
.find_map(|e| match e {
EngineEvent::Footer { prompt_tokens, .. } => Some(*prompt_tokens),
_ => None,
})
.expect("Footer event must be emitted");
assert_eq!(
last_context_used, 10,
"ContextUsage.used must be the last iteration's prompt_tokens (10), \
not the cumulative sum — got {last_context_used} (#946)"
);
assert_eq!(
footer_tokens, 30,
"Footer.prompt_tokens must remain the cumulative sum across iterations \
(3 iterations × 10 = 30), got {footer_tokens}"
);
assert_ne!(
last_context_used as i64, footer_tokens,
"ContextUsage.used and Footer.prompt_tokens must diverge on \
multi-iteration turns (they're measuring different things)"
);
assert!(
last_context_used <= 25,
"context meter must never exceed max_context_tokens — \
got {last_context_used}/25 (#946 regression)"
);
}