use bloop_sdk::*;
#[test]
fn test_hmac_signing_produces_hex_string() {
let signature = bloop_sdk::signing::sign("test-key", b"hello world");
assert_eq!(signature.len(), 64);
assert!(signature.chars().all(|c| c.is_ascii_hexdigit()));
}
#[test]
fn test_hmac_signing_deterministic() {
let sig1 = bloop_sdk::signing::sign("key", b"body");
let sig2 = bloop_sdk::signing::sign("key", b"body");
assert_eq!(sig1, sig2);
}
#[test]
fn test_hmac_signing_different_keys_differ() {
let sig1 = bloop_sdk::signing::sign("key-a", b"body");
let sig2 = bloop_sdk::signing::sign("key-b", b"body");
assert_ne!(sig1, sig2);
}
#[test]
fn test_hmac_signing_different_bodies_differ() {
let sig1 = bloop_sdk::signing::sign("key", b"body-a");
let sig2 = bloop_sdk::signing::sign("key", b"body-b");
assert_ne!(sig1, sig2);
}
#[test]
fn test_event_creation_defaults() {
let event = Event {
error_type: "TypeError".into(),
message: "something broke".into(),
..Default::default()
};
assert_eq!(event.error_type, "TypeError");
assert_eq!(event.message, "something broke");
assert!(event.source.is_none());
assert!(event.metadata.is_none());
}
#[test]
fn test_event_with_all_fields() {
let event = Event {
error_type: "NetworkError".into(),
message: "timeout".into(),
source: Some("api-server".into()),
route_or_procedure: Some("/api/users".into()),
screen: Some("dashboard".into()),
stack: Some("at main.rs:42".into()),
http_status: Some(500),
request_id: Some("req-123".into()),
user_id_hash: Some("abc123".into()),
metadata: Some(serde_json::json!({"key": "value"})),
};
assert_eq!(event.http_status, Some(500));
assert_eq!(event.source.as_deref(), Some("api-server"));
}
#[test]
fn test_event_serialization_skips_none() {
let event = Event {
error_type: "Error".into(),
message: "msg".into(),
..Default::default()
};
let json = serde_json::to_string(&event).unwrap();
assert!(json.contains("\"error_type\":\"Error\""));
assert!(json.contains("\"message\":\"msg\""));
assert!(!json.contains("\"source\""));
assert!(!json.contains("\"stack\""));
assert!(!json.contains("\"metadata\""));
}
#[test]
fn test_span_type_serialization() {
assert_eq!(serde_json::to_string(&SpanType::Generation).unwrap(), "\"generation\"");
assert_eq!(serde_json::to_string(&SpanType::Tool).unwrap(), "\"tool\"");
assert_eq!(serde_json::to_string(&SpanType::Retrieval).unwrap(), "\"retrieval\"");
assert_eq!(serde_json::to_string(&SpanType::Custom).unwrap(), "\"custom\"");
}
#[test]
fn test_span_status_serialization() {
assert_eq!(serde_json::to_string(&SpanStatus::Ok).unwrap(), "\"ok\"");
assert_eq!(serde_json::to_string(&SpanStatus::Error).unwrap(), "\"error\"");
}
#[test]
fn test_trace_status_serialization() {
assert_eq!(serde_json::to_string(&TraceStatus::Running).unwrap(), "\"running\"");
assert_eq!(serde_json::to_string(&TraceStatus::Completed).unwrap(), "\"completed\"");
assert_eq!(serde_json::to_string(&TraceStatus::Error).unwrap(), "\"error\"");
}
#[test]
fn test_span_creation() {
let trace = Trace::new("test");
let span = Span::new(&trace, SpanType::Generation, "gpt-4o call")
.model("gpt-4o")
.provider("openai")
.input_text("hello world");
assert_eq!(span.data().name, "gpt-4o call");
assert_eq!(span.data().model.as_deref(), Some("gpt-4o"));
assert_eq!(span.data().provider.as_deref(), Some("openai"));
assert_eq!(span.data().input.as_deref(), Some("hello world"));
assert!(!span.data().id.is_empty());
assert!(span.data().started_at > 0);
}
#[test]
fn test_span_parent() {
let trace = Trace::new("test");
let span = Span::new(&trace, SpanType::Tool, "search")
.parent("parent-span-123");
assert_eq!(span.data().parent_span_id.as_deref(), Some("parent-span-123"));
}
#[test]
fn test_span_set_usage() {
let trace = Trace::new("test");
let mut span = Span::new(&trace, SpanType::Generation, "call");
span.set_tokens(100, 50);
span.set_cost(0.0025);
assert_eq!(span.data().input_tokens, 100);
assert_eq!(span.data().output_tokens, 50);
assert!((span.data().cost - 0.0025).abs() < f64::EPSILON);
}
#[test]
fn test_span_set_output() {
let trace = Trace::new("test");
let mut span = Span::new(&trace, SpanType::Generation, "call");
span.set_output("Hello, world!");
assert_eq!(span.data().output.as_deref(), Some("Hello, world!"));
}
#[test]
fn test_span_set_error() {
let trace = Trace::new("test");
let mut span = Span::new(&trace, SpanType::Generation, "call");
span.set_error("rate limit exceeded");
assert_eq!(span.data().status, SpanStatus::Error);
assert_eq!(span.data().error_message.as_deref(), Some("rate limit exceeded"));
}
#[test]
fn test_span_set_latency() {
let trace = Trace::new("test");
let mut span = Span::new(&trace, SpanType::Generation, "call");
span.set_latency(150);
assert_eq!(span.data().latency_ms, 150);
}
#[test]
fn test_span_end_adds_to_trace() {
let trace = Trace::new("test");
let mut span = Span::new(&trace, SpanType::Generation, "call");
span.set_tokens(10, 20);
span.end();
assert_eq!(trace.span_count(), 1);
}
#[test]
fn test_span_end_calculates_latency_if_unset() {
let trace = Trace::new("test");
let span = Span::new(&trace, SpanType::Generation, "call");
span.end();
assert_eq!(trace.span_count(), 1);
let spans = trace.spans();
assert!(spans[0].latency_ms >= 0);
}
#[test]
fn test_trace_creation() {
let trace = Trace::new("chat-completion");
assert_eq!(trace.name(), "chat-completion");
assert!(!trace.id().is_empty());
assert!(trace.started_at() > 0);
assert_eq!(trace.span_count(), 0);
}
#[test]
fn test_trace_with_options() {
let trace = Trace::new("test")
.session_id("session-1")
.user_id("user-1")
.input_text("Hello")
.prompt_name("my-prompt")
.prompt_version("v2");
assert_eq!(trace.name(), "test");
let data = trace.to_data();
assert_eq!(data.session_id.as_deref(), Some("session-1"));
assert_eq!(data.user_id.as_deref(), Some("user-1"));
assert_eq!(data.input.as_deref(), Some("Hello"));
assert_eq!(data.prompt_name.as_deref(), Some("my-prompt"));
assert_eq!(data.prompt_version.as_deref(), Some("v2"));
}
#[test]
fn test_trace_with_multiple_spans() {
let trace = Trace::new("agent-pipeline");
Span::new(&trace, SpanType::Retrieval, "vector search").end();
Span::new(&trace, SpanType::Generation, "llm call").end();
Span::new(&trace, SpanType::Tool, "api call").end();
assert_eq!(trace.span_count(), 3);
}
#[test]
fn test_trace_end_sets_ended_at() {
let trace = Trace::new("test");
Span::new(&trace, SpanType::Generation, "call").end();
trace.set_output("final output");
trace.end();
let data = trace.to_data();
assert!(data.ended_at.is_some());
assert_eq!(data.output.as_deref(), Some("final output"));
assert_eq!(data.status, TraceStatus::Completed);
}
#[test]
fn test_trace_end_with_error_status() {
let trace = Trace::new("test");
trace.set_status(TraceStatus::Error);
trace.end();
let data = trace.to_data();
assert_eq!(data.status, TraceStatus::Error);
}
#[test]
fn test_trace_serialization() {
let trace = Trace::new("test");
trace.end();
let data = trace.to_data();
let json = serde_json::to_string(&data).unwrap();
assert!(json.contains("\"name\":\"test\""));
assert!(json.contains("\"status\":\"completed\""));
assert!(json.contains("\"ended_at\""));
assert!(!json.contains("\"session_id\""));
assert!(!json.contains("\"user_id\""));
}
#[test]
fn test_trace_unique_ids() {
let t1 = Trace::new("a");
let t2 = Trace::new("b");
assert_ne!(t1.id(), t2.id());
}
#[test]
fn test_trace_data_round_trip_json() {
let trace = Trace::new("roundtrip")
.session_id("s1")
.prompt_name("prompt1");
{
let mut span = Span::new(&trace, SpanType::Generation, "llm");
span.set_tokens(100, 50);
span.set_cost(0.005);
span.set_latency(200);
span.end();
}
trace.end();
let data = trace.to_data();
let json = serde_json::to_string(&data).unwrap();
let parsed: TraceData = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.name, "roundtrip");
assert_eq!(parsed.session_id.as_deref(), Some("s1"));
assert_eq!(parsed.prompt_name.as_deref(), Some("prompt1"));
assert_eq!(parsed.spans.len(), 1);
assert_eq!(parsed.spans[0].input_tokens, 100);
assert_eq!(parsed.spans[0].output_tokens, 50);
}
#[test]
fn test_builder_requires_endpoint() {
let result = BloopClient::builder()
.project_key("key")
.build();
assert!(result.is_err());
assert!(result.unwrap_err().contains("endpoint"));
}
#[test]
fn test_builder_requires_project_key() {
let result = BloopClient::builder()
.endpoint("http://localhost")
.build();
assert!(result.is_err());
assert!(result.unwrap_err().contains("project_key"));
}
#[test]
fn test_builder_success() {
let result = BloopClient::builder()
.endpoint("http://localhost:3000")
.project_key("test-key")
.environment("staging")
.release("1.0.0")
.source("my-app")
.build();
assert!(result.is_ok());
}
#[test]
fn test_builder_strips_trailing_slash() {
let client = BloopClient::builder()
.endpoint("http://localhost:3000/")
.project_key("key")
.build()
.unwrap();
client.capture_error("Test", "msg");
}
#[test]
fn test_client_capture_error() {
let client = BloopClient::builder()
.endpoint("http://localhost:9999")
.project_key("test-key")
.build()
.unwrap();
client.capture_error("Error1", "message1");
client.capture_error("Error2", "message2");
}
#[test]
fn test_client_capture_event() {
let client = BloopClient::builder()
.endpoint("http://localhost:9999")
.project_key("test-key")
.build()
.unwrap();
client.capture(Event {
error_type: "TypeError".into(),
message: "null ref".into(),
route_or_procedure: Some("/api/users".into()),
..Default::default()
});
}
#[test]
fn test_client_start_trace() {
let client = BloopClient::builder()
.endpoint("http://localhost:9999")
.project_key("test-key")
.build()
.unwrap();
let trace = client.start_trace("chat-completion");
assert_eq!(trace.name(), "chat-completion");
}
#[test]
fn test_client_send_trace() {
let client = BloopClient::builder()
.endpoint("http://localhost:9999")
.project_key("test-key")
.build()
.unwrap();
let trace = client.start_trace("test");
{
let mut span = Span::new(&trace, SpanType::Generation, "call");
span.set_tokens(10, 20);
span.end();
}
trace.end();
client.send_trace(trace);
}
#[test]
fn test_client_flush_does_not_panic() {
let client = BloopClient::builder()
.endpoint("http://localhost:9999")
.project_key("test-key")
.build()
.unwrap();
client.capture_error("Err", "msg");
let trace = client.start_trace("test");
trace.end();
client.send_trace(trace);
client.flush();
}
#[test]
fn test_client_close_does_not_panic() {
let client = BloopClient::builder()
.endpoint("http://localhost:9999")
.project_key("test-key")
.build()
.unwrap();
client.capture_error("Err", "msg");
client.close();
}
#[test]
fn test_client_debug() {
let client = BloopClient::builder()
.endpoint("http://localhost:9999")
.project_key("test-key")
.environment("staging")
.build()
.unwrap();
let debug = format!("{:?}", client);
assert!(debug.contains("BloopClient"));
assert!(debug.contains("staging"));
assert!(!debug.contains("test-key"));
}