use std::sync::Arc;
use aether_core::context::CompactionConfig;
use aether_core::events::{
AgentEvent, CompactionOutcome, ContextEvent, LlmCallOutcome, LlmCallPurpose, TurnEvent, TurnOutcome,
};
use aether_core::testing::{TestScenario, test_agent};
use llm::testing::llm_response;
use llm::types::IsoString;
use llm::{ChatMessage, ContentBlock};
use tokio::sync::Notify;
fn user_message(text: &str) -> ChatMessage {
ChatMessage::User { content: vec![ContentBlock::text(text)], timestamp: IsoString::now() }
}
#[tokio::test]
async fn oversized_context_is_compacted_before_the_llm_call() {
let result = test_agent()
.llm_responses(&[llm_response("sum").text(&["summary"]).build(), llm_response("msg").text(&["hello"]).build()])
.context_window_override(100)
.compaction_config(CompactionConfig::with_threshold(0.85))
.messages(vec![user_message(&"x".repeat(400))])
.user_text("go")
.run_with_context()
.await
.unwrap();
let contexts = result.captured_contexts.lock().unwrap();
let messages = contexts.last().expect("chat request should reach the provider").messages();
assert!(
matches!(&messages[0], ChatMessage::Summary { content, .. } if content == "summary"),
"expected the prior conversation compacted into a summary, got {messages:?}"
);
assert!(
matches!(&messages[1], ChatMessage::User { content, .. } if content == &vec![ContentBlock::text("go")]),
"the fresh user message must survive compaction as a real turn, got {messages:?}"
);
}
#[tokio::test]
async fn cancel_during_compaction_ends_the_turn() {
let release = Arc::new(Notify::new());
let trace = test_agent()
.llm_responses(&[llm_response("sum").text(&["summary"]).build()])
.context_window_override(100)
.compaction_config(CompactionConfig::with_threshold(0.85))
.messages(vec![user_message(&"x".repeat(400))])
.pause_turn_after(0, 0, release)
.scenario(TestScenario::new().user_text("go").wait_for_compaction_start().cancel().wait_for_turn_end())
.run_trace()
.await
.unwrap();
let events = trace.events();
assert!(
events.iter().any(|event| matches!(
event,
AgentEvent::Turn(TurnEvent::LlmCallEnded {
purpose: LlmCallPurpose::Compaction,
outcome: LlmCallOutcome::Cancelled,
})
)),
"cancel should end the in-flight compaction call"
);
assert!(
events.iter().any(|event| matches!(
event,
AgentEvent::Context(ContextEvent::CompactionEnded { outcome: CompactionOutcome::Cancelled })
)),
"cancel should end the semantic compaction lifecycle"
);
assert!(
matches!(events.last(), Some(AgentEvent::Turn(TurnEvent::Ended { outcome: TurnOutcome::Cancelled }))),
"the turn must end as cancelled"
);
}
#[tokio::test]
async fn cancel_during_compaction_preserves_the_initiating_message() {
let release = Arc::new(Notify::new());
let result = test_agent()
.llm_responses(&[
llm_response("sum").text(&["summary"]).build(),
llm_response("sum2").text(&["summary"]).build(),
llm_response("msg").text(&["hello"]).build(),
])
.context_window_override(100)
.compaction_config(CompactionConfig::with_threshold(0.85))
.messages(vec![user_message(&"x".repeat(400))])
.pause_turn_after(0, 0, release)
.scenario(
TestScenario::new()
.user_text("go")
.wait_for_compaction_start()
.cancel()
.wait_for_turn_end()
.user_text("next")
.wait_for_turn_end(),
)
.run_with_context()
.await
.unwrap();
let contexts = result.captured_contexts.lock().unwrap();
let preserved = contexts.iter().any(|context| {
context
.messages()
.iter()
.any(|message| matches!(message, ChatMessage::User { content, .. } if content == &vec![ContentBlock::text("go")]))
});
assert!(
preserved,
"the message that initiated the cancelled turn must stay in the conversation, as it did before compaction became cancellable"
);
}