use crate::error::Result;
use crate::http::HttpClient;
use crate::types::event::GlobalEvent;
use crate::types::event::GlobalEventPayload;
use reqwest::Method;
#[derive(Clone)]
pub struct GlobalApi {
http: HttpClient,
}
impl GlobalApi {
pub fn new(http: HttpClient) -> Self {
Self { http }
}
pub fn event_stream_url(&self) -> String {
format!("{}/global/event", self.http.base())
}
pub fn directory_event_stream_url(&self) -> String {
format!("{}/event", self.http.base())
}
pub async fn health(&self) -> Result<crate::http::misc::HealthInfo> {
self.http
.request_json(Method::GET, "/global/health", None)
.await
}
}
impl GlobalEvent {
pub fn is_directory(&self, dir: &str) -> bool {
self.directory.as_deref() == Some(dir)
}
pub fn is_question_event(&self) -> bool {
matches!(
&self.payload,
GlobalEventPayload::Event(
event
) if matches!(
**event,
crate::types::event::Event::QuestionAsked { .. }
| crate::types::event::Event::QuestionReplied { .. }
| crate::types::event::Event::QuestionRejected { .. }
)
)
}
pub fn is_session_event(&self) -> bool {
matches!(
&self.payload,
GlobalEventPayload::Event(
event
) if matches!(
**event,
crate::types::event::Event::SessionCreated { .. }
| crate::types::event::Event::SessionUpdated { .. }
| crate::types::event::Event::SessionDeleted { .. }
| crate::types::event::Event::SessionError { .. }
| crate::types::event::Event::SessionIdle { .. }
)
)
}
pub fn is_message_event(&self) -> bool {
matches!(
&self.payload,
GlobalEventPayload::Event(
event
) if matches!(
**event,
crate::types::event::Event::MessageUpdated { .. }
| crate::types::event::Event::MessageRemoved { .. }
| crate::types::event::Event::MessagePartUpdated { .. }
| crate::types::event::Event::MessagePartRemoved { .. }
)
)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::http::HttpConfig;
use crate::types::event::Event;
use crate::types::event::GlobalEventPayload;
use crate::types::event::QuestionAskedProps;
use crate::types::question::QuestionRequest;
use std::time::Duration;
use wiremock::Mock;
use wiremock::MockServer;
use wiremock::ResponseTemplate;
use wiremock::matchers::method;
use wiremock::matchers::path;
#[tokio::test]
async fn test_global_api_event_stream_url() {
let mock_server = MockServer::start().await;
let client = HttpClient::new(HttpConfig {
base_url: mock_server.uri(),
directory: None,
workspace: None,
timeout: Duration::from_secs(30),
})
.unwrap();
let api = GlobalApi::new(client);
let url = api.event_stream_url();
assert!(url.ends_with("/global/event"));
}
#[tokio::test]
async fn test_global_api_directory_event_stream_url() {
let mock_server = MockServer::start().await;
let client = HttpClient::new(HttpConfig {
base_url: mock_server.uri(),
directory: Some("/my/project".to_string()),
workspace: None,
timeout: Duration::from_secs(30),
})
.unwrap();
let api = GlobalApi::new(client);
let url = api.directory_event_stream_url();
assert!(url.ends_with("/event"));
}
#[tokio::test]
async fn test_global_api_health() {
let mock_server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/global/health"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"healthy": true,
"version": "1.0.0"
})))
.mount(&mock_server)
.await;
let client = HttpClient::new(HttpConfig {
base_url: mock_server.uri(),
directory: None,
workspace: None,
timeout: Duration::from_secs(30),
})
.unwrap();
let api = GlobalApi::new(client);
let result = api.health().await.unwrap();
assert!(result.healthy);
}
#[test]
fn test_global_event_envelope_is_directory() {
let envelope = GlobalEvent {
directory: Some("/my/project".to_string()),
project: None,
workspace: None,
payload: GlobalEventPayload::Event(Box::new(Event::ServerHeartbeat {
properties: serde_json::Value::Null,
})),
};
assert!(envelope.is_directory("/my/project"));
assert!(!envelope.is_directory("/other/project"));
}
#[test]
fn test_global_event_envelope_is_question_event() {
let envelope = GlobalEvent {
directory: Some("/test".to_string()),
project: None,
workspace: None,
payload: GlobalEventPayload::Event(Box::new(Event::QuestionAsked {
properties: QuestionAskedProps {
request: QuestionRequest {
id: "q1".to_string(),
session_id: "s1".to_string(),
questions: vec![],
tool: None,
extra: serde_json::Value::Null,
},
},
})),
};
assert!(envelope.is_question_event());
assert!(!envelope.is_session_event());
assert!(!envelope.is_message_event());
}
#[test]
fn test_global_event_envelope_is_session_event() {
let envelope = GlobalEvent {
directory: Some("/test".to_string()),
project: None,
workspace: None,
payload: GlobalEventPayload::Event(Box::new(Event::SessionIdle {
properties: crate::types::event::SessionIdleProps {
session_id: "s1".to_string(),
extra: serde_json::Value::Null,
},
})),
};
assert!(envelope.is_session_event());
assert!(!envelope.is_question_event());
}
#[test]
fn test_global_event_envelope_deserialize() {
let json = r#"{
"directory": "/project/path",
"payload": {
"type": "server.heartbeat",
"properties": {}
}
}"#;
let envelope: GlobalEvent = serde_json::from_str(json).unwrap();
assert_eq!(envelope.directory.as_deref(), Some("/project/path"));
assert!(matches!(
envelope.payload,
GlobalEventPayload::Event(event) if matches!(*event, Event::ServerHeartbeat { .. })
));
}
}