use super::runtime::{ChildReport, ChildResultEvent};
#[derive(Clone, Debug)]
pub enum ChildResultRoute {
Progress {
agent_path: String,
status: String,
summary: Option<String>,
},
Held {
held: usize,
},
Batch {
reports: Vec<ChildReport>,
},
}
#[derive(Debug, Default)]
pub struct ChildResultRouter {
pending_reports: Vec<ChildReport>,
}
impl ChildResultRouter {
pub const MAX_REPORT_CHARS: usize = 24_000;
pub fn new() -> Self {
Self {
pending_reports: Vec::new(),
}
}
pub fn on_event(&mut self, agent_running: bool, event: ChildResultEvent) -> ChildResultRoute {
match event {
ChildResultEvent::Progress {
agent_path,
status,
summary,
} => ChildResultRoute::Progress {
agent_path,
status,
summary,
},
ChildResultEvent::Batch { reports } => {
if agent_running {
self.pending_reports.extend(reports);
ChildResultRoute::Held {
held: self.pending_reports.len(),
}
} else {
ChildResultRoute::Batch { reports }
}
}
}
}
pub fn flush_when_idle(&mut self) -> Option<Vec<ChildReport>> {
if self.pending_reports.is_empty() {
return None;
}
Some(std::mem::take(&mut self.pending_reports))
}
pub fn clamp_report(message: &str) -> (String, usize) {
let total = message.chars().count();
if total <= Self::MAX_REPORT_CHARS {
(message.to_string(), total)
} else {
(
message.chars().take(Self::MAX_REPORT_CHARS).collect(),
total,
)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn report(path: &str, message: &str) -> ChildReport {
ChildReport {
agent_path: path.to_string(),
status: "ok".to_string(),
result: Some(message.to_string()),
message: format!("[子 agent {path} 已完成]\n{message}"),
}
}
fn progress(path: &str) -> ChildResultEvent {
ChildResultEvent::Progress {
agent_path: path.to_string(),
status: "ok".to_string(),
summary: None,
}
}
fn batch(reports: Vec<ChildReport>) -> ChildResultEvent {
ChildResultEvent::Batch { reports }
}
#[test]
fn progress_is_passed_through_as_display_data() {
let mut router = ChildResultRouter::new();
match router.on_event(true, progress("root/analyze-pi")) {
ChildResultRoute::Progress {
agent_path, status, ..
} => {
assert_eq!(agent_path, "root/analyze-pi");
assert_eq!(status, "ok");
}
other => panic!("Progress must pass through, got {other:?}"),
}
assert!(router.flush_when_idle().is_none());
}
#[test]
fn batch_when_idle_is_delivered_now() {
let mut router = ChildResultRouter::new();
match router.on_event(
false,
batch(vec![
report("root/a", "report a"),
report("root/b", "report b"),
]),
) {
ChildResultRoute::Batch { reports } => {
assert_eq!(reports.len(), 2);
assert_eq!(reports[0].agent_path, "root/a");
assert_eq!(reports[1].agent_path, "root/b");
}
other => panic!("idle agent must deliver now, got {other:?}"),
}
assert!(router.flush_when_idle().is_none());
}
#[test]
fn batch_when_running_holds_and_counts() {
let mut router = ChildResultRouter::new();
match router.on_event(true, batch(vec![report("root/a", "one")])) {
ChildResultRoute::Held { held } => assert_eq!(held, 1),
other => panic!("running agent must hold, got {other:?}"),
}
match router.on_event(true, batch(vec![report("root/b", "two")])) {
ChildResultRoute::Held { held } => assert_eq!(held, 2, "count accumulates"),
other => panic!("running agent must hold, got {other:?}"),
}
let flushed = router.flush_when_idle().expect("held reports must flush");
assert_eq!(flushed.len(), 2);
assert_eq!(flushed[0].message, "[子 agent root/a 已完成]\none");
assert_eq!(flushed[1].message, "[子 agent root/b 已完成]\ntwo");
assert!(router.flush_when_idle().is_none());
}
#[test]
fn progress_between_batches_does_not_break_flush() {
let mut router = ChildResultRouter::new();
assert!(matches!(
router.on_event(true, batch(vec![report("root/a", "one")])),
ChildResultRoute::Held { .. }
));
assert!(matches!(
router.on_event(true, progress("root/x")),
ChildResultRoute::Progress { .. }
));
let flushed = router.flush_when_idle().expect("held batch must flush");
assert_eq!(flushed.len(), 1);
assert_eq!(flushed[0].agent_path, "root/a", "Progress must not be held");
}
#[test]
fn after_flush_new_batch_is_delivered_not_stale() {
let mut router = ChildResultRouter::new();
assert!(matches!(
router.on_event(true, batch(vec![report("root/first", "one")])),
ChildResultRoute::Held { .. }
));
assert!(router.flush_when_idle().is_some());
match router.on_event(false, batch(vec![report("root/second", "two")])) {
ChildResultRoute::Batch { reports } => assert_eq!(reports[0].agent_path, "root/second"),
other => panic!("expected immediate delivery, got {other:?}"),
}
assert!(router.flush_when_idle().is_none());
}
#[test]
fn flush_without_pending_is_a_noop() {
let mut router = ChildResultRouter::new();
assert!(router.flush_when_idle().is_none());
}
#[test]
fn clamp_report_caps_oversized_messages() {
let long = "x".repeat(ChildResultRouter::MAX_REPORT_CHARS + 5_000);
let (kept, total) = ChildResultRouter::clamp_report(&long);
assert_eq!(kept.chars().count(), ChildResultRouter::MAX_REPORT_CHARS);
assert_eq!(total, ChildResultRouter::MAX_REPORT_CHARS + 5_000);
}
#[test]
fn clamp_report_passes_short_messages_verbatim() {
let (kept, total) = ChildResultRouter::clamp_report("report a");
assert_eq!(kept, "report a");
assert_eq!(total, 8);
}
}