mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-27 21:08:00 +00:00
172 lines
6.2 KiB
Rust
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");
|
|
}
|