use super::*;
fn app_with_model(model: &str) -> (App, SessionState) {
(idle_app(), SessionState::new("s1", None, model))
}
fn stream_with(messages: Vec<StreamMsg>) -> Stream {
let (tx, rx) = unbounded_channel();
for message in messages {
let _ = tx.send(message);
}
Stream {
rx,
cancel: CancellationToken::new(),
prompt: "question".into(),
}
}
fn run_turn(app: &mut App, state: &mut SessionState, messages: Vec<StreamMsg>) -> String {
app.request.stream = Some(stream_with(messages));
assert!(app.drain_stream(state), "the turn finished");
app.transcript
.blocks()
.iter()
.rev()
.find(|b| b.text.contains("tokens in"))
.expect("the footer was pushed")
.text
.clone()
}
fn output(usage: TokenUsage) -> AgentOutput {
AgentOutput {
answer: "the answer".into(),
events: Vec::new(),
used_bounded_sql_query: false,
tool_metadata: Vec::new(),
usage,
learning_usage: None,
truncated: false,
answer_sql: None,
}
}
fn answering_report(input: u64) -> StreamMsg {
StreamMsg::Event(AgentEvent::usage(
UsageCall::Answer,
TokenUsage::new(input, 20),
))
}
fn notice_texts(app: &App) -> Vec<String> {
app.transcript
.blocks()
.iter()
.filter(|b| b.kind == BlockKind::System && !b.text.contains("tokens in"))
.map(|b| b.text.clone())
.collect()
}
#[test]
fn crossing_the_warn_threshold_emits_one_notice_then_stays_silent() {
use saya_agent::CONTEXT_WARN_PERCENT;
let (mut app, mut state) = app_with_model("gpt-4o");
let warned = (CONTEXT_WARN_PERCENT * 128_000) / 100;
let first = run_turn(
&mut app,
&mut state,
vec![
answering_report(warned),
StreamMsg::Done(Ok(output(TokenUsage::new(warned, 20)))),
],
);
assert!(
first.contains("· ctx 70% of 128k"),
"the footer's existing output is unchanged: {first}"
);
let notices = notice_texts(&app);
assert_eq!(notices.len(), 1, "one notice on the crossing: {notices:?}");
let notice = ¬ices[0];
assert!(
notice.contains("70%"),
"the notice states the percentage: {notice}"
);
assert!(
notice.contains("128k"),
"the notice states the window: {notice}"
);
assert!(
notice.contains(&saya_agent::CONTEXT_COMPACT_PERCENT.to_string()),
"the notice names what happens at the compact threshold: {notice}"
);
for turn in 1..=3 {
let footer = run_turn(
&mut app,
&mut state,
vec![
answering_report(warned + 1_000),
StreamMsg::Done(Ok(output(TokenUsage::new(warned + 1_000, 20)))),
],
);
assert!(
footer.contains("ctx"),
"staying above keeps the footer figure: {footer}"
);
assert_eq!(
notice_texts(&app).len(),
1,
"turn {turn} above the threshold emits no second notice"
);
}
}
#[test]
fn falling_below_re_arms_and_crossing_again_emits_a_second_notice() {
use saya_agent::CONTEXT_WARN_PERCENT;
let (mut app, mut state) = app_with_model("gpt-4o");
let warned = (CONTEXT_WARN_PERCENT * 128_000) / 100;
let _ = run_turn(
&mut app,
&mut state,
vec![
answering_report(warned),
StreamMsg::Done(Ok(output(TokenUsage::new(warned, 20)))),
],
);
assert_eq!(notice_texts(&app).len(), 1);
let below = run_turn(
&mut app,
&mut state,
vec![
answering_report(10_000),
StreamMsg::Done(Ok(output(TokenUsage::new(10_000, 20)))),
],
);
assert!(
below.contains("ctx 8% of 128k"),
"below the threshold the footer still renders: {below}"
);
assert_eq!(
notice_texts(&app).len(),
1,
"falling below re-arms silently, with no new notice"
);
let silent = run_turn(
&mut app,
&mut state,
vec![StreamMsg::Done(Ok(output(TokenUsage::new(10_000, 20))))],
);
assert!(
!silent.contains("ctx"),
"no per-call report means no ctx figure: {silent}"
);
assert_eq!(
notice_texts(&app).len(),
1,
"a silent turn leaves the armed flag untouched"
);
let _ = run_turn(
&mut app,
&mut state,
vec![
answering_report(warned),
StreamMsg::Done(Ok(output(TokenUsage::new(warned, 20)))),
],
);
assert_eq!(
notice_texts(&app).len(),
2,
"the second crossing emits a second notice"
);
}