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.
This commit is contained in:
parent
ef1d8fdd31
commit
1ed4d8211f
6 changed files with 795 additions and 0 deletions
20
tests/fake_cli/fake-claude.sh
Executable file
20
tests/fake_cli/fake-claude.sh
Executable file
|
|
@ -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
|
||||
93
tests/transport_end_to_end.rs
Normal file
93
tests/transport_end_to_end.rs
Normal file
|
|
@ -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:?}"),
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue