diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..816cd6a --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,537 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "async-stream" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476" +dependencies = [ + "async-stream-impl", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-stream-impl" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "bitflags" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "claude-agent-sdk" +version = "0.0.1" +dependencies = [ + "anyhow", + "async-stream", + "futures-core", + "futures-util", + "serde", + "serde_json", + "thiserror", + "tokio", + "tokio-stream", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-macro" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-core", + "futures-macro", + "futures-task", + "pin-project-lite", + "slab", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "mio" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" +dependencies = [ + "libc", + "wasi", + "windows-sys", +] + +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "socket2" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "tokio" +version = "1.52.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" +dependencies = [ + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys", +] + +[[package]] +name = "tokio-macros" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-stream" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex-automata", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/examples/basic.rs b/examples/basic.rs new file mode 100644 index 0000000..55eb87d --- /dev/null +++ b/examples/basic.rs @@ -0,0 +1,50 @@ +//! Minimal `query()` example. +//! +//! Run: +//! +//! ```sh +//! cargo run --example basic +//! ``` +//! +//! Requires the `claude` CLI on `PATH` and authenticated. Output prints each +//! assistant text block, then the total cost from the terminal `result` +//! message. + +use claude_agent_sdk::{ClaudeAgentOptions, ContentBlock, Message, query}; +use tokio_stream::StreamExt; + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + tracing_subscriber::fmt() + .with_env_filter( + tracing_subscriber::EnvFilter::try_from_default_env() + .unwrap_or_else(|_| "claude_agent_sdk=info".into()), + ) + .init(); + + let opts = ClaudeAgentOptions::new().with_max_turns(1); + + let mut stream = query("What is 2 + 2? Reply with just the digit.", opts).await?; + + while let Some(item) = stream.next().await { + match item? { + Message::Assistant(a) => { + for block in &a.message.content { + if let ContentBlock::Text(t) = block { + println!("Claude: {}", t.text); + } + } + } + Message::Result(r) => { + println!( + "\n[result] subtype={} turns={} cost_usd={:?}", + r.subtype, r.num_turns, r.total_cost_usd + ); + break; + } + _ => {} + } + } + + Ok(()) +} diff --git a/examples/interactive.rs b/examples/interactive.rs new file mode 100644 index 0000000..ea3fd9e --- /dev/null +++ b/examples/interactive.rs @@ -0,0 +1,52 @@ +//! Multi-turn [`Client`] example. +//! +//! Sends two prompts back-to-back without re-spawning the CLI subprocess — +//! mirrors the Python SDK's `ClaudeSDKClient` async-context pattern. +//! +//! Run: +//! +//! ```sh +//! cargo run --example interactive +//! ``` + +use claude_agent_sdk::{ClaudeAgentOptions, Client, ContentBlock, Message}; +use tokio_stream::StreamExt; + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + let mut client = Client::new(ClaudeAgentOptions::new().with_max_turns(1)).await?; + client.connect().await?; + let mut stream = client.messages(); + + client.send("Say 'one'.").await?; + drain_until_result(&mut stream, "turn 1").await?; + + client.send("Say 'two'.").await?; + drain_until_result(&mut stream, "turn 2").await?; + + client.disconnect().await?; + Ok(()) +} + +async fn drain_until_result( + stream: &mut (impl tokio_stream::Stream> + Unpin), + label: &str, +) -> anyhow::Result<()> { + while let Some(item) = stream.next().await { + match item? { + Message::Assistant(a) => { + for block in &a.message.content { + if let ContentBlock::Text(t) = block { + println!("[{label}] Claude: {}", t.text); + } + } + } + Message::Result(_) => { + println!("[{label}] done"); + return Ok(()); + } + _ => {} + } + } + Ok(()) +} diff --git a/examples/with_options.rs b/examples/with_options.rs new file mode 100644 index 0000000..63ae982 --- /dev/null +++ b/examples/with_options.rs @@ -0,0 +1,43 @@ +//! `query()` with a custom system prompt, model selection, and `cwd`. +//! +//! Run: +//! +//! ```sh +//! cargo run --example with_options +//! ``` + +use claude_agent_sdk::{ + ClaudeAgentOptions, ContentBlock, Effort, Message, PermissionMode, query, +}; +use tokio_stream::StreamExt; + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + let opts = ClaudeAgentOptions::new() + .with_system_prompt("You are a senior Rust engineer. Be terse.") + .with_model("claude-sonnet-4-5") + .with_max_turns(1) + .with_effort(Effort::Low) + .with_permission_mode(PermissionMode::Plan); + + let mut stream = query( + "In one sentence, what does the `?` operator do in Rust?", + opts, + ) + .await?; + + while let Some(item) = stream.next().await { + match item? { + Message::Assistant(a) => { + for block in &a.message.content { + if let ContentBlock::Text(t) = block { + println!("{}", t.text); + } + } + } + Message::Result(_) => break, + _ => {} + } + } + Ok(()) +} diff --git a/tests/fake_cli/fake-claude.sh b/tests/fake_cli/fake-claude.sh new file mode 100755 index 0000000..d005077 --- /dev/null +++ b/tests/fake_cli/fake-claude.sh @@ -0,0 +1,20 @@ +#!/bin/sh +# Tiny fake `claude` CLI for integration tests. Mimics the wire protocol: +# reads newline-delimited JSON frames on stdin, emits assistant + result +# frames on stdout. Echoes whatever the user said. +# +# Args: ignores everything except `-v` (prints a version, exits 0). + +if [ "$1" = "-v" ]; then + echo "2.1.0 (Claude Code)" + exit 0 +fi + +# Read one user-message frame (we don't actually parse it — just block on stdin). +# After the first newline we emit one assistant message + one result message. +read -r FIRST_LINE +printf '{"type":"assistant","message":{"role":"assistant","content":[{"type":"text","text":"hello from fake"}],"model":"fake-model"},"session_id":"s1","uuid":"u1"}\n' +printf '{"type":"result","subtype":"success","duration_ms":1,"duration_api_ms":1,"is_error":false,"num_turns":1,"session_id":"s1","total_cost_usd":0.0001}\n' + +# Stay alive until stdin closes (so the parent can drain stdout cleanly). +cat > /dev/null diff --git a/tests/transport_end_to_end.rs b/tests/transport_end_to_end.rs new file mode 100644 index 0000000..27f73e6 --- /dev/null +++ b/tests/transport_end_to_end.rs @@ -0,0 +1,93 @@ +//! End-to-end integration tests that wire the transport up to a tiny fake +//! `claude` binary (`tests/fake_cli/fake-claude.sh`). +//! +//! This proves that the spawn/write/read/close lifecycle works against a real +//! subprocess on a real platform, without needing an authenticated `claude` +//! install. The fake CLI ignores arguments, reads one stdin frame, and emits +//! a fixed assistant + result pair on stdout. + +use std::path::PathBuf; + +use claude_agent_sdk::{ClaudeAgentOptions, Client, ContentBlock, Message, query}; +use tokio_stream::StreamExt; + +fn fake_cli_path() -> PathBuf { + let mut p = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + p.push("tests"); + p.push("fake_cli"); + p.push("fake-claude.sh"); + assert!(p.is_file(), "fake-claude.sh missing at {}", p.display()); + p +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn query_round_trip_against_fake_cli() { + let opts = ClaudeAgentOptions::new() + .with_cli_path(fake_cli_path()) + .with_skip_version_check(true); + + let mut stream = query("Hi!", opts).await.expect("query start"); + + let mut saw_assistant = false; + let mut saw_result = false; + while let Some(item) = stream.next().await { + let msg = item.expect("frame"); + match msg { + Message::Assistant(a) => { + saw_assistant = true; + assert_eq!(a.message.model, "fake-model"); + match &a.message.content[0] { + ContentBlock::Text(t) => assert_eq!(t.text, "hello from fake"), + other => panic!("expected TextBlock, got {other:?}"), + } + } + Message::Result(r) => { + saw_result = true; + assert_eq!(r.subtype, "success"); + assert_eq!(r.num_turns, 1); + break; + } + _ => {} + } + } + assert!(saw_assistant); + assert!(saw_result); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn client_round_trip_against_fake_cli() { + let opts = ClaudeAgentOptions::new() + .with_cli_path(fake_cli_path()) + .with_skip_version_check(true); + + let mut client = Client::new(opts).await.expect("client new"); + client.connect().await.expect("connect"); + let mut stream = client.messages(); + client.send("ping").await.expect("send"); + + let mut saw_result = false; + while let Some(item) = stream.next().await { + let msg = item.expect("frame"); + if matches!(msg, Message::Result(_)) { + saw_result = true; + break; + } + } + assert!(saw_result); + + client.disconnect().await.expect("disconnect"); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn cli_not_found_surfaces_typed_error() { + let bogus = PathBuf::from("/nonexistent/path/to/claude"); + let opts = ClaudeAgentOptions::new() + .with_cli_path(bogus) + .with_skip_version_check(true); + + match query("hi", opts).await { + Ok(_) => panic!("expected CliNotFound, got Ok"), + Err(claude_agent_sdk::Error::CliNotFound(_)) => {} + Err(other) => panic!("expected CliNotFound, got {other:?}"), + } +}