claude-agent-sdk-rust/tests/transport_end_to_end.rs
Kayos 1ed4d8211f add examples, end-to-end tests, and Cargo.lock
Three examples:
- basic.rs: query() one-shot prompt + cost reporting.
- with_options.rs: system_prompt, model selection, effort, permission mode.
- interactive.rs: Client multi-turn session with two back-to-back sends.

Integration tests in tests/transport_end_to_end.rs spawn a tiny POSIX
shell script (tests/fake_cli/fake-claude.sh) as a stand-in for the real
claude CLI. The fake answers -v with a version, reads one user-message
frame on stdin, and emits a fixed assistant + result pair on stdout, then
blocks on stdin until close. This proves the spawn / write / read /
disconnect lifecycle works without an authenticated claude install.

Coverage:
- query() round-trip: stream yields Assistant then Result.
- Client round-trip: connect + send + drain to Result + disconnect.
- CLI-not-found surfaces as a typed Error::CliNotFound.

Cargo.lock is committed since this is, in practice, both a library and a
binary (the examples link the crate). Locking dev-deps avoids surprise
churn in CI.
2026-05-14 08:04:04 -07:00

93 lines
3.1 KiB
Rust

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