use aethershell::ai::a2ui::{
A2UIChannel, A2UIEvent, A2UIEventType, EventPriority, NotificationLevel, PromptResponse,
PromptType, RenderContent, A2UI_CHANNEL,
};
use aethershell::{builtins::call, env::Env, value::Value};
use std::collections::BTreeMap;
use uuid::Uuid;
#[test]
fn test_notification_level_parsing() {
assert_eq!(
"info".parse::<NotificationLevel>().unwrap(),
NotificationLevel::Info
);
assert_eq!(
"success".parse::<NotificationLevel>().unwrap(),
NotificationLevel::Success
);
assert_eq!(
"warning".parse::<NotificationLevel>().unwrap(),
NotificationLevel::Warning
);
assert_eq!(
"warn".parse::<NotificationLevel>().unwrap(),
NotificationLevel::Warning
);
assert_eq!(
"error".parse::<NotificationLevel>().unwrap(),
NotificationLevel::Error
);
assert_eq!(
"INFO".parse::<NotificationLevel>().unwrap(),
NotificationLevel::Info
);
assert!("invalid".parse::<NotificationLevel>().is_err());
}
#[test]
fn test_event_priority_ordering() {
assert!(EventPriority::Low < EventPriority::Normal);
assert!(EventPriority::Normal < EventPriority::High);
assert!(EventPriority::High < EventPriority::Critical);
}
#[test]
fn test_event_creation_notify() {
let event = A2UIEvent::notify("test_agent", "Hello World");
assert_eq!(event.source, "test_agent");
match event.event_type {
A2UIEventType::Notify {
message,
level,
duration_ms,
} => {
assert_eq!(message, "Hello World");
assert_eq!(level, NotificationLevel::Info);
assert!(duration_ms.is_none());
}
_ => panic!("Expected Notify event"),
}
}
#[test]
fn test_event_creation_with_level() {
let event = A2UIEvent::notify_level("agent", "Error!", NotificationLevel::Error);
match event.event_type {
A2UIEventType::Notify { level, .. } => {
assert_eq!(level, NotificationLevel::Error);
}
_ => panic!("Expected Notify event"),
}
}
#[test]
fn test_event_priority() {
let event = A2UIEvent::notify("agent", "test").with_priority(EventPriority::Critical);
assert_eq!(event.priority, EventPriority::Critical);
}
#[test]
fn test_progress_event() {
let progress_id = Uuid::new_v4();
let event = A2UIEvent::progress("agent", progress_id, "Downloading", 50, 100);
match event.event_type {
A2UIEventType::Progress {
id,
label,
current,
total,
message,
} => {
assert_eq!(id, progress_id);
assert_eq!(label, "Downloading");
assert_eq!(current, 50);
assert_eq!(total, 100);
assert!(message.is_none());
}
_ => panic!("Expected Progress event"),
}
}
#[test]
fn test_toast_event() {
let event = A2UIEvent::toast("agent", "Quick message", NotificationLevel::Success, 3000);
match event.event_type {
A2UIEventType::Toast {
message,
level,
duration_ms,
} => {
assert_eq!(message, "Quick message");
assert_eq!(level, NotificationLevel::Success);
assert_eq!(duration_ms, 3000);
}
_ => panic!("Expected Toast event"),
}
}
#[test]
fn test_agent_lifecycle_events() {
let started = A2UIEvent::agent_started("system", "agent1");
match started.event_type {
A2UIEventType::AgentStarted { agent_id, task } => {
assert_eq!(agent_id, "agent1");
assert!(task.is_none());
}
_ => panic!("Expected AgentStarted event"),
}
let thinking = A2UIEvent::agent_thinking("system", "agent1", "Analyzing...", 1);
match thinking.event_type {
A2UIEventType::AgentThinking {
agent_id,
thought,
step,
} => {
assert_eq!(agent_id, "agent1");
assert_eq!(thought, "Analyzing...");
assert_eq!(step, 1);
}
_ => panic!("Expected AgentThinking event"),
}
let completed = A2UIEvent::agent_completed("system", "agent1", true);
match completed.event_type {
A2UIEventType::AgentCompleted {
agent_id,
result,
success,
} => {
assert_eq!(agent_id, "agent1");
assert!(result.is_none());
assert!(success);
}
_ => panic!("Expected AgentCompleted event"),
}
}
#[test]
fn test_status_event() {
let event = A2UIEvent::status("agent", "Processing...");
match event.event_type {
A2UIEventType::Status { text, section } => {
assert_eq!(text, "Processing...");
assert!(section.is_none());
}
_ => panic!("Expected Status event"),
}
}
#[test]
fn test_channel_send_receive() {
let channel = A2UIChannel::new();
channel.notify("agent1", "Test 1").unwrap();
channel.notify("agent2", "Test 2").unwrap();
assert_eq!(channel.event_count().unwrap(), 2);
let events = channel.receive_all().unwrap();
assert_eq!(events.len(), 2);
assert_eq!(channel.event_count().unwrap(), 0);
}
#[test]
fn test_channel_capacity() {
let channel = A2UIChannel::with_capacity(3);
for i in 0..5 {
channel.notify("agent", &format!("Message {}", i)).unwrap();
}
assert_eq!(channel.event_count().unwrap(), 3);
}
#[test]
fn test_channel_peek() {
let channel = A2UIChannel::new();
channel.notify("agent", "Test").unwrap();
let peeked = channel.peek().unwrap();
assert_eq!(peeked.len(), 1);
assert_eq!(channel.event_count().unwrap(), 1);
let received = channel.receive_all().unwrap();
assert_eq!(received.len(), 1);
assert_eq!(channel.event_count().unwrap(), 0);
}
#[test]
fn test_channel_has_events() {
let channel = A2UIChannel::new();
assert!(!channel.has_events().unwrap());
channel.notify("agent", "Test").unwrap();
assert!(channel.has_events().unwrap());
}
#[test]
fn test_channel_clear() {
let channel = A2UIChannel::new();
channel.notify("agent", "Test 1").unwrap();
channel.notify("agent", "Test 2").unwrap();
channel.clear().unwrap();
assert_eq!(channel.event_count().unwrap(), 0);
}
#[test]
fn test_channel_receive_limited() {
let channel = A2UIChannel::new();
for i in 0..5 {
channel.notify("agent", &format!("Message {}", i)).unwrap();
}
let events = channel.receive(2).unwrap();
assert_eq!(events.len(), 2);
assert_eq!(channel.event_count().unwrap(), 3);
}
#[test]
fn test_prompt_text_event() {
let (event, prompt_id) = A2UIEvent::prompt_text("agent", "Enter name:");
match event.event_type {
A2UIEventType::Prompt {
id,
message,
prompt_type,
} => {
assert_eq!(id, prompt_id);
assert_eq!(message, "Enter name:");
match prompt_type {
PromptType::Text { placeholder } => assert!(placeholder.is_none()),
_ => panic!("Expected Text prompt"),
}
}
_ => panic!("Expected Prompt event"),
}
}
#[test]
fn test_prompt_confirm_event() {
let (event, prompt_id) = A2UIEvent::prompt_confirm("agent", "Continue?");
match event.event_type {
A2UIEventType::Prompt {
id,
message,
prompt_type,
} => {
assert_eq!(id, prompt_id);
assert_eq!(message, "Continue?");
match prompt_type {
PromptType::Confirm { default } => assert!(default.is_none()),
_ => panic!("Expected Confirm prompt"),
}
}
_ => panic!("Expected Prompt event"),
}
}
#[test]
fn test_prompt_select_event() {
let options = vec!["A".to_string(), "B".to_string(), "C".to_string()];
let (event, prompt_id) = A2UIEvent::prompt_select("agent", "Choose:", options.clone());
match event.event_type {
A2UIEventType::Prompt {
id,
message,
prompt_type,
} => {
assert_eq!(id, prompt_id);
assert_eq!(message, "Choose:");
match prompt_type {
PromptType::Select {
options: opts,
default,
} => {
assert_eq!(opts, options);
assert!(default.is_none());
}
_ => panic!("Expected Select prompt"),
}
}
_ => panic!("Expected Prompt event"),
}
}
#[test]
fn test_prompt_response_submission() {
let channel = A2UIChannel::new();
let prompt_id = Uuid::new_v4();
channel
.submit_response(prompt_id, PromptResponse::Text("user input".to_string()))
.unwrap();
assert!(channel.has_response(prompt_id).unwrap());
}
#[test]
fn test_render_text() {
let channel = A2UIChannel::new();
channel
.render("agent", RenderContent::Text("Plain text".to_string()))
.unwrap();
let events = channel.receive_all().unwrap();
match &events[0].event_type {
A2UIEventType::Render { content, .. } => match content {
RenderContent::Text(t) => assert_eq!(t, "Plain text"),
_ => panic!("Expected Text content"),
},
_ => panic!("Expected Render event"),
}
}
#[test]
fn test_render_markdown() {
let channel = A2UIChannel::new();
channel
.render("agent", RenderContent::Markdown("# Header".to_string()))
.unwrap();
assert_eq!(channel.event_count().unwrap(), 1);
}
#[test]
fn test_render_table() {
let channel = A2UIChannel::new();
channel
.render(
"agent",
RenderContent::Table {
headers: vec!["Name".to_string(), "Value".to_string()],
rows: vec![vec!["foo".to_string(), "bar".to_string()]],
},
)
.unwrap();
let events = channel.receive_all().unwrap();
match &events[0].event_type {
A2UIEventType::Render { content, .. } => match content {
RenderContent::Table { headers, rows } => {
assert_eq!(headers.len(), 2);
assert_eq!(rows.len(), 1);
}
_ => panic!("Expected Table content"),
},
_ => panic!("Expected Render event"),
}
}
#[test]
fn test_render_code() {
let channel = A2UIChannel::new();
channel
.render(
"agent",
RenderContent::Code {
language: "rust".to_string(),
content: "fn main() {}".to_string(),
},
)
.unwrap();
assert_eq!(channel.event_count().unwrap(), 1);
}
#[test]
fn test_render_thinking() {
let channel = A2UIChannel::new();
channel
.render(
"agent",
RenderContent::Thinking {
steps: vec![
"Analyzing input".to_string(),
"Generating response".to_string(),
],
final_answer: Some("Done!".to_string()),
},
)
.unwrap();
let events = channel.receive_all().unwrap();
match &events[0].event_type {
A2UIEventType::Render { content, .. } => match content {
RenderContent::Thinking {
steps,
final_answer,
} => {
assert_eq!(steps.len(), 2);
assert_eq!(final_answer.as_deref(), Some("Done!"));
}
_ => panic!("Expected Thinking content"),
},
_ => panic!("Expected Render event"),
}
}
#[test]
fn test_builtin_a2ui_notify() {
A2UI_CHANNEL.clear().unwrap();
let mut env = Env::new();
let result = call(
"a2ui_notify",
vec![Value::Str("Test notification".to_string())],
&mut env,
)
.unwrap();
if let Value::Record(r) = result {
assert_eq!(r.get("sent"), Some(&Value::Bool(true)));
} else {
panic!("Expected Record result");
}
A2UI_CHANNEL.clear().unwrap();
}
#[test]
fn test_builtin_a2ui_notify_with_level() {
A2UI_CHANNEL.clear().unwrap();
let mut env = Env::new();
let mut opts = BTreeMap::new();
opts.insert("level".to_string(), Value::Str("error".to_string()));
let result = call(
"a2ui_notify",
vec![Value::Str("Error message".to_string()), Value::Record(opts)],
&mut env,
)
.unwrap();
if let Value::Record(r) = result {
assert_eq!(r.get("sent"), Some(&Value::Bool(true)));
} else {
panic!("Expected Record result");
}
A2UI_CHANNEL.clear().unwrap();
}
#[test]
fn test_builtin_a2ui_toast() {
A2UI_CHANNEL.clear().unwrap();
let mut env = Env::new();
let result = call(
"a2ui_toast",
vec![
Value::Str("Quick toast".to_string()),
Value::Str("success".to_string()),
Value::Int(5000),
],
&mut env,
)
.unwrap();
assert_eq!(result, Value::Bool(true));
let events = A2UI_CHANNEL.receive_all().unwrap();
let toast_event = events.iter().find(|e| {
matches!(&e.event_type, A2UIEventType::Toast { message, .. } if message == "Quick toast")
});
if let Some(event) = toast_event {
if let A2UIEventType::Toast {
message,
level,
duration_ms,
} = &event.event_type
{
assert_eq!(message, "Quick toast");
assert_eq!(*level, NotificationLevel::Success);
assert_eq!(*duration_ms, 5000);
}
}
}
#[test]
fn test_builtin_a2ui_progress() {
A2UI_CHANNEL.clear().unwrap();
let mut env = Env::new();
let result = call(
"a2ui_progress",
vec![
Value::Str("Downloading".to_string()),
Value::Int(50),
Value::Int(100),
],
&mut env,
)
.unwrap();
if let Value::Record(r) = result {
assert!(r.contains_key("progress_id"));
assert_eq!(r.get("current"), Some(&Value::Int(50)));
assert_eq!(r.get("total"), Some(&Value::Int(100)));
} else {
panic!("Expected Record result");
}
A2UI_CHANNEL.clear().unwrap();
}
#[test]
fn test_builtin_a2ui_status() {
A2UI_CHANNEL.clear().unwrap();
let mut env = Env::new();
let result = call(
"a2ui_status",
vec![Value::Str("Processing...".to_string())],
&mut env,
)
.unwrap();
assert_eq!(result, Value::Bool(true));
let events = A2UI_CHANNEL.receive_all().unwrap();
let status_event = events.iter().find(
|e| matches!(&e.event_type, A2UIEventType::Status { text, .. } if text == "Processing..."),
);
if let Some(event) = status_event {
if let A2UIEventType::Status { text, .. } = &event.event_type {
assert_eq!(text, "Processing...");
}
}
}
#[test]
fn test_builtin_a2ui_render() {
A2UI_CHANNEL.clear().unwrap();
let mut env = Env::new();
let result = call(
"a2ui_render",
vec![Value::Str("Rendered content".to_string())],
&mut env,
)
.unwrap();
assert_eq!(result, Value::Bool(true));
A2UI_CHANNEL.clear().unwrap();
}
#[test]
fn test_builtin_a2ui_render_table() {
A2UI_CHANNEL.clear().unwrap();
let mut env = Env::new();
let headers = Value::Array(vec![
Value::Str("Name".to_string()),
Value::Str("Age".to_string()),
]);
let rows = Value::Array(vec![Value::Array(vec![
Value::Str("Alice".to_string()),
Value::Str("30".to_string()),
])]);
let result = call("a2ui_render_table", vec![headers, rows], &mut env).unwrap();
assert_eq!(result, Value::Bool(true));
A2UI_CHANNEL.clear().unwrap();
}
#[test]
fn test_builtin_a2ui_render_code() {
A2UI_CHANNEL.clear().unwrap();
let mut env = Env::new();
let result = call(
"a2ui_render_code",
vec![
Value::Str("rust".to_string()),
Value::Str("fn main() { println!(\"Hello\"); }".to_string()),
],
&mut env,
)
.unwrap();
assert_eq!(result, Value::Bool(true));
A2UI_CHANNEL.clear().unwrap();
}
#[test]
fn test_builtin_a2ui_clear() {
A2UI_CHANNEL.notify("test", "Message 1").unwrap();
A2UI_CHANNEL.notify("test", "Message 2").unwrap();
let mut env = Env::new();
let result = call("a2ui_clear", vec![], &mut env).unwrap();
assert_eq!(result, Value::Bool(true));
assert_eq!(A2UI_CHANNEL.event_count().unwrap(), 0);
}
#[test]
fn test_builtin_a2ui_agent_started() {
A2UI_CHANNEL.clear().unwrap();
let mut env = Env::new();
let result = call(
"a2ui_agent_started",
vec![
Value::Str("agent1".to_string()),
Value::Str("Processing task".to_string()),
],
&mut env,
)
.unwrap();
assert_eq!(result, Value::Bool(true));
let events = A2UI_CHANNEL.receive_all().unwrap();
let started_event = events.iter().find(|e| {
matches!(&e.event_type, A2UIEventType::AgentStarted { agent_id, .. } if agent_id == "agent1")
});
if let Some(event) = started_event {
if let A2UIEventType::AgentStarted { agent_id, task } = &event.event_type {
assert_eq!(agent_id, "agent1");
assert_eq!(task.as_deref(), Some("Processing task"));
}
}
}
#[test]
fn test_builtin_a2ui_agent_completed() {
A2UI_CHANNEL.clear().unwrap();
let mut env = Env::new();
let result = call(
"a2ui_agent_completed",
vec![
Value::Str("agent1".to_string()),
Value::Bool(true),
Value::Str("Task completed successfully".to_string()),
],
&mut env,
)
.unwrap();
assert_eq!(result, Value::Bool(true));
let events = A2UI_CHANNEL.receive_all().unwrap();
let completed_event = events.iter().find(|e| {
matches!(&e.event_type, A2UIEventType::AgentCompleted { agent_id, .. } if agent_id == "agent1")
});
if let Some(event) = completed_event {
if let A2UIEventType::AgentCompleted {
agent_id,
result,
success,
} = &event.event_type
{
assert_eq!(agent_id, "agent1");
assert!(*success);
assert!(result.is_some());
}
}
}
#[test]
fn test_builtin_a2ui_agent_thinking() {
A2UI_CHANNEL.clear().unwrap();
let mut env = Env::new();
let result = call(
"a2ui_agent_thinking",
vec![
Value::Str("agent1".to_string()),
Value::Str("Analyzing the problem...".to_string()),
Value::Int(1),
],
&mut env,
)
.unwrap();
assert_eq!(result, Value::Bool(true));
let events = A2UI_CHANNEL.receive_all().unwrap();
let thinking_event = events.iter().find(|e| {
matches!(&e.event_type, A2UIEventType::AgentThinking { agent_id, .. } if agent_id == "agent1")
});
if let Some(event) = thinking_event {
if let A2UIEventType::AgentThinking {
agent_id,
thought,
step,
} = &event.event_type
{
assert_eq!(agent_id, "agent1");
assert_eq!(thought, "Analyzing the problem...");
assert_eq!(*step, 1);
}
}
}
#[test]
fn test_builtin_aliases() {
A2UI_CHANNEL.clear().unwrap();
let mut env = Env::new();
let result = call(
"notify_user",
vec![Value::Str("Test".to_string())],
&mut env,
);
assert!(result.is_ok());
let result = call(
"toast",
vec![
Value::Str("Toast".to_string()),
Value::Str("info".to_string()),
Value::Int(3000),
],
&mut env,
);
assert!(result.is_ok());
let result = call(
"status_bar",
vec![Value::Str("Status".to_string())],
&mut env,
);
assert!(result.is_ok());
let result = call(
"render_content",
vec![Value::Str("Content".to_string())],
&mut env,
);
assert!(result.is_ok());
A2UI_CHANNEL.clear().unwrap();
}
#[test]
fn test_event_serialization() {
let event = A2UIEvent::notify("agent", "Test message");
let json = serde_json::to_string(&event).unwrap();
assert!(json.contains("Test message"));
let deserialized: A2UIEvent = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized.source, "agent");
}
#[test]
fn test_render_content_serialization() {
let content = RenderContent::Table {
headers: vec!["A".to_string(), "B".to_string()],
rows: vec![vec!["1".to_string(), "2".to_string()]],
};
let json = serde_json::to_string(&content).unwrap();
let deserialized: RenderContent = serde_json::from_str(&json).unwrap();
match deserialized {
RenderContent::Table { headers, rows } => {
assert_eq!(headers.len(), 2);
assert_eq!(rows.len(), 1);
}
_ => panic!("Expected Table content"),
}
}
#[test]
fn test_channel_thread_safety() {
use std::sync::Arc;
use std::thread;
let channel = Arc::new(A2UIChannel::new());
let mut handles = vec![];
for i in 0..10 {
let ch = Arc::clone(&channel);
handles.push(thread::spawn(move || {
ch.notify("agent", &format!("Message {}", i)).unwrap();
}));
}
for h in handles {
h.join().unwrap();
}
assert_eq!(channel.event_count().unwrap(), 10);
}
#[test]
fn test_global_channel_thread_safety() {
use std::thread;
A2UI_CHANNEL.clear().unwrap();
let mut handles = vec![];
for i in 0..5 {
handles.push(thread::spawn(move || {
aethershell::ai::a2ui::notify("thread", &format!("Message {}", i)).unwrap();
}));
}
for h in handles {
h.join().unwrap();
}
let count = A2UI_CHANNEL.event_count().unwrap();
assert!(count > 0);
A2UI_CHANNEL.clear().unwrap();
}