use std::time::Duration;
use claude_code_sdk_rust::{ClaudeAgentClient, ClaudeAgentOptions, PermissionResult, StreamEvent};
const TOOL_CAPABLE_MODEL: &str = "claude-haiku-4-5-20251001";
const STREAM_TIMEOUT: Duration = Duration::from_secs(180);
fn has_real_claude_auth() -> bool {
std::env::var_os("ANTHROPIC_API_KEY").is_some()
|| std::env::var_os("CLAUDE_CODE_OAUTH_TOKEN").is_some()
|| has_logged_in_claude_cli()
}
fn has_logged_in_claude_cli() -> bool {
let cli_responds = std::process::Command::new("claude")
.arg("--version")
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status()
.map(|status| status.success())
.unwrap_or(false);
let has_user_config = std::env::var_os("HOME")
.map(|home| std::path::Path::new(&home).join(".claude.json").exists())
.unwrap_or(false);
cli_responds && has_user_config
}
fn looks_unavailable(text: &str) -> bool {
let lowered = text.to_ascii_lowercase();
lowered.contains("currently unavailable")
|| lowered.contains("is unavailable")
|| lowered.contains("does not have access")
|| lowered.contains("model not found")
|| lowered.contains("unknown model")
}
fn looks_web_unavailable(text: &str) -> bool {
let lowered = text.to_ascii_lowercase();
lowered.contains("web search is not")
|| lowered.contains("websearch is not")
|| lowered.contains("web fetch is not")
|| lowered.contains("webfetch is not")
|| lowered.contains("not enabled")
|| lowered.contains("not available")
|| lowered.contains("don't have access to the web")
|| lowered.contains("do not have access to the web")
|| lowered.contains("don't have the ability to")
|| lowered.contains("unable to access the internet")
|| lowered.contains("cannot access the internet")
|| lowered.contains("no internet")
}
struct Captured {
content: String,
tool_starts: Vec<String>,
errors: Vec<String>,
saw_complete: bool,
}
impl Captured {
fn used_tool(&self, needle: &str) -> bool {
let needle = needle.to_ascii_lowercase();
self.tool_starts
.iter()
.any(|name| name.to_ascii_lowercase().contains(&needle))
}
}
async fn drain_stream(mut events: tokio::sync::mpsc::UnboundedReceiver<StreamEvent>) -> Captured {
let mut cap = Captured {
content: String::new(),
tool_starts: Vec::new(),
errors: Vec::new(),
saw_complete: false,
};
let _ = tokio::time::timeout(STREAM_TIMEOUT, async {
while let Some(event) = events.recv().await {
match event {
StreamEvent::ContentChunk(text) => cap.content.push_str(&text),
StreamEvent::ToolUseStart { name, .. } => cap.tool_starts.push(name),
StreamEvent::Complete(_) | StreamEvent::TurnComplete(_) => cap.saw_complete = true,
StreamEvent::Error(message) => cap.errors.push(message),
_ => {}
}
}
})
.await;
cap
}
fn web_tool_options() -> ClaudeAgentOptions {
ClaudeAgentOptions::builder()
.model(TOOL_CAPABLE_MODEL)
.include_partial_messages(true)
.max_turns(4)
.allowed_tools(vec!["WebSearch".to_string(), "WebFetch".to_string()])
.can_use_tool(|_tool_name, _input, _ctx| async move { Ok(PermissionResult::allow()) })
.build()
}
#[tokio::test]
#[ignore = "requires Claude CLI authentication, web access, and may incur API usage"]
async fn real_claude_cli_uses_web_search_tool() {
if !has_real_claude_auth() {
eprintln!("skipping: Claude CLI authentication is required");
return;
}
let events = ClaudeAgentClient::spawn_stream_message(
web_tool_options(),
"Use the WebSearch tool to find the official Rust programming language \
website, then reply with the website's domain (for example example.com) \
and nothing else.",
);
let cap = drain_stream(events).await;
if cap.errors.iter().any(|e| looks_unavailable(e)) {
eprintln!("skipping: model unavailable: {:?}", cap.errors);
return;
}
if cap.errors.iter().any(|e| looks_web_unavailable(e)) {
eprintln!("skipping: web tool unavailable: {:?}", cap.errors);
return;
}
assert!(cap.errors.is_empty(), "unexpected stream errors: {:?}", cap.errors);
assert!(cap.saw_complete, "stream ended without a Complete/TurnComplete event");
if !cap.used_tool("websearch") && !cap.used_tool("web_search") {
if looks_web_unavailable(&cap.content) {
eprintln!("skipping: web search unavailable: {:?}", cap.content);
return;
}
panic!(
"expected a WebSearch ToolUseStart event; tools seen: {:?}, content: {:?}",
cap.tool_starts, cap.content
);
}
assert!(
cap.content.to_ascii_lowercase().contains("rust-lang.org"),
"expected the web search result (rust-lang.org) in the reply, got {:?}",
cap.content
);
}
#[tokio::test]
#[ignore = "requires Claude CLI authentication, web access, and may incur API usage"]
async fn real_claude_cli_uses_web_fetch_tool() {
if !has_real_claude_auth() {
eprintln!("skipping: Claude CLI authentication is required");
return;
}
let events = ClaudeAgentClient::spawn_stream_message(
web_tool_options(),
"Use the WebFetch tool to fetch https://example.com and reply with the \
main heading text on that page and nothing else.",
);
let cap = drain_stream(events).await;
if cap.errors.iter().any(|e| looks_unavailable(e)) {
eprintln!("skipping: model unavailable: {:?}", cap.errors);
return;
}
if cap.errors.iter().any(|e| looks_web_unavailable(e)) {
eprintln!("skipping: web tool unavailable: {:?}", cap.errors);
return;
}
assert!(cap.errors.is_empty(), "unexpected stream errors: {:?}", cap.errors);
assert!(cap.saw_complete, "stream ended without a Complete/TurnComplete event");
if !cap.used_tool("webfetch") && !cap.used_tool("web_fetch") {
if looks_web_unavailable(&cap.content) {
eprintln!("skipping: web fetch unavailable: {:?}", cap.content);
return;
}
panic!(
"expected a WebFetch ToolUseStart event; tools seen: {:?}, content: {:?}",
cap.tool_starts, cap.content
);
}
assert!(
cap.content.to_ascii_lowercase().contains("example domain"),
"expected the fetched page heading ('Example Domain') in the reply, got {:?}",
cap.content
);
}