huskies: merge 736_story_drain_and_prepend_buffered_status_events_on_the_user_s_next_agent_message
This commit is contained in:
@@ -1,7 +1,5 @@
|
||||
//! [`StatusEventBuffer`] — bounded, per-instance accumulator over a
|
||||
//! [`StatusBroadcaster`].
|
||||
// Infrastructure module: public items are not yet wired to production call
|
||||
// sites but are exercised by the unit tests below.
|
||||
#![allow(dead_code)]
|
||||
//!
|
||||
//! A `StatusEventBuffer` subscribes to a [`StatusBroadcaster`] on construction
|
||||
@@ -162,6 +160,40 @@ impl Drop for StatusEventBuffer {
|
||||
}
|
||||
}
|
||||
|
||||
// ── Formatting ────────────────────────────────────────────────────────────────
|
||||
|
||||
/// Format a [`BufferedItem`] slice into a `<recent-events>…</recent-events>` block
|
||||
/// ready to prepend to an agent message.
|
||||
///
|
||||
/// Each [`BufferedItem::Event`] is rendered with
|
||||
/// [`format_status_event`](crate::service::status::format::format_status_event).
|
||||
/// A [`BufferedItem::Truncated`] entry produces a note about how many older
|
||||
/// events were omitted.
|
||||
///
|
||||
/// Returns `None` when `items` is empty so callers can skip the prepend
|
||||
/// entirely.
|
||||
pub fn format_buffered_items(items: &[BufferedItem]) -> Option<String> {
|
||||
if items.is_empty() {
|
||||
return None;
|
||||
}
|
||||
use crate::service::status::format::format_status_event;
|
||||
let mut lines = Vec::with_capacity(items.len());
|
||||
for item in items {
|
||||
match item {
|
||||
BufferedItem::Truncated(n) => {
|
||||
lines.push(format!("[{n} older event(s) were omitted due to overflow]"));
|
||||
}
|
||||
BufferedItem::Event(event) => {
|
||||
lines.push(format_status_event(event));
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(format!(
|
||||
"<recent-events>\n{}\n</recent-events>",
|
||||
lines.join("\n")
|
||||
))
|
||||
}
|
||||
|
||||
// ── Tests ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -353,4 +385,55 @@ mod tests {
|
||||
"clear should discard events and truncation marker"
|
||||
);
|
||||
}
|
||||
|
||||
// ── format_buffered_items ─────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn format_empty_slice_returns_none() {
|
||||
assert!(format_buffered_items(&[]).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn format_events_produces_recent_events_block() {
|
||||
let items = vec![
|
||||
BufferedItem::Event(StatusEvent::MergeFailure {
|
||||
story_id: "42_story_foo".to_string(),
|
||||
story_name: Some("Foo".to_string()),
|
||||
reason: "conflict".to_string(),
|
||||
}),
|
||||
BufferedItem::Event(StatusEvent::StoryBlocked {
|
||||
story_id: "7_story_bar".to_string(),
|
||||
story_name: None,
|
||||
reason: "retry limit".to_string(),
|
||||
}),
|
||||
];
|
||||
let block = format_buffered_items(&items).expect("non-empty slice must produce a block");
|
||||
assert!(
|
||||
block.starts_with("<recent-events>\n"),
|
||||
"must open with <recent-events>: {block}"
|
||||
);
|
||||
assert!(
|
||||
block.ends_with("\n</recent-events>"),
|
||||
"must close with </recent-events>: {block}"
|
||||
);
|
||||
assert!(block.contains("conflict"), "must include event content");
|
||||
assert!(block.contains("retry limit"), "must include event content");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn format_truncation_marker_is_included() {
|
||||
let items = vec![
|
||||
BufferedItem::Truncated(3),
|
||||
BufferedItem::Event(StatusEvent::MergeFailure {
|
||||
story_id: "1_story_x".to_string(),
|
||||
story_name: None,
|
||||
reason: "test".to_string(),
|
||||
}),
|
||||
];
|
||||
let block = format_buffered_items(&items).unwrap();
|
||||
assert!(
|
||||
block.contains("3 older event(s) were omitted"),
|
||||
"truncation note must appear in block: {block}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user