Files
openhuman/tests/raw_coverage/channels_bus_presentation_raw_coverage_e2e.rs
T

172 lines
6.2 KiB
Rust

//! Round20 focused raw coverage for channel bus and presentation paths.
//!
//! Uses debug-only seams plus in-memory web-channel events. No external
//! channel credentials, providers, or network services are required.
use std::time::Duration;
use openhuman_core::core::event_bus::{DomainEvent, EventHandler};
use openhuman_core::openhuman::agent_memory::memory_loader::MemoryCitation;
use openhuman_core::openhuman::channels::bus::ChannelInboundSubscriber;
use openhuman_core::openhuman::channels::providers::web::presentation::test_support as presentation_test_support;
use openhuman_core::openhuman::channels::providers::web::{
subscribe_web_channel_events, test_support as web_test_support,
};
use serde_json::json;
use tokio::time::timeout;
#[tokio::test]
async fn presentation_segments_text_and_delivers_single_bubble_with_citations() {
assert_eq!(
presentation_test_support::segment_for_delivery_for_test(" Hello there. "),
vec!["Hello there.".to_string()]
);
assert_eq!(
presentation_test_support::segment_for_delivery_for_test(
"Here is code:\n\n```rust\nfn main() {}\n```\n\nKeep it together."
)
.len(),
1
);
assert!(presentation_test_support::is_structured_content_for_test(
"# Heading\n- First\n- Second"
));
assert_eq!(presentation_test_support::segment_delay_for_test(""), 500);
let citation = MemoryCitation {
id: "mem-1".to_string(),
key: "project".to_string(),
namespace: Some("test".to_string()),
score: Some(0.91),
timestamp: "2026-05-29T00:00:00Z".to_string(),
snippet: "OpenHuman channel presentation citation.".to_string(),
};
let mut rx = subscribe_web_channel_events();
presentation_test_support::deliver_response_for_test(
"round20-client",
"round20-thread",
"round20-single",
"Short final answer.",
"",
std::slice::from_ref(&citation),
)
.await;
// `subscribe_web_channel_events` is a process-global bus shared with the
// other presentation tests, which run concurrently and emit their own
// `chat_segment`/`chat_done` events. Filter to this delivery's request id so
// a sibling's segment event can't be mistaken for our single bubble.
let event = timeout(Duration::from_secs(5), async {
loop {
let event = rx.recv().await.expect("single bubble event");
if event.request_id == "round20-single" {
break event;
}
}
})
.await
.expect("single bubble event timeout");
assert_eq!(event.event, "chat_done");
assert_eq!(event.full_response.as_deref(), Some("Short final answer."));
assert_eq!(event.reaction_emoji, None);
assert!(event
.citations
.expect("citations")
.to_string()
.contains("mem-1"));
}
#[tokio::test]
async fn presentation_delivers_segment_events_then_deduping_done_event() {
let response = [
"First paragraph has enough natural language content to stand alone as a separate chat bubble.",
"Second paragraph also contains enough prose to exercise segmented delivery and delay calculation.",
"Third paragraph ensures the final chat_done event carries the complete response for deduplication.",
]
.join("\n\n");
let segments = presentation_test_support::segment_for_delivery_for_test(&response);
assert!(segments.len() >= 2, "expected segmented delivery");
let mut rx = subscribe_web_channel_events();
presentation_test_support::deliver_response_for_test(
"round20-client",
"round20-thread",
"round20-segmented",
&response,
"",
&[],
)
.await;
let mut seen_segments = 0_u32;
let final_event = timeout(Duration::from_secs(10), async {
loop {
let event = rx.recv().await.expect("presentation event");
if event.request_id != "round20-segmented" {
continue;
}
match event.event.as_str() {
"chat_segment" => {
assert_eq!(event.segment_total, Some(segments.len() as u32));
assert_eq!(event.segment_index, Some(seen_segments));
assert!(event.full_response.as_deref().unwrap_or("").len() >= 40);
seen_segments += 1;
}
"chat_done" => break event,
other => panic!("unexpected presentation event {other}"),
}
}
})
.await
.expect("segmented delivery timeout");
assert_eq!(seen_segments, segments.len() as u32);
assert_eq!(final_event.segment_total, Some(segments.len() as u32));
assert_eq!(
final_event.full_response.as_deref(),
Some(response.as_str())
);
}
#[tokio::test]
async fn channel_inbound_subscriber_handles_forced_web_error_without_external_services() {
let subscriber = ChannelInboundSubscriber::new();
assert_eq!(subscriber.name(), "channel::inbound_handler");
assert_eq!(subscriber.domains(), Some(&["channel"][..]));
web_test_support::set_forced_run_chat_task_error_for_test(Some(
"openrouter API error (429 Too Many Requests): Retry-After: 3",
))
.await;
timeout(
Duration::from_secs(10),
subscriber.handle(&DomainEvent::ChannelInboundMessage {
event_name: "discord:message".to_string(),
channel: "discord:guild-1".to_string(),
message: "Please summarize the thread.".to_string(),
sender: Some("user-a".to_string()),
reply_target: Some("channel-a".to_string()),
thread_ts: Some("1700000000.001".to_string()),
raw_data: json!({ "round": 20 }),
}),
)
.await
.expect("inbound subscriber should finish after forced web error");
web_test_support::set_forced_run_chat_task_error_for_test(None).await;
}
#[tokio::test]
async fn channel_inbound_subscriber_ignores_unrelated_events() {
timeout(
Duration::from_secs(2),
ChannelInboundSubscriber::default().handle(&DomainEvent::SystemStartup {
component: "round20".to_string(),
}),
)
.await
.expect("unrelated event should return immediately");
}