mod support;
use anthropic_async::types::content::ContentBlock;
use anthropic_async::types::content::ContentBlockParam;
use anthropic_async::types::content::MessageContentParam;
use anthropic_async::types::content::MessageParam;
use anthropic_async::types::content::MessageRole;
use anthropic_async::types::content::ToolResultContent;
use anthropic_async::types::messages::MessagesCreateRequest;
use anthropic_async::types::tools::Tool;
use anthropic_async::types::tools::ToolChoice;
use insta::assert_json_snapshot;
use support::snapshots::SnapshotHarness;
fn weather_tool() -> Tool {
Tool {
name: "get_weather".into(),
description: Some("Get the current weather for a location".into()),
input_schema: serde_json::json!({
"type": "object",
"properties": {
"location": {
"type": "string",
"description": "The city and country, e.g., 'Paris, France'"
}
},
"required": ["location"]
}),
cache_control: None,
strict: None,
}
}
fn extract_tool_use_id(content: &[ContentBlock]) -> Option<String> {
content.iter().find_map(|block| match block {
ContentBlock::ToolUse { id, .. } => Some(id.clone()),
_ => None,
})
}
#[tokio::test]
#[expect(clippy::too_many_lines)]
async fn multi_turn_tool_conversation() {
let harness = SnapshotHarness::new("multi_turn_tool_conversation").await;
if harness.is_live() {
eprintln!("Running in LIVE mode against {}", harness.base_url());
} else {
assert!(harness.has_server(), "Replay mode requires a mock server");
}
let client = harness.client();
let user_message = MessageParam {
role: MessageRole::User,
content: "What's the weather like in Paris right now?".into(),
};
let req1 = MessagesCreateRequest {
model: "claude-sonnet-4-20250514".into(),
max_tokens: 256,
temperature: Some(0.0),
messages: vec![user_message.clone()],
tools: Some(vec![weather_tool()]),
tool_choice: Some(ToolChoice::Tool {
name: "get_weather".into(),
disable_parallel_tool_use: Some(true),
}),
..Default::default()
};
let resp1 = client
.messages()
.create(req1)
.await
.expect("First turn should succeed");
let has_tool_use = resp1
.content
.iter()
.any(|b| matches!(b, ContentBlock::ToolUse { .. }));
assert!(
has_tool_use,
"Expected assistant to return tool_use block, got: {:?}",
resp1.content
);
let tool_use_id = extract_tool_use_id(&resp1.content).expect("Should have tool_use ID");
let tool_name = resp1.content.iter().find_map(|block| match block {
ContentBlock::ToolUse { name, .. } => Some(name.clone()),
_ => None,
});
assert_eq!(
tool_name.as_deref(),
Some("get_weather"),
"Expected get_weather tool, got: {tool_name:?}"
);
assert_json_snapshot!("turn1_response", &resp1, {
".id" => "[redacted]",
".content[].id" => "[redacted]",
});
let assistant_msg = resp1
.try_into_message_param()
.expect("Should be able to convert response to MessageParam");
assert_eq!(assistant_msg.role, MessageRole::Assistant);
match &assistant_msg.content {
MessageContentParam::Blocks(blocks) => {
let has_tool_use_param = blocks
.iter()
.any(|b| matches!(b, ContentBlockParam::ToolUse { .. }));
assert!(
has_tool_use_param,
"Echoed message should contain ToolUse block"
);
}
MessageContentParam::String(_) => panic!("Expected Blocks content after echo conversion"),
}
let tool_result = ContentBlockParam::ToolResult {
tool_use_id,
content: Some(ToolResultContent::String(
"Currently sunny with a temperature of 22°C (72°F). Light breeze from the west.".into(),
)),
is_error: None,
cache_control: None,
};
let tool_result_message = MessageParam {
role: MessageRole::User,
content: MessageContentParam::Blocks(vec![tool_result]),
};
let req2 = MessagesCreateRequest {
model: "claude-sonnet-4-20250514".into(),
max_tokens: 256,
temperature: Some(0.0),
messages: vec![user_message, assistant_msg, tool_result_message],
tools: Some(vec![weather_tool()]),
tool_choice: Some(ToolChoice::None),
..Default::default()
};
let resp2 = client
.messages()
.create(req2)
.await
.expect("Second turn should succeed");
assert_json_snapshot!("turn2_response", &resp2, {
".id" => "[redacted]",
});
let has_text = resp2
.content
.iter()
.any(|b| matches!(b, ContentBlock::Text { .. }));
assert!(
has_text,
"Expected assistant to return text response, got: {:?}",
resp2.content
);
let text_content: String = resp2
.content
.iter()
.filter_map(|block| match block {
ContentBlock::Text { text, .. } => Some(text.clone()),
_ => None,
})
.collect();
let text_lc = text_content.to_lowercase();
assert!(
text_lc.contains("paris")
&& (text_lc.contains("sunny") || text_lc.contains("22") || text_lc.contains("72")),
"Expected response to reference Paris AND weather data (sunny/22/72), got: {text_content}"
);
}
#[test]
fn echo_pattern_preserves_tool_use_details() {
use anthropic_async::types::content::ContentBlockParam;
let tool_use = ContentBlock::ToolUse {
id: "toolu_01XYZ".into(),
name: "get_weather".into(),
input: serde_json::json!({
"location": "Paris, France"
}),
};
let param = ContentBlockParam::try_from(&tool_use).expect("ToolUse should be convertible");
match param {
ContentBlockParam::ToolUse {
id,
name,
input,
cache_control,
} => {
assert_eq!(id, "toolu_01XYZ");
assert_eq!(name, "get_weather");
assert_eq!(input["location"], "Paris, France");
assert!(
cache_control.is_none(),
"cache_control should be None after conversion"
);
}
_ => panic!("Expected ToolUse variant"),
}
}
#[test]
fn echo_pattern_serialization() {
let tool_use = ContentBlock::ToolUse {
id: "toolu_test123".into(),
name: "calculator".into(),
input: serde_json::json!({ "expression": "2 + 2" }),
};
let param = ContentBlockParam::try_from(&tool_use).unwrap();
let json = serde_json::to_value(¶m).expect("serialization should succeed");
assert_eq!(json["type"], "tool_use");
assert_eq!(json["id"], "toolu_test123");
assert_eq!(json["name"], "calculator");
assert_eq!(json["input"]["expression"], "2 + 2");
}
mod unit_tests {
use super::*;
#[test]
fn weather_tool_schema_valid() {
let tool = weather_tool();
assert_eq!(tool.name, "get_weather");
assert!(tool.description.is_some());
assert!(tool.input_schema.is_object());
assert!(tool.input_schema["properties"]["location"].is_object());
}
#[test]
fn extract_tool_use_id_finds_id() {
let content = vec![
ContentBlock::Text {
text: "Let me check the weather.".into(),
citations: None,
},
ContentBlock::ToolUse {
id: "toolu_abc123".into(),
name: "get_weather".into(),
input: serde_json::json!({}),
},
];
let id = extract_tool_use_id(&content);
assert_eq!(id, Some("toolu_abc123".into()));
}
#[test]
fn extract_tool_use_id_returns_none_when_no_tool_use() {
let content = vec![ContentBlock::Text {
text: "Just text.".into(),
citations: None,
}];
let id = extract_tool_use_id(&content);
assert!(id.is_none());
}
}