mod common;
use common::*;
use std::sync::Arc;
use phi_agent::bridge::messages::PROTOCOL_VERSION;
use phi_agent::bridge::server::ProtocolServer;
use serde_json::{Value, json};
#[tokio::test]
async fn test_full_run_returns_events() {
let mock = Arc::new(MockLlmClient::new());
let server = build_server(mock);
let sid = server.create_session(None).await.0;
let mut event_rx = server.subscribe_events();
let server_clone = server.clone();
tokio::spawn(async move {
let _ = server_clone.run_turn(&sid, "hello", |_event| Ok(())).await;
});
let events = collect_events(&mut event_rx).await;
assert!(!events.is_empty(), "should receive at least one event");
assert!(events.contains(&"run_finished".to_string()), "should finish: {events:?}");
}
#[tokio::test]
async fn test_create_session_and_subscribe() {
let mock = Arc::new(MockLlmClient::new());
let server = build_server(mock);
let (sid, ext) = server.create_session(Some("my-session".to_string())).await;
assert_eq!(ext.as_deref(), Some("my-session"));
assert!(sid.id > 0);
}
#[test]
fn test_protocol_version_is_1() {
assert_eq!(PROTOCOL_VERSION, 1);
}
#[tokio::test]
async fn test_br_04_empty_slot_returns_error() {
let mock = Arc::new(MockLlmClient::new());
mock.set_tool_call("test_tool", &json!({"arg": "value"})).await;
let server = build_server(mock.clone());
server
.register_tool("test_tool".to_string(), "A test tool".to_string(), json!({}))
.await;
let (sid, _) = server.create_session(None).await;
let mut event_rx = server.subscribe_events();
let server_clone = server.clone();
let sid_clone = sid.clone();
tokio::spawn(async move {
let _ = server_clone.run_turn(&sid_clone, "call the tool", |_event| Ok(())).await;
});
let events = collect_events(&mut event_rx).await;
assert!(
events.contains(&"run_finished".to_string()),
"should finish even with empty slot: {events:?}"
);
}
#[tokio::test]
async fn test_br_05_session_id_reuse() {
let mock = Arc::new(MockLlmClient::new());
let server = build_server(mock);
let sid1 = server
.get_or_create_session(Some("shared-session".to_string()))
.await;
let sid2 = server
.get_or_create_session(Some("shared-session".to_string()))
.await;
assert_eq!(sid1.id, sid2.id, "same external_id should reuse session");
let sid3 = server
.get_or_create_session(Some("other-session".to_string()))
.await;
assert_ne!(sid3.id, sid1.id, "different external_id should create new session");
let sid4 = server.get_or_create_session(None).await;
let sid5 = server.get_or_create_session(None).await;
assert_ne!(sid4.id, sid5.id, "None external_id should always create new");
}
#[tokio::test]
async fn test_br_06_sequential_tool_calls() {
let mock = Arc::new(MockLlmClient::new());
mock.set_tool_call("test_tool", &json!({"step": 1})).await;
let server = build_server(mock.clone());
server
.register_tool("test_tool".to_string(), "Tool 1".to_string(), json!({}))
.await;
let sid = server.get_or_create_session(None).await;
let mut event_rx = server.subscribe_events();
let _tx1 = server.prepare_tool_call().await;
let server_clone = server.clone();
let sid_clone = sid.clone();
tokio::spawn(async move {
let _ = server_clone.run_turn(&sid_clone, "call tool 1", |_event| Ok(())).await;
});
let events1 = collect_events(&mut event_rx).await;
assert!(
events1.contains(&"run_finished".to_string()),
"first turn should finish: {events1:?}"
);
mock.set_tool_call("test_tool", &json!({"step": 2})).await;
let mut event_rx2 = server.subscribe_events();
let _tx2 = server.prepare_tool_call().await;
let sid_clone2 = sid.clone();
let server_clone2 = server.clone();
tokio::spawn(async move {
let _ = server_clone2.run_turn(&sid_clone2, "call tool again", |_event| Ok(())).await;
});
let events2 = collect_events(&mut event_rx2).await;
assert!(
events2.contains(&"run_finished".to_string()),
"second turn should finish: {events2:?}"
);
let finished1 = events1.iter().any(|t| t == "run_finished");
let finished2 = events2.iter().any(|t| t == "run_finished");
assert!(finished1, "first turn should finish: {events1:?}");
assert!(finished2, "second turn should finish: {events2:?}");
}
#[tokio::test]
async fn test_register_tool_appears_in_list() {
let mock = Arc::new(MockLlmClient::new());
let server = build_server(mock);
server.register_tool("my_tool".into(), "A test tool".into(), json!({})).await;
let tools = server.list_tools().await;
assert!(tools.iter().any(|t| t.name == "my_tool"));
}
#[tokio::test]
async fn test_register_multiple_tools() {
let mock = Arc::new(MockLlmClient::new());
let server = build_server(mock);
server.register_tool("zzz_tool".into(), "Z".into(), json!({})).await;
server.register_tool("aaa_tool".into(), "A".into(), json!({})).await;
server.register_tool("mmm_tool".into(), "M".into(), json!({})).await;
let tools = server.list_tools().await;
let names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect();
assert!(tools.len() >= 3);
assert_eq!(names[0], "aaa_tool");
assert_eq!(names[1], "mmm_tool");
assert_eq!(names[2], "zzz_tool");
}
#[tokio::test]
async fn test_prepare_tool_call_sender_usable() {
let mock = Arc::new(MockLlmClient::new());
let server = build_server(mock);
let tx = server.prepare_tool_call().await;
let result = tx.send(Ok(agent_base::ToolOutput {
summary: "done".into(),
raw: None,
control_flow: agent_base::ToolControlFlow::Continue,
truncation: None,
}));
assert!(result.is_ok());
}
#[tokio::test]
async fn test_subscribe_events_receiver_open() {
let mock = Arc::new(MockLlmClient::new());
let server = build_server(mock);
let rx = server.subscribe_events();
assert_eq!(rx.len(), 0);
}
#[tokio::test]
async fn test_get_or_create_different_external_ids() {
let mock = Arc::new(MockLlmClient::new());
let server = build_server(mock);
let sid1 = server.get_or_create_session(Some("ext-1".into())).await;
let sid2 = server.get_or_create_session(Some("ext-2".into())).await;
assert_ne!(sid1.id, sid2.id, "different external_ids should create different sessions");
}
#[tokio::test]
async fn test_create_session_without_external_id() {
let mock = Arc::new(MockLlmClient::new());
let server = build_server(mock);
let sid = server.get_or_create_session(None).await;
assert!(sid.id > 0);
assert!(sid.external_id.is_none());
}