use crate::brain::agent::context::{AgentContext, CompactionScope};
use crate::brain::agent::service::AgentService;
use crate::brain::agent::service::compaction::{
BudgetPhase, PendingState, gate_announces_compaction, gate_warn_message,
must_wait_for_compaction,
};
use crate::brain::provider::{ContentBlock, Message, Role};
fn ctx(messages: Vec<Message>) -> AgentContext {
let mut c = AgentContext::new(uuid::Uuid::nil(), 200_000);
for m in messages {
c.add_message(m);
}
c
}
fn tool_use(id: &str) -> Message {
Message {
role: Role::Assistant,
content: vec![ContentBlock::ToolUse {
id: id.into(),
name: "bash".into(),
input: serde_json::json!({ "command": "ls" }),
}],
}
}
fn tool_result(id: &str, body: &str) -> Message {
Message {
role: Role::User,
content: vec![ContentBlock::ToolResult {
tool_use_id: id.into(),
content: body.into(),
is_error: None,
}],
}
}
fn text_of(msg: &Message) -> String {
msg.content
.iter()
.filter_map(|b| match b {
ContentBlock::Text { text } => Some(text.clone()),
ContentBlock::ToolResult { content, .. } => Some(content.clone()),
_ => None,
})
.collect::<Vec<_>>()
.join(" ")
}
#[test]
fn work_done_during_the_summariser_call_survives_the_swap() {
let mut context = ctx(vec![
Message::user("old question"),
Message::assistant("old answer"),
Message::user("new question"),
tool_use("tu_1"),
tool_result("tu_1", "the command output"),
]);
AgentService::apply_compaction_summary_after(
&mut context,
CompactionScope::FullWindow,
"SUMMARY BODY",
2,
);
let rendered: Vec<String> = context.messages.iter().map(text_of).collect();
assert!(
rendered[0].contains("SUMMARY BODY"),
"summary is not the anchor: {rendered:?}"
);
assert!(
rendered.iter().any(|t| t.contains("new question")),
"the turn's own question was deleted: {rendered:?}"
);
assert!(
rendered.iter().any(|t| t.contains("the command output")),
"a tool result the summary never saw was deleted: {rendered:?}"
);
}
#[test]
fn the_summarised_prefix_does_not_survive_as_messages() {
let mut context = ctx(vec![
Message::user("old question"),
Message::assistant("old answer"),
Message::user("new question"),
]);
AgentService::apply_compaction_summary_after(
&mut context,
CompactionScope::FullWindow,
"SUMMARY BODY",
2,
);
assert_eq!(
context.messages.len(),
2,
"compaction kept what it just summarised as live messages"
);
assert_eq!(text_of(&context.messages[1]), "new question");
}
#[test]
fn a_delta_opening_on_tool_results_drops_the_orphans() {
let mut context = ctx(vec![
Message::user("question"),
tool_use("tu_1"),
tool_result("tu_1", "orphaned output"),
Message::assistant("carrying on"),
]);
AgentService::apply_compaction_summary_after(
&mut context,
CompactionScope::FullWindow,
"SUMMARY BODY",
2,
);
assert!(
!AgentContext::is_orphaned_tool_result_msg(&context.messages[1]),
"an orphaned tool result was left directly after the summary"
);
let rendered: Vec<String> = context.messages.iter().map(text_of).collect();
assert!(
rendered.iter().any(|t| t.contains("carrying on")),
"everything after the orphan was thrown away too: {rendered:?}"
);
}
#[test]
fn an_all_orphan_delta_leaves_just_the_summary() {
let mut context = ctx(vec![
Message::user("question"),
tool_use("tu_1"),
tool_result("tu_1", "one"),
tool_result("tu_1", "two"),
]);
AgentService::apply_compaction_summary_after(
&mut context,
CompactionScope::FullWindow,
"SUMMARY BODY",
2,
);
assert_eq!(context.messages.len(), 1);
assert!(text_of(&context.messages[0]).contains("SUMMARY BODY"));
}
#[test]
fn no_delta_behaves_like_a_blocking_compaction() {
let mut context = ctx(vec![
Message::user("question"),
Message::assistant("answer"),
]);
let len = context.messages.len();
AgentService::apply_compaction_summary_after(
&mut context,
CompactionScope::FullWindow,
"SUMMARY BODY",
len,
);
assert_eq!(context.messages.len(), 1);
assert!(text_of(&context.messages[0]).contains("SUMMARY BODY"));
}
#[test]
fn a_snapshot_that_outlived_its_context_still_applies() {
let mut context = ctx(vec![Message::user("only message")]);
AgentService::apply_compaction_summary_after(
&mut context,
CompactionScope::FullWindow,
"SUMMARY BODY",
99,
);
assert_eq!(context.messages.len(), 1);
assert!(text_of(&context.messages[0]).contains("SUMMARY BODY"));
}
#[test]
fn the_budget_counts_the_delta_it_kept() {
let mut context = ctx(vec![
Message::user("old question"),
Message::assistant("old answer"),
Message::user("a considerably longer message that carries real weight in the budget"),
]);
AgentService::apply_compaction_summary_after(
&mut context,
CompactionScope::FullWindow,
"SUMMARY BODY",
2,
);
let recomputed: usize = context
.messages
.iter()
.map(|m| context.estimate_message_tokens(m))
.sum::<usize>()
+ context
.system_brain
.as_deref()
.map(AgentContext::estimate_tokens)
.unwrap_or(0);
assert_eq!(
context.token_count, recomputed,
"kept messages are in the context but not in its budget"
);
}
#[test]
fn a_turn_about_to_answer_always_waits() {
for usage in [10.0, 66.0, 79.9, 95.0] {
assert!(
must_wait_for_compaction(BudgetPhase::TurnStart, usage),
"turn start ran ahead of the summariser at {usage}%"
);
}
}
#[test]
fn the_tool_loop_runs_ahead_below_the_ceiling() {
for usage in [10.0, 66.0, 79.9] {
assert!(
!must_wait_for_compaction(BudgetPhase::MidLoop, usage),
"the loop blocked at {usage}%, which is the stall we removed"
);
}
}
#[test]
fn the_tool_loop_waits_at_the_ceiling() {
for usage in [80.0, 91.0, 140.0] {
assert!(
must_wait_for_compaction(BudgetPhase::MidLoop, usage),
"the loop kept growing the context at {usage}%"
);
}
}
#[test]
fn one_cycle_announces_once_across_three_hits() {
let cycle = [
PendingState::Empty,
PendingState::StillRunning,
PendingState::StillRunning,
];
let notices = cycle
.iter()
.filter(|state| gate_announces_compaction(state))
.count();
assert_eq!(notices, 1, "three gate hits announced {notices} times");
}
#[test]
fn an_applied_summary_is_never_re_announced() {
assert!(!gate_announces_compaction(&PendingState::Applied(
"the summary".into()
)));
}
#[test]
fn the_blocking_fallback_still_announces() {
assert!(gate_announces_compaction(&PendingState::Failed));
}
#[test]
fn the_announcement_sits_below_the_guard() {
const SRC: &str = include_str!("../brain/agent/service/compaction.rs");
let guard = SRC
.find("if !gate_announces_compaction(")
.expect("the gate guard");
let announce = SRC[guard..]
.find("tracing::warn!(\"{}\", gate_warn_message(")
.expect("the gate WARN")
+ guard;
assert!(
guard < announce,
"the guard at {guard} must return before the announcement at {announce}"
);
}
#[test]
fn the_gate_warn_never_displays_below_the_trigger_band() {
for pct in [65.01, 65.04, 65.05, 65.4, 65.95, 89.99, 97.3] {
let msg = gate_warn_message(pct);
let shown: f64 = msg
.split("Context at ")
.nth(1)
.and_then(|rest| rest.split('%').next())
.and_then(|digits| digits.parse().ok())
.expect("the message must carry a parseable fill percentage");
assert!(
shown >= 65.0,
"{pct}% fired the gate but the message displays {shown}%: {msg}"
);
assert!(
msg.contains("(threshold 65%)"),
"the trigger must be a label, not an inequality against the shown value: {msg}"
);
}
}