use super::*;
use bytes::Bytes;
use proptest::prelude::*;
use std::sync::mpsc;
use crate::ai::provider::sse::SseEventParser;
#[test]
fn test_async_anthropic_client_new() {
let client = AsyncAnthropicClient::new(
"sk-ant-test".to_string(),
"claude-3-haiku".to_string(),
1024,
);
assert!(format!("{:?}", client).contains("AsyncAnthropicClient"));
}
#[test]
fn test_sse_parser_parse_delta_text_valid() {
let data =
r#"{"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"Hello"}}"#;
let parser = AnthropicEventParser;
let result = parser.parse_data(data);
assert_eq!(result, Some("Hello".to_string()));
}
#[test]
fn test_sse_parser_parse_delta_text_not_delta() {
let data = r#"{"type":"message_start","message":{"id":"msg_123"}}"#;
let parser = AnthropicEventParser;
let result = parser.parse_data(data);
assert_eq!(result, None);
}
#[test]
fn test_sse_parser_parse_delta_text_invalid_json() {
let data = "not valid json";
let parser = AnthropicEventParser;
let result = parser.parse_data(data);
assert_eq!(result, None);
}
#[test]
fn test_sse_parser_parse_chunk_single_event() {
let mut parser = SseParser::new(AnthropicEventParser);
let data = b"event: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\"Hello\"}}\n\n";
let results = parser.parse_chunk(&Bytes::from_static(data));
assert_eq!(results, vec!["Hello".to_string()]);
}
#[test]
fn test_sse_parser_parse_chunk_multiple_events() {
let mut parser = SseParser::new(AnthropicEventParser);
let data = b"event: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\"Hello\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\" World\"}}\n\n";
let results = parser.parse_chunk(&Bytes::from_static(data));
assert_eq!(results, vec!["Hello".to_string(), " World".to_string()]);
}
#[test]
fn test_sse_parser_parse_chunk_skips_non_delta_events() {
let mut parser = SseParser::new(AnthropicEventParser);
let data = b"event: message_start\ndata: {\"type\":\"message_start\",\"message\":{\"id\":\"msg_123\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\"Hello\"}}\n\n";
let results = parser.parse_chunk(&Bytes::from_static(data));
assert_eq!(results, vec!["Hello".to_string()]);
}
#[test]
fn test_sse_parser_parse_chunk_handles_done() {
let mut parser = SseParser::new(AnthropicEventParser);
let data = b"event: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\"Test\"}}\n\ndata: [DONE]\n";
let results = parser.parse_chunk(&Bytes::from_static(data));
assert_eq!(results, vec!["Test".to_string()]);
}
#[test]
fn test_sse_parser_parse_chunk_empty() {
let mut parser = SseParser::new(AnthropicEventParser);
let data = b"";
let results = parser.parse_chunk(&Bytes::from_static(data));
assert!(results.is_empty());
}
#[test]
fn test_sse_parser_parse_chunk_skips_empty_text() {
let mut parser = SseParser::new(AnthropicEventParser);
let data = b"event: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\"\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\"Real content\"}}\n\n";
let results = parser.parse_chunk(&Bytes::from_static(data));
assert_eq!(results, vec!["Real content".to_string()]);
}
#[test]
fn test_sse_parser_buffers_incomplete_lines() {
let mut parser = SseParser::new(AnthropicEventParser);
let data1 = b"event: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\"Hel";
let results1 = parser.parse_chunk(&Bytes::from_static(data1));
assert!(results1.is_empty());
let data2 = b"lo\"}}\n\n";
let results2 = parser.parse_chunk(&Bytes::from_static(data2));
assert_eq!(results2, vec!["Hello".to_string()]);
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(100))]
#[test]
fn prop_cancellation_aborts_request(
api_key in "[a-zA-Z0-9]{10,20}",
model in "[a-zA-Z0-9-]{5,20}",
max_tokens in 100u32..4096u32,
prompt in "[a-zA-Z0-9 ]{1,50}",
) {
let client = AsyncAnthropicClient::new(
api_key,
model,
max_tokens,
);
let cancel_token = CancellationToken::new();
cancel_token.cancel();
let (response_tx, _response_rx) = mpsc::channel();
let rt = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.unwrap();
let result = rt.block_on(async {
client.stream_with_cancel(
&prompt,
1,
cancel_token,
response_tx,
).await
});
prop_assert!(
matches!(result, Err(AiError::Cancelled)),
"Pre-cancelled token should result in AiError::Cancelled, got {:?}",
result
);
}
#[test]
fn prop_cancellation_checked_before_request(
api_key in "[a-zA-Z0-9]{10,20}",
model in "[a-zA-Z0-9-]{5,20}",
max_tokens in 100u32..4096u32,
prompt in "[a-zA-Z0-9 ]{1,50}",
request_id in 1u64..1000u64,
) {
let client = AsyncAnthropicClient::new(
api_key,
model,
max_tokens,
);
let cancel_token = CancellationToken::new();
cancel_token.cancel();
let (response_tx, response_rx) = mpsc::channel();
let rt = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.unwrap();
let result = rt.block_on(async {
client.stream_with_cancel(
&prompt,
request_id,
cancel_token,
response_tx,
).await
});
prop_assert!(
matches!(result, Err(AiError::Cancelled)),
"Pre-cancelled token should return AiError::Cancelled immediately"
);
prop_assert!(
response_rx.try_recv().is_err(),
"No response chunks should be sent when cancelled before start"
);
}
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(100))]
#[test]
fn prop_idempotent_cancellation(
num_cancels in 1..10usize,
) {
let token = CancellationToken::new();
prop_assert!(!token.is_cancelled(), "Token should not be cancelled initially");
for i in 0..num_cancels {
token.cancel();
prop_assert!(
token.is_cancelled(),
"Token should be cancelled after cancel() call {}",
i + 1
);
}
prop_assert!(token.is_cancelled(), "Token should remain cancelled");
}
}
#[test]
fn test_anthropic_uses_shared_sse_parser() {
let mut parser = SseParser::new(AnthropicEventParser);
let data = b"data: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\"Hello from Anthropic\"}}\n\n";
let results = parser.parse_chunk(&Bytes::from_static(data));
assert_eq!(results.len(), 1);
assert_eq!(results[0], "Hello from Anthropic");
let data2 = b"data: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\" World\"}}\n\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\"!\"}}\n\n";
let results2 = parser.parse_chunk(&Bytes::from_static(data2));
assert_eq!(results2.len(), 2);
assert_eq!(results2[0], " World");
assert_eq!(results2[1], "!");
let data3 = b"data: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\"Final\"}}\n\ndata: [DONE]\n";
let results3 = parser.parse_chunk(&Bytes::from_static(data3));
assert_eq!(results3.len(), 1);
assert_eq!(results3[0], "Final");
}