aether-acp-utils 0.3.21

Agent Client Protocol (ACP) utilities for the Aether AI agent framework
Documentation
use acp_utils::client::TokioAcpAgent;
use agent_client_protocol::{Client, ConnectTo};
use std::path::Path;
use std::str::FromStr;
use tokio::task::LocalSet;

#[test]
fn parses_shell_command() {
    let agent = TokioAcpAgent::from_str("aether acp --foo bar").expect("parses");
    let server = agent.stdio();
    assert_eq!(server.command.as_path(), Path::new("aether"));
    assert_eq!(server.args.as_slice(), &["acp", "--foo", "bar"]);
}

#[test]
fn parses_quoted_shell_command() {
    let agent = TokioAcpAgent::from_str(r#"python "my agent.py" --name "Test Agent""#).expect("parses");
    let server = agent.stdio();
    assert_eq!(server.command.as_path(), Path::new("python"));
    assert_eq!(server.args.as_slice(), &["my agent.py", "--name", "Test Agent"]);
}

#[test]
fn parses_leading_environment_variables() {
    let agent = TokioAcpAgent::from_str("RUST_LOG=debug aether acp").expect("parses");
    let server = agent.stdio();
    assert_eq!(server.command.as_path(), Path::new("aether"));
    assert_eq!(server.args.as_slice(), &["acp"]);
    assert_eq!(server.env[0].name, "RUST_LOG");
    assert_eq!(server.env[0].value, "debug");
}

#[test]
fn parses_json_stdio_agent_config() {
    let agent = TokioAcpAgent::from_str(
        r#"{"type":"stdio","name":"test-agent","command":"/usr/bin/python","args":["agent.py","--verbose"],"env":[{"name":"RUST_LOG","value":"debug"}]}"#,
    )
    .expect("parses");

    let server = agent.stdio();
    assert_eq!(server.command.as_path(), Path::new("/usr/bin/python"));
    assert_eq!(server.args.as_slice(), &["agent.py", "--verbose"]);
    assert_eq!(server.env[0].name, "RUST_LOG");
    assert_eq!(server.env[0].value, "debug");
}

#[test]
fn rejects_empty_command() {
    assert!(TokioAcpAgent::from_str("").is_err());
    assert!(TokioAcpAgent::from_str("   ").is_err());
}

#[test]
fn rejects_non_stdio_transport_at_parse_time() {
    let json = r#"{"type":"http","name":"remote","url":"https://example.com/agent","headers":[]}"#;
    assert!(TokioAcpAgent::from_str(json).is_err());
}

#[tokio::test(flavor = "current_thread")]
async fn agent_exited_message_includes_stderr_and_status() {
    LocalSet::new()
        .run_until(async {
            let agent = TokioAcpAgent::from_str("/bin/ls /nonexistent-aether-test-path-12345").expect("parses");
            let result = ConnectTo::<Client>::connect_to(agent, Client.builder()).await;

            let err = result.expect_err("child exited with non-zero status");
            let msg = format!("{err}");
            assert!(msg.contains("exited"), "expected exit info in error: {msg}");
            assert!(msg.contains("No such file"), "expected stderr in error: {msg}");
        })
        .await;
}