use crate::brain::agent::service::{
AgentService, MessageQueueCallback, ProgressCallback, ProgressEvent, QueuedUserMessage,
};
use crate::brain::provider::{
ContentBlock, LLMRequest, LLMResponse, Provider, ProviderStream, Role, StopReason, TokenUsage,
};
use crate::brain::tools::ToolRegistry;
use crate::db::Database;
use crate::services::{ServiceContext, SessionService};
use crate::tests::agent_service_mocks::{MockProviderWithTools, MockTool};
use async_trait::async_trait;
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use uuid::Uuid;
const DRAFT: &str = "SUPERSEDED-DRAFT-1784 is the recipe for the first request.";
const FINAL: &str = "SINGLE-FINAL-1784 covers both requests in one reply.";
const QUEUE_TEXT: &str = "and also the smoothie one.";
const THIRD: &str = "THIRD-ROUND-1784 means the loop kept going after the fold.";
fn text_response(text: &str) -> LLMResponse {
LLMResponse {
id: "fold-mock-response".to_string(),
model: "mock-model".to_string(),
content: vec![ContentBlock::Text {
text: text.to_string(),
}],
stop_reason: Some(StopReason::EndTurn),
usage: TokenUsage {
input_tokens: 10,
output_tokens: 20,
..Default::default()
},
streaming_active_secs: None,
tool_text_leak: false,
}
}
struct FoldProvider {
requests: Mutex<Vec<Vec<(String, String)>>>,
}
impl FoldProvider {
fn new() -> Self {
Self {
requests: Mutex::new(Vec::new()),
}
}
fn snapshot(&self) -> Vec<Vec<(String, String)>> {
self.requests.lock().unwrap().clone()
}
}
#[async_trait]
impl Provider for FoldProvider {
async fn complete(&self, request: LLMRequest) -> crate::brain::provider::Result<LLMResponse> {
let seen: Vec<(String, String)> = request
.messages
.iter()
.map(|m| {
let text = m
.content
.iter()
.filter_map(|b| match b {
ContentBlock::Text { text } => Some(text.as_str()),
_ => None,
})
.collect::<Vec<_>>()
.join("\n");
(format!("{:?}", m.role), text)
})
.collect();
let round = {
let mut guard = self.requests.lock().unwrap();
guard.push(seen);
guard.len()
};
let reply = match round {
1 => DRAFT,
2 => FINAL,
_ => THIRD,
};
Ok(text_response(reply))
}
async fn stream(&self, request: LLMRequest) -> crate::brain::provider::Result<ProviderStream> {
use crate::brain::provider::{ContentDelta, MessageDelta, StreamEvent, StreamMessage};
let response = self.complete(request).await?;
let mut events = vec![Ok(StreamEvent::MessageStart {
message: StreamMessage {
id: response.id.clone(),
model: response.model.clone(),
role: Role::Assistant,
usage: response.usage,
},
})];
for (i, block) in response.content.iter().enumerate() {
if let ContentBlock::Text { text } = block {
events.push(Ok(StreamEvent::ContentBlockStart {
index: i,
content_block: ContentBlock::Text {
text: String::new(),
},
}));
events.push(Ok(StreamEvent::ContentBlockDelta {
index: i,
delta: ContentDelta::TextDelta { text: text.clone() },
}));
events.push(Ok(StreamEvent::ContentBlockStop { index: i }));
}
}
events.push(Ok(StreamEvent::MessageDelta {
delta: MessageDelta {
stop_reason: response.stop_reason,
stop_sequence: None,
},
usage: response.usage,
}));
events.push(Ok(StreamEvent::MessageStop));
Ok(Box::pin(futures::stream::iter(events)))
}
fn name(&self) -> &str {
"fold-mock"
}
fn default_model(&self) -> &str {
"mock-model"
}
fn supported_models(&self) -> Vec<String> {
vec!["mock-model".to_string()]
}
fn context_window(&self, _model: &str) -> Option<u32> {
Some(4096)
}
fn calculate_cost(&self, _model: &str, _input: u32, _output: u32) -> f64 {
0.0
}
}
async fn fresh_session() -> (ServiceContext, Uuid) {
let db = Database::connect_in_memory().await.unwrap();
db.run_migrations().await.unwrap();
let context = ServiceContext::new(db.pool().clone());
let session = SessionService::new(context.clone())
.create_session(Some("1784 fold test".to_string()))
.await
.unwrap();
(context, session.id)
}
fn collector() -> (ProgressCallback, Arc<Mutex<Vec<ProgressEvent>>>) {
let sink = Arc::new(Mutex::new(Vec::new()));
let s = sink.clone();
let cb: ProgressCallback = Arc::new(move |_sid, ev| {
s.lock().unwrap().push(ev);
});
(cb, sink)
}
fn one_message_queue(session_id: Uuid) -> MessageQueueCallback {
let queues = Arc::new(tokio::sync::Mutex::new(HashMap::from([(
session_id,
QUEUE_TEXT.to_string(),
)])));
Arc::new(move |sid: Uuid| {
let q = queues.clone();
Box::pin(async move { q.lock().await.remove(&sid).map(QueuedUserMessage::plain) })
})
}
fn delivered_text(events: &[ProgressEvent]) -> Vec<String> {
events
.iter()
.filter_map(|e| match e {
ProgressEvent::IntermediateText { text, .. } if !text.is_empty() => Some(text.clone()),
_ => None,
})
.collect()
}
fn event_summary(events: &[ProgressEvent]) -> String {
let parts: Vec<String> = events
.iter()
.map(|e| match e {
ProgressEvent::IntermediateText { text, .. } => {
let head: String = text.chars().take(24).collect();
format!("IntermediateText({head:?})")
}
ProgressEvent::QueuedUserMessage { text } => {
let head: String = text.chars().take(24).collect();
format!("QueuedUserMessage({head:?})")
}
ProgressEvent::StreamingChunk { text } => format!("StreamingChunk(len={})", text.len()),
other => {
let mut s = format!("{other:?}");
s.truncate(24);
s
}
})
.collect();
parts.join(" ")
}
#[tokio::test]
async fn a_text_only_round_with_a_queued_message_does_not_deliver_the_draft() {
let (context, session_id) = fresh_session().await;
let provider = Arc::new(FoldProvider::new());
let (progress, sink) = collector();
let agent = AgentService::new_for_test(provider.clone(), context)
.await
.with_message_queue_callback(Some(one_message_queue(session_id)));
let response = agent
.send_message_with_tools_and_callback(
session_id,
"give me the pasta recipe".to_string(),
None,
None,
None,
Some(progress),
"test",
None,
None,
)
.await
.unwrap();
let events = sink.lock().unwrap().clone();
assert!(
!delivered_text(&events)
.iter()
.any(|t| t.contains("SUPERSEDED-DRAFT")),
"the superseded draft was relayed as its own message: {events:?}"
);
assert!(
response.content.contains("SINGLE-FINAL-1784"),
"the turn must end with the folded final, got {:?}",
response.content
);
let bodies = delivered_text(&events);
assert!(
!bodies.iter().any(|t| t.contains("SUPERSEDED-DRAFT")),
"the superseded draft was relayed as its own message: [{}]",
event_summary(&events)
);
assert!(
bodies.iter().all(|t| !t.contains("THIRD-ROUND")),
"the fold made the loop keep iterating: [{}]",
event_summary(&events)
);
assert_eq!(
provider.snapshot().len(),
2,
"a text-only fold must cost exactly two provider rounds (draft, then the folded final), got {}",
provider.snapshot().len()
);
let announced = events
.iter()
.filter(|e| matches!(e, ProgressEvent::QueuedUserMessage { .. }))
.count();
assert_eq!(announced, 1, "queued message announced {announced} times");
}
#[tokio::test]
async fn the_injected_turn_says_the_previous_draft_was_never_delivered() {
let (context, session_id) = fresh_session().await;
let provider = Arc::new(FoldProvider::new());
let agent = AgentService::new_for_test(provider.clone(), context)
.await
.with_message_queue_callback(Some(one_message_queue(session_id)));
agent
.send_message_with_tools_and_callback(
session_id,
"give me the pasta recipe".to_string(),
None,
None,
None,
None,
"test",
None,
None,
)
.await
.unwrap();
let requests = provider.snapshot();
assert!(
requests.len() >= 2,
"expected a second model call after the fold, got {}",
requests.len()
);
let second = &requests[1];
let draft_kept = second
.iter()
.any(|(role, text)| role == "Assistant" && text.contains("SUPERSEDED-DRAFT"));
assert!(
draft_kept,
"the draft must stay in context so the model can fold it, got {second:?}"
);
let noted = second.iter().any(|(role, text)| {
role == "User" && text.contains(QUEUE_TEXT) && text.contains("never delivered")
});
assert!(
noted,
"the injected user turn must carry the queued request AND the \
not-delivered note, got {second:?}"
);
}
#[tokio::test]
async fn a_tool_round_with_a_queued_message_still_narrates() {
let (context, session_id) = fresh_session().await;
let provider = Arc::new(MockProviderWithTools::new());
let registry = ToolRegistry::new();
registry.register(Arc::new(MockTool));
let (progress, sink) = collector();
let agent = AgentService::new_for_test(provider, context)
.await
.with_tool_registry(Arc::new(registry))
.with_auto_approve_tools(true)
.with_message_queue_callback(Some(one_message_queue(session_id)));
agent
.send_message_with_tools_and_callback(
session_id,
"use the test tool".to_string(),
None,
None,
None,
Some(progress),
"test",
None,
None,
)
.await
.unwrap();
let events = sink.lock().unwrap().clone();
assert!(
delivered_text(&events)
.iter()
.any(|t| t.contains("I'll use the test tool.")),
"pre-tool narration must stay deliverable on a tool round: {events:?}"
);
}
#[test]
fn the_fold_note_wraps_the_queued_text_without_touching_it() {
let folded = crate::brain::agent::service::queued_fold::injected_context(
true,
"and also the smoothie one",
);
assert!(
folded.contains("never delivered"),
"the model must be told the draft was not sent: {folded}"
);
assert!(
folded.ends_with("and also the smoothie one"),
"the user's words must survive verbatim at the end: {folded}"
);
let plain =
crate::brain::agent::service::queued_fold::injected_context(false, "and also the one");
assert_eq!(
plain, "and also the one",
"a round that drafted nothing hid nothing; no note may be claimed"
);
}
#[test]
fn the_fold_site_emits_no_deliverable_draft() {
let src = include_str!("../brain/agent/service/tool_loop.rs");
let marker = "Injecting queued user message (from_buf={})";
let start = src.find(marker).expect("the text-only fold site exists");
let rest = &src[start..];
let end = rest
.find("continue;")
.expect("the site ends at its continue");
let region = &rest[..end];
assert!(
!region.contains("text: iteration_text,"),
"the deliverable emission of the superseded draft is back (#1784)"
);
assert!(
region.contains("text: String::new()"),
"the #616 reasoning reroute must stay, reasoning is not a chat message"
);
assert!(
region.contains("queued_fold::injected_context("),
"the injected turn must be built by queued_fold so the note is testable"
);
}
#[test]
fn the_tool_iteration_fold_site_is_unchanged() {
let src = include_str!("../brain/agent/service/tool_loop.rs");
let marker = "Injecting queued user message between tool iterations";
let start = src
.find(marker)
.expect("the tool-iteration fold site exists");
let rest = &src[start..];
let end = rest
.find("assistant_db_msg = message_service")
.expect("the site ends when the new assistant placeholder is made");
let region = &rest[..end];
assert!(
!region.contains("IntermediateText"),
"tool-iteration folding must not start emitting or suppressing narration here"
);
assert!(
region.contains("queued_msg.context_text.clone()"),
"the tool path injects the queued text as-is: no draft was superseded (#1784)"
);
}
fn empty_queue() -> MessageQueueCallback {
Arc::new(|_sid: Uuid| Box::pin(async { Option::<QueuedUserMessage>::None }))
}
#[tokio::test]
async fn a_fold_delivers_no_more_reply_bodies_than_a_plain_turn() {
let (context, baseline_session) = fresh_session().await;
let baseline_provider = Arc::new(FoldProvider::new());
let (baseline_progress, baseline_sink) = collector();
let baseline_agent = AgentService::new_for_test(baseline_provider.clone(), context)
.await
.with_message_queue_callback(Some(empty_queue()));
let baseline_response = baseline_agent
.send_message_with_tools_and_callback(
baseline_session,
"give me the pasta recipe".to_string(),
None,
None,
None,
Some(baseline_progress),
"test",
None,
None,
)
.await
.unwrap();
let baseline_events = baseline_sink.lock().unwrap().clone();
let baseline_bodies = delivered_text(&baseline_events);
let (folded_context, folded_session) = fresh_session().await;
let folded_provider = Arc::new(FoldProvider::new());
let (folded_progress, folded_sink) = collector();
let folded_agent = AgentService::new_for_test(folded_provider.clone(), folded_context)
.await
.with_message_queue_callback(Some(one_message_queue(folded_session)));
let folded_response = folded_agent
.send_message_with_tools_and_callback(
folded_session,
"give me the pasta recipe".to_string(),
None,
None,
None,
Some(folded_progress),
"test",
None,
None,
)
.await
.unwrap();
let folded_events = folded_sink.lock().unwrap().clone();
let folded_bodies = delivered_text(&folded_events);
assert_eq!(
baseline_provider.snapshot().len(),
1,
"the control turn itself changed shape: {} provider round(s), trace [{}]",
baseline_provider.snapshot().len(),
event_summary(&baseline_events)
);
assert_eq!(
folded_provider.snapshot().len(),
2,
"one queued message must cost exactly one extra provider round, got {}",
folded_provider.snapshot().len()
);
assert_eq!(
folded_bodies.len(),
baseline_bodies.len(),
"a queued follow-up changed how many bodies a channel can post (baseline {}, folded {}): [{}]",
baseline_bodies.len(),
folded_bodies.len(),
event_summary(&folded_events)
);
assert!(
!folded_bodies.iter().any(|t| t.contains("SUPERSEDED-DRAFT")),
"the superseded draft reached a channel as its own reply: [{}]",
event_summary(&folded_events)
);
assert!(
baseline_response.content.contains(DRAFT) && folded_response.content.contains(FINAL),
"baseline reply {:?}, folded reply {:?}",
baseline_response.content,
folded_response.content
);
}