//! 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:?}"), } }