use serde::{Deserialize, Serialize};
use crate::control::messages::{ControlRequest, ControlResponse};
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum Message {
System(SystemMessage),
Assistant(AssistantMessage),
User(UserMessage),
Result(ResultMessage),
ControlRequest {
request_id: String,
#[serde(flatten)]
request: ControlRequest,
},
ControlResponse {
request_id: String,
#[serde(flatten)]
response: ControlResponse,
},
RateLimitEvent(serde_json::Value),
#[serde(rename = "mcp_message")]
McpMessage(serde_json::Value),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "subtype", rename_all = "snake_case")]
pub enum SystemMessage {
Init {
session_id: String,
tools: Vec<ToolInfo>,
mcp_servers: Vec<McpServerInfo>,
#[serde(flatten)]
extra: serde_json::Value,
},
CompactBoundary,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum AssistantMessageError {
AuthenticationFailed,
BillingError,
RateLimit,
InvalidRequest,
ServerError,
Overloaded,
#[serde(other)]
Unknown,
}
impl AssistantMessageError {
pub fn as_str(&self) -> &str {
match self {
AssistantMessageError::AuthenticationFailed => "authentication_failed",
AssistantMessageError::BillingError => "billing_error",
AssistantMessageError::RateLimit => "rate_limit",
AssistantMessageError::InvalidRequest => "invalid_request",
AssistantMessageError::ServerError => "server_error",
AssistantMessageError::Overloaded => "overloaded",
AssistantMessageError::Unknown => "unknown",
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AssistantMessage {
pub message: ApiMessage,
#[serde(default)]
pub parent_tool_use_id: Option<String>,
#[serde(default)]
pub duration_ms: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub error: Option<AssistantMessageError>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UserMessage {
pub message: ApiMessage,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub uuid: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub parent_tool_use_id: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "subtype", rename_all = "snake_case")]
pub enum ResultMessage {
Success {
result: String,
#[serde(default)]
duration_ms: Option<u64>,
#[serde(default)]
duration_api_ms: Option<u64>,
#[serde(default)]
num_turns: Option<u32>,
#[serde(default)]
session_id: Option<String>,
#[serde(default)]
total_cost_usd: Option<f64>,
#[serde(default)]
usage: Option<UsageInfo>,
#[serde(default)]
structured_output: Option<serde_json::Value>,
#[serde(default)]
is_error: Option<bool>,
},
Error {
error: String,
#[serde(flatten)]
extra: serde_json::Value,
},
InputRequired,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum ContentBlock {
Text {
text: String,
},
ToolUse {
id: String,
name: String,
input: serde_json::Value,
},
ToolResult {
tool_use_id: String,
content: serde_json::Value,
#[serde(default)]
is_error: bool,
},
Thinking {
thinking: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
signature: Option<String>,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StreamEvent {
pub event_type: String,
pub data: serde_json::Value,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ApiMessage {
pub role: String,
pub content: Vec<ContentBlock>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UsageInfo {
pub input_tokens: u32,
pub output_tokens: u32,
}
#[derive(Debug, Clone, Serialize)]
pub struct ToolInfo {
pub name: String,
#[serde(default)]
pub description: Option<String>,
#[serde(default)]
pub input_schema: Option<serde_json::Value>,
}
impl<'de> serde::Deserialize<'de> for ToolInfo {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
use serde::de;
#[derive(Deserialize)]
struct ToolInfoObj {
name: String,
#[serde(default)]
description: Option<String>,
#[serde(default)]
input_schema: Option<serde_json::Value>,
}
let value = serde_json::Value::deserialize(deserializer)?;
match value {
serde_json::Value::String(name) => Ok(ToolInfo {
name,
description: None,
input_schema: None,
}),
serde_json::Value::Object(_) => {
let obj: ToolInfoObj = serde_json::from_value(value).map_err(de::Error::custom)?;
Ok(ToolInfo {
name: obj.name,
description: obj.description,
input_schema: obj.input_schema,
})
}
_ => Err(de::Error::custom(
"expected a string or object for ToolInfo",
)),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct McpServerInfo {
pub name: String,
#[serde(flatten)]
pub extra: serde_json::Value,
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn test_message_system_init() {
let json = json!({
"type": "system",
"subtype": "init",
"session_id": "sess_123",
"tools": [{"name": "bash", "description": "Run shell commands"}],
"mcp_servers": [{"name": "filesystem"}],
"extra_field": "value"
});
let msg: Message = serde_json::from_value(json.clone()).unwrap();
match &msg {
Message::System(SystemMessage::Init {
session_id,
tools,
mcp_servers,
..
}) => {
assert_eq!(session_id, "sess_123");
assert_eq!(tools.len(), 1);
assert_eq!(tools[0].name, "bash");
assert_eq!(mcp_servers.len(), 1);
assert_eq!(mcp_servers[0].name, "filesystem");
}
_ => panic!("Expected System::Init message"),
}
let serialized = serde_json::to_value(&msg).unwrap();
assert_eq!(serialized["type"], "system");
assert_eq!(serialized["subtype"], "init");
}
#[test]
fn test_message_system_compact_boundary() {
let json = json!({
"type": "system",
"subtype": "compact_boundary"
});
let msg: Message = serde_json::from_value(json).unwrap();
match msg {
Message::System(SystemMessage::CompactBoundary) => {}
_ => panic!("Expected System::CompactBoundary message"),
}
}
#[test]
fn test_message_assistant() {
let json = json!({
"type": "assistant",
"message": {
"role": "assistant",
"content": [
{"type": "text", "text": "Hello!"}
]
},
"parent_tool_use_id": "tool_123",
"duration_ms": 250
});
let msg: Message = serde_json::from_value(json).unwrap();
match msg {
Message::Assistant(assistant_msg) => {
assert_eq!(assistant_msg.message.role, "assistant");
assert_eq!(assistant_msg.message.content.len(), 1);
assert_eq!(
assistant_msg.parent_tool_use_id,
Some("tool_123".to_string())
);
assert_eq!(assistant_msg.duration_ms, Some(250));
}
_ => panic!("Expected Assistant message"),
}
}
#[test]
fn test_message_user() {
let json = json!({
"type": "user",
"message": {
"role": "user",
"content": [
{"type": "text", "text": "Hello assistant!"}
]
}
});
let msg: Message = serde_json::from_value(json).unwrap();
match msg {
Message::User(user_msg) => {
assert_eq!(user_msg.message.role, "user");
assert_eq!(user_msg.message.content.len(), 1);
}
_ => panic!("Expected User message"),
}
}
#[test]
fn test_message_result_success() {
let json = json!({
"type": "result",
"subtype": "success",
"result": "Task completed",
"duration_ms": 1000,
"num_turns": 5,
"session_id": "sess_123",
"total_cost_usd": 0.025,
"usage": {
"input_tokens": 100,
"output_tokens": 50
}
});
let msg: Message = serde_json::from_value(json).unwrap();
match msg {
Message::Result(ResultMessage::Success {
result,
duration_ms,
num_turns,
session_id,
total_cost_usd,
usage,
..
}) => {
assert_eq!(result, "Task completed");
assert_eq!(duration_ms, Some(1000));
assert_eq!(num_turns, Some(5));
assert_eq!(session_id, Some("sess_123".to_string()));
assert_eq!(total_cost_usd, Some(0.025));
assert!(usage.is_some());
let usage = usage.unwrap();
assert_eq!(usage.input_tokens, 100);
assert_eq!(usage.output_tokens, 50);
}
_ => panic!("Expected Result::Success message"),
}
}
#[test]
fn test_message_result_error() {
let json = json!({
"type": "result",
"subtype": "error",
"error": "Something went wrong",
"code": 500
});
let msg: Message = serde_json::from_value(json).unwrap();
match msg {
Message::Result(ResultMessage::Error { error, extra }) => {
assert_eq!(error, "Something went wrong");
assert_eq!(extra["code"], 500);
}
_ => panic!("Expected Result::Error message"),
}
}
#[test]
fn test_message_result_input_required() {
let json = json!({
"type": "result",
"subtype": "input_required"
});
let msg: Message = serde_json::from_value(json).unwrap();
match msg {
Message::Result(ResultMessage::InputRequired) => {}
_ => panic!("Expected Result::InputRequired message"),
}
}
#[test]
fn test_content_block_text() {
let json = json!({"type": "text", "text": "Hello world"});
let block: ContentBlock = serde_json::from_value(json).unwrap();
match block {
ContentBlock::Text { text } => {
assert_eq!(text, "Hello world");
}
_ => panic!("Expected Text block"),
}
}
#[test]
fn test_content_block_tool_use() {
let json = json!({
"type": "tool_use",
"id": "tool_123",
"name": "bash",
"input": {"command": "ls -la"}
});
let block: ContentBlock = serde_json::from_value(json).unwrap();
match block {
ContentBlock::ToolUse { id, name, input } => {
assert_eq!(id, "tool_123");
assert_eq!(name, "bash");
assert_eq!(input["command"], "ls -la");
}
_ => panic!("Expected ToolUse block"),
}
}
#[test]
fn test_content_block_tool_result() {
let json = json!({
"type": "tool_result",
"tool_use_id": "tool_123",
"content": {"output": "file1.txt\nfile2.txt"},
"is_error": false
});
let block: ContentBlock = serde_json::from_value(json).unwrap();
match block {
ContentBlock::ToolResult {
tool_use_id,
content,
is_error,
} => {
assert_eq!(tool_use_id, "tool_123");
assert_eq!(content["output"], "file1.txt\nfile2.txt");
assert!(!is_error);
}
_ => panic!("Expected ToolResult block"),
}
}
#[test]
fn test_content_block_tool_result_default_is_error() {
let json = json!({
"type": "tool_result",
"tool_use_id": "tool_123",
"content": {"output": "success"}
});
let block: ContentBlock = serde_json::from_value(json).unwrap();
match block {
ContentBlock::ToolResult { is_error, .. } => {
assert!(!is_error); }
_ => panic!("Expected ToolResult block"),
}
}
#[test]
fn test_content_block_thinking() {
let json = json!({
"type": "thinking",
"thinking": "Let me consider this..."
});
let block: ContentBlock = serde_json::from_value(json).unwrap();
match block {
ContentBlock::Thinking { thinking, .. } => {
assert_eq!(thinking, "Let me consider this...");
}
_ => panic!("Expected Thinking block"),
}
}
#[test]
fn test_stream_event() {
let json = json!({
"event_type": "message_start",
"data": {"message_id": "msg_123"}
});
let event: StreamEvent = serde_json::from_value(json).unwrap();
assert_eq!(event.event_type, "message_start");
assert_eq!(event.data["message_id"], "msg_123");
}
#[test]
fn test_usage_info() {
let json = json!({
"input_tokens": 100,
"output_tokens": 50
});
let usage: UsageInfo = serde_json::from_value(json).unwrap();
assert_eq!(usage.input_tokens, 100);
assert_eq!(usage.output_tokens, 50);
}
#[test]
fn test_tool_info_minimal() {
let json = json!({"name": "bash"});
let tool: ToolInfo = serde_json::from_value(json).unwrap();
assert_eq!(tool.name, "bash");
assert!(tool.description.is_none());
assert!(tool.input_schema.is_none());
}
#[test]
fn test_tool_info_full() {
let json = json!({
"name": "bash",
"description": "Run shell commands",
"input_schema": {
"type": "object",
"properties": {"command": {"type": "string"}}
}
});
let tool: ToolInfo = serde_json::from_value(json).unwrap();
assert_eq!(tool.name, "bash");
assert_eq!(tool.description, Some("Run shell commands".to_string()));
assert!(tool.input_schema.is_some());
}
#[test]
fn test_tool_info_from_string() {
let json = json!("Task");
let tool: ToolInfo = serde_json::from_value(json).unwrap();
assert_eq!(tool.name, "Task");
assert!(tool.description.is_none());
assert!(tool.input_schema.is_none());
}
#[test]
fn test_tool_info_array_of_strings() {
let json = json!(["Task", "Bash", "Read"]);
let tools: Vec<ToolInfo> = serde_json::from_value(json).unwrap();
assert_eq!(tools.len(), 3);
assert_eq!(tools[0].name, "Task");
assert_eq!(tools[1].name, "Bash");
assert_eq!(tools[2].name, "Read");
}
#[test]
fn test_mcp_server_info() {
let json = json!({
"name": "filesystem",
"version": "1.0.0",
"extra": "data"
});
let server: McpServerInfo = serde_json::from_value(json).unwrap();
assert_eq!(server.name, "filesystem");
assert_eq!(server.extra["version"], "1.0.0");
assert_eq!(server.extra["extra"], "data");
}
#[test]
fn test_optional_fields_default() {
let json = json!({
"type": "assistant",
"message": {
"role": "assistant",
"content": []
}
});
let msg: Message = serde_json::from_value(json).unwrap();
match msg {
Message::Assistant(assistant_msg) => {
assert!(assistant_msg.parent_tool_use_id.is_none());
assert!(assistant_msg.duration_ms.is_none());
}
_ => panic!("Expected Assistant message"),
}
}
#[test]
fn test_json_round_trip_complex() {
let original = Message::Assistant(AssistantMessage {
message: ApiMessage {
role: "assistant".to_string(),
content: vec![
ContentBlock::Text {
text: "I'll run that command.".to_string(),
},
ContentBlock::ToolUse {
id: "tool_xyz".to_string(),
name: "bash".to_string(),
input: json!({"command": "echo hello"}),
},
ContentBlock::Thinking {
thinking: "This should work...".to_string(),
signature: None,
},
],
},
parent_tool_use_id: None,
duration_ms: Some(150),
error: None,
});
let json = serde_json::to_value(&original).unwrap();
let roundtrip: Message = serde_json::from_value(json).unwrap();
match roundtrip {
Message::Assistant(assistant_msg) => {
assert_eq!(assistant_msg.message.content.len(), 3);
assert_eq!(assistant_msg.duration_ms, Some(150));
}
_ => panic!("Expected Assistant message"),
}
}
fn load_fixture(name: &str) -> Vec<Message> {
use std::fs::File;
use std::io::{BufRead, BufReader};
let fixture_path = format!(
"{}/tests/fixtures/{}.ndjson",
env!("CARGO_MANIFEST_DIR"),
name
);
let file = File::open(&fixture_path)
.unwrap_or_else(|e| panic!("Failed to open fixture '{}': {}", fixture_path, e));
BufReader::new(file)
.lines()
.enumerate()
.map(|(i, line)| {
let line = line.unwrap_or_else(|e| {
panic!(
"Failed to read line {} from fixture '{}': {}",
i + 1,
name,
e
)
});
serde_json::from_str(&line).unwrap_or_else(|e| {
panic!(
"Failed to parse line {} from fixture '{}': {}\nLine: {}",
i + 1,
name,
e,
line
)
})
})
.collect()
}
#[test]
fn test_simple_query_fixture() {
let messages = load_fixture("simple_query");
assert_eq!(messages.len(), 3, "Expected 3 messages in simple_query");
match &messages[0] {
Message::System(SystemMessage::Init {
session_id,
tools,
mcp_servers,
..
}) => {
assert_eq!(session_id, "sess_simple_001");
assert_eq!(tools.len(), 1);
assert_eq!(tools[0].name, "bash");
assert_eq!(mcp_servers.len(), 0);
}
_ => panic!("Expected System::Init as first message"),
}
match &messages[1] {
Message::Assistant(assistant_msg) => {
assert_eq!(assistant_msg.message.role, "assistant");
assert_eq!(assistant_msg.message.content.len(), 1);
match &assistant_msg.message.content[0] {
ContentBlock::Text { text } => {
assert!(text.contains("README.md"));
}
_ => panic!("Expected Text content block"),
}
assert_eq!(assistant_msg.duration_ms, Some(142));
}
_ => panic!("Expected Assistant message as second message"),
}
match &messages[2] {
Message::Result(ResultMessage::Success {
result,
duration_ms,
num_turns,
usage,
..
}) => {
assert_eq!(result, "Listed directory contents successfully");
assert_eq!(*duration_ms, Some(156));
assert_eq!(*num_turns, Some(1));
assert!(usage.is_some());
let usage = usage.as_ref().unwrap();
assert_eq!(usage.input_tokens, 45);
assert_eq!(usage.output_tokens, 28);
}
_ => panic!("Expected Result::Success as third message"),
}
}
#[test]
fn test_tool_use_fixture() {
let messages = load_fixture("tool_use");
assert_eq!(messages.len(), 5, "Expected 5 messages in tool_use");
match &messages[0] {
Message::System(SystemMessage::Init {
session_id,
tools,
mcp_servers,
..
}) => {
assert_eq!(session_id, "sess_tool_002");
assert_eq!(tools.len(), 1);
assert_eq!(tools[0].name, "bash");
assert!(tools[0].input_schema.is_some());
assert_eq!(mcp_servers.len(), 1);
assert_eq!(mcp_servers[0].name, "filesystem");
}
_ => panic!("Expected System::Init"),
}
match &messages[1] {
Message::Assistant(assistant_msg) => {
assert_eq!(assistant_msg.message.content.len(), 2);
match &assistant_msg.message.content[0] {
ContentBlock::Text { text } => {
assert!(text.contains("check the current directory"));
}
_ => panic!("Expected Text block"),
}
match &assistant_msg.message.content[1] {
ContentBlock::ToolUse { id, name, input } => {
assert_eq!(id, "toolu_01ABC");
assert_eq!(name, "bash");
assert_eq!(input["command"], "ls -la");
}
_ => panic!("Expected ToolUse block"),
}
}
_ => panic!("Expected Assistant message"),
}
match &messages[2] {
Message::User(user_msg) => {
assert_eq!(user_msg.message.content.len(), 1);
match &user_msg.message.content[0] {
ContentBlock::ToolResult {
tool_use_id,
content,
is_error,
} => {
assert_eq!(tool_use_id, "toolu_01ABC");
assert!(!is_error);
let content_str = content.as_str().unwrap();
assert!(content_str.contains("Cargo.toml"));
}
_ => panic!("Expected ToolResult block"),
}
}
_ => panic!("Expected User message"),
}
match &messages[3] {
Message::Assistant(assistant_msg) => {
assert_eq!(assistant_msg.message.content.len(), 1);
match &assistant_msg.message.content[0] {
ContentBlock::Text { text } => {
assert!(text.contains("Rust project"));
}
_ => panic!("Expected Text block"),
}
}
_ => panic!("Expected Assistant message"),
}
match &messages[4] {
Message::Result(ResultMessage::Success { num_turns, .. }) => {
assert_eq!(*num_turns, Some(2));
}
_ => panic!("Expected Result::Success"),
}
}
#[test]
fn test_error_response_fixture() {
let messages = load_fixture("error_response");
assert_eq!(messages.len(), 3, "Expected 3 messages in error_response");
match &messages[0] {
Message::System(SystemMessage::Init { session_id, .. }) => {
assert_eq!(session_id, "sess_error_003");
}
_ => panic!("Expected System::Init"),
}
match &messages[1] {
Message::Assistant(_) => {}
_ => panic!("Expected Assistant message"),
}
match &messages[2] {
Message::Result(ResultMessage::Error { error, extra }) => {
assert_eq!(error, "Failed to execute command: permission denied");
assert_eq!(extra["error_code"], "EACCES");
assert_eq!(extra["exit_code"], 126);
}
_ => panic!("Expected Result::Error"),
}
}
#[test]
fn test_thinking_content_fixture() {
let messages = load_fixture("thinking_content");
assert_eq!(messages.len(), 3, "Expected 3 messages in thinking_content");
match &messages[0] {
Message::System(SystemMessage::Init { session_id, .. }) => {
assert_eq!(session_id, "sess_think_004");
}
_ => panic!("Expected System::Init"),
}
match &messages[1] {
Message::Assistant(assistant_msg) => {
assert_eq!(assistant_msg.message.content.len(), 2);
match &assistant_msg.message.content[0] {
ContentBlock::Thinking { thinking, .. } => {
assert!(thinking.contains("analyze this request"));
assert!(thinking.contains("bash tool"));
}
_ => panic!("Expected Thinking block"),
}
match &assistant_msg.message.content[1] {
ContentBlock::Text { text } => {
assert!(text.contains("list the files"));
}
_ => panic!("Expected Text block"),
}
assert_eq!(assistant_msg.duration_ms, Some(234));
}
_ => panic!("Expected Assistant message"),
}
match &messages[2] {
Message::Result(ResultMessage::Success { usage, .. }) => {
assert!(usage.is_some());
}
_ => panic!("Expected Result::Success"),
}
}
#[test]
fn test_all_fixtures_valid() {
let fixtures = [
"simple_query",
"tool_use",
"error_response",
"thinking_content",
];
for fixture_name in &fixtures {
let messages = load_fixture(fixture_name);
assert!(
!messages.is_empty(),
"Fixture '{}' should contain at least one message",
fixture_name
);
for (i, msg) in messages.iter().enumerate() {
match msg {
Message::System(_)
| Message::Assistant(_)
| Message::User(_)
| Message::Result(_)
| Message::ControlRequest { .. }
| Message::ControlResponse { .. }
| Message::RateLimitEvent(_)
| Message::McpMessage(_) => {}
}
let _ = i; }
}
}
#[test]
fn test_empty_string_text_content() {
let json = json!({"type": "text", "text": ""});
let block: ContentBlock = serde_json::from_value(json).unwrap();
match block {
ContentBlock::Text { text } => {
assert_eq!(text, "");
}
_ => panic!("Expected Text block"),
}
}
#[test]
fn test_empty_content_array() {
let json = json!({
"type": "assistant",
"message": {
"role": "assistant",
"content": []
}
});
let msg: Message = serde_json::from_value(json).unwrap();
match msg {
Message::Assistant(assistant_msg) => {
assert_eq!(assistant_msg.message.content.len(), 0);
}
_ => panic!("Expected Assistant message"),
}
}
#[test]
fn test_minimal_system_init() {
let json = json!({
"type": "system",
"subtype": "init",
"session_id": "min_123",
"tools": [],
"mcp_servers": []
});
let msg: Message = serde_json::from_value(json).unwrap();
match msg {
Message::System(SystemMessage::Init {
session_id,
tools,
mcp_servers,
..
}) => {
assert_eq!(session_id, "min_123");
assert_eq!(tools.len(), 0);
assert_eq!(mcp_servers.len(), 0);
}
_ => panic!("Expected System::Init"),
}
}
#[test]
fn test_large_tool_input() {
let complex_input = json!({
"config": {
"nested": {
"deeply": {
"structure": ["array", "of", "values"],
"number": 42,
"boolean": true
}
},
"large_array": vec!["item"; 100]
},
"metadata": {
"timestamp": "2026-01-01T00:00:00Z",
"version": "1.0.0"
}
});
let json = json!({
"type": "tool_use",
"id": "tool_large",
"name": "complex_tool",
"input": complex_input
});
let block: ContentBlock = serde_json::from_value(json).unwrap();
match block {
ContentBlock::ToolUse { id, name, input } => {
assert_eq!(id, "tool_large");
assert_eq!(name, "complex_tool");
assert_eq!(input["config"]["nested"]["deeply"]["number"], 42);
assert_eq!(
input["config"]["large_array"].as_array().unwrap().len(),
100
);
}
_ => panic!("Expected ToolUse block"),
}
}
#[test]
fn test_unicode_in_text() {
let json = json!({
"type": "text",
"text": "Hello δΈη! π Emoji test: β
β π"
});
let block: ContentBlock = serde_json::from_value(json).unwrap();
match block {
ContentBlock::Text { text } => {
assert!(text.contains("δΈη"));
assert!(text.contains("π"));
assert!(text.contains("β
"));
}
_ => panic!("Expected Text block"),
}
}
}