#![allow(missing_docs)]
use serde_json::{json, Value};
use tirea_protocol_ag_ui::{
convert_agui_messages, Message, Role, RunAgentInput, Tool, ToolExecutionLocation,
};
#[test]
fn frontend_tools_filters_backend_tools() {
let request = RunAgentInput {
tools: vec![
Tool::backend("search", "backend"),
Tool::frontend("copy_to_clipboard", "frontend"),
Tool {
name: "open_modal".to_string(),
description: "frontend by default marker".to_string(),
parameters: None,
execute: ToolExecutionLocation::Frontend,
},
],
..RunAgentInput::new("thread_1", "run_1")
};
let frontend: Vec<String> = request
.frontend_tools()
.iter()
.map(|tool| tool.name.clone())
.collect();
assert_eq!(frontend, vec!["copy_to_clipboard", "open_modal"]);
}
#[test]
fn interaction_responses_ignore_non_tool_messages_and_tool_without_id() {
let request = RunAgentInput::new("thread_1", "run_1")
.with_message(Message::user("hello"))
.with_message(Message::assistant("ignored"))
.with_message(Message {
role: Role::Tool,
content: "true".to_string(),
id: None,
tool_call_id: None,
})
.with_message(Message::tool("false", "interaction_1"));
let responses = request.interaction_responses();
assert_eq!(responses.len(), 1);
assert_eq!(responses[0].target_id, "interaction_1");
assert_eq!(responses[0].result, Value::Bool(false));
assert!(request.has_any_interaction_responses());
assert!(request.has_any_suspension_decisions());
let decisions = request.suspension_decisions();
assert_eq!(decisions.len(), 1);
assert_eq!(decisions[0].target_id, "interaction_1");
let run_request = request.into_runtime_run_request("agent".to_string());
assert_eq!(run_request.initial_decisions.len(), 1);
assert_eq!(run_request.initial_decisions[0].target_id, "interaction_1");
}
#[test]
fn has_user_input_only_counts_non_empty_user_messages() {
let no_user = RunAgentInput::new("thread_1", "run_1")
.with_message(Message::assistant("ignored"))
.with_message(Message::tool("true", "interaction_1"));
assert!(!no_user.has_user_input());
let empty_user = RunAgentInput::new("thread_1", "run_1").with_message(Message::user(" "));
assert!(!empty_user.has_user_input());
let with_user = RunAgentInput::new("thread_1", "run_1")
.with_message(Message::user("hello"))
.with_message(Message::tool("true", "interaction_1"));
assert!(with_user.has_user_input());
}
#[test]
fn interaction_response_non_json_content_is_preserved_as_string() {
let request =
RunAgentInput::new("thread_1", "run_1").with_message(Message::tool("approved", "i1"));
let responses = request.interaction_responses();
assert_eq!(responses.len(), 1);
assert_eq!(responses[0].result, Value::String("approved".to_string()));
}
#[test]
fn approved_and_denied_ids_follow_runtime_interaction_semantics() {
let request = RunAgentInput::new("thread_1", "run_1")
.with_message(Message::tool("true", "approved_bool"))
.with_message(Message::tool(r#"{"allowed": true}"#, "approved_object"))
.with_message(Message::tool(r#"{"approved": false}"#, "denied_object"))
.with_message(Message::tool(
r#"{"status":"cancelled"}"#,
"cancelled_object",
))
.with_message(Message::tool("no", "denied_string"))
.with_message(Message::tool("cancelled", "cancelled_string"))
.with_message(Message::tool("maybe", "neither"));
assert_eq!(
request.approved_target_ids(),
vec!["approved_bool", "approved_object", "neither"]
);
assert_eq!(
request.denied_target_ids(),
vec![
"denied_object",
"cancelled_object",
"denied_string",
"cancelled_string"
]
);
}
#[test]
fn conflicting_results_for_same_target_id_use_last_result() {
let request = RunAgentInput::new("thread_1", "run_1")
.with_message(Message::tool("true", "same_id"))
.with_message(Message::tool("false", "same_id"));
assert!(request.approved_target_ids().is_empty());
assert_eq!(request.denied_target_ids(), vec!["same_id"]);
}
#[test]
fn suspension_decisions_preserve_last_write_order() {
let request = RunAgentInput::new("thread_1", "run_1")
.with_message(Message::tool("true", "perm_1"))
.with_message(Message::tool("true", "perm_2"))
.with_message(Message::tool("false", "perm_1"));
let run_request = request.into_runtime_run_request("agent".to_string());
let decision_targets: Vec<&str> = run_request
.initial_decisions
.iter()
.map(|decision| decision.target_id.as_str())
.collect();
assert_eq!(
decision_targets,
vec!["perm_2", "perm_1"],
"last-write ordering should be stable after dedup"
);
}
#[test]
fn interaction_responses_filter_to_pending_ids_when_state_exists() {
let request = RunAgentInput::new("thread_1", "run_1")
.with_state(json!({
"__tool_call_scope": {
"call_pending": {
"suspended_call": {
"call_id": "call_pending",
"tool_name": "confirm",
"suspension": { "id": "call_pending", "action": "confirm" },
"arguments": {},
"pending": {
"id": "call_pending",
"name": "confirm",
"arguments": {}
},
"resume_mode": "replay_tool_call"
}
}
}
}))
.with_message(Message::tool("true", "call_pending"))
.with_message(Message::tool("true", "unrelated_tool_result"));
let responses = request.interaction_responses();
assert_eq!(responses.len(), 1);
assert_eq!(responses[0].target_id, "call_pending");
}
#[test]
fn interaction_response_preserves_json_values() {
let payload = json!({
"approved": true,
"meta": {
"source": "client",
"reason": "user_confirmed"
}
});
let request = RunAgentInput::new("thread_1", "run_1")
.with_message(Message::tool(payload.to_string(), "interaction_1"));
let responses = request.interaction_responses();
assert_eq!(responses.len(), 1);
assert_eq!(responses[0].result, payload);
}
#[test]
fn run_request_deserializes_forwarded_props_aliases() {
let camel: RunAgentInput = serde_json::from_value(json!({
"threadId": "t1",
"runId": "r1",
"messages": [],
"forwardedProps": { "foo": 1 }
}))
.unwrap();
assert_eq!(camel.forwarded_props, Some(json!({ "foo": 1 })));
let snake: RunAgentInput = serde_json::from_value(json!({
"threadId": "t1",
"runId": "r1",
"messages": [],
"forwarded_props": { "bar": 2 }
}))
.unwrap();
assert_eq!(snake.forwarded_props, Some(json!({ "bar": 2 })));
}
#[test]
fn run_request_forwards_parent_thread_id_into_runtime_request() {
let req: RunAgentInput = serde_json::from_value(json!({
"threadId": "t1",
"runId": "r1",
"parentThreadId": "parent-thread-1",
"messages": [{ "role": "user", "content": "hi" }],
"tools": []
}))
.unwrap();
let runtime = req.into_runtime_run_request("agent".to_string());
assert_eq!(runtime.parent_thread_id.as_deref(), Some("parent-thread-1"));
}
#[test]
fn convert_agui_messages_filters_activity_and_reasoning() {
let input = vec![
Message::user("hello"),
Message::activity("status"),
Message::reasoning("thought"),
];
let messages = convert_agui_messages(&input);
assert_eq!(messages.len(), 1);
assert_eq!(messages[0].content, "hello");
}