unified-agent-api-claude-code 0.3.5

Async wrapper around the Claude Code CLI for non-interactive prompting
Documentation
mod support_paths;

use claude_code::{ClaudeStreamJsonErrorCode, ClaudeStreamJsonEvent, ClaudeStreamJsonParser};

fn read_fixture(name: &str) -> String {
    let path = support_paths::claude_code_stream_json_fixtures_dir().join(name);
    std::fs::read_to_string(path).expect("read fixture")
}

fn parse_single_line(name: &str) -> ClaudeStreamJsonEvent {
    let mut parser = ClaudeStreamJsonParser::new();
    let text = read_fixture(name);
    let line = text
        .lines()
        .find(|l| !l.chars().all(|c| c.is_whitespace()))
        .unwrap();
    parser.parse_line(line).unwrap().unwrap()
}

#[test]
fn parses_system_init_and_other() {
    assert!(matches!(
        parse_single_line("system_init.jsonl"),
        ClaudeStreamJsonEvent::SystemInit { .. }
    ));
    assert!(matches!(
        parse_single_line("system_other.jsonl"),
        ClaudeStreamJsonEvent::SystemOther { .. }
    ));
}

#[test]
fn result_discriminator_success_vs_error() {
    assert!(matches!(
        parse_single_line("result_success.jsonl"),
        ClaudeStreamJsonEvent::ResultSuccess { .. }
    ));
    assert!(matches!(
        parse_single_line("result_error.jsonl"),
        ClaudeStreamJsonEvent::ResultError { .. }
    ));
}

#[test]
fn normalize_is_emitted_only_for_result_inconsistency() {
    let mut parser = ClaudeStreamJsonParser::new();
    let line = read_fixture("result_inconsistent_is_error.jsonl");
    let err = parser.parse_line(line.trim()).unwrap_err();
    assert_eq!(err.code, ClaudeStreamJsonErrorCode::Normalize);
}

#[test]
fn unknown_outer_type_is_unknown_event_not_error() {
    assert!(matches!(
        parse_single_line("unknown_outer_type.jsonl"),
        ClaudeStreamJsonEvent::Unknown { .. }
    ));
}

#[test]
fn missing_required_path_for_known_type_is_typedparse() {
    let mut parser = ClaudeStreamJsonParser::new();
    let line = read_fixture("missing_required_path_typedparse.jsonl");
    let err = parser.parse_line(line.trim()).unwrap_err();
    assert_eq!(err.code, ClaudeStreamJsonErrorCode::TypedParse);
}

#[test]
fn stream_event_is_typed_and_preserves_inner_event_type() {
    let ev = parse_single_line("stream_event_text_delta.jsonl");
    match ev {
        ClaudeStreamJsonEvent::StreamEvent { stream, .. } => {
            assert_eq!(stream.event_type, "content_block_delta");
        }
        _ => panic!("expected StreamEvent"),
    }
}

#[test]
fn blank_lines_are_ignored_and_crlf_is_tolerated() {
    let mut parser = ClaudeStreamJsonParser::new();
    let text = read_fixture("blank_lines.jsonl");
    let mut count = 0usize;
    for line in text.lines() {
        if let Ok(Some(_)) = parser.parse_line(line) {
            count += 1;
        }
    }
    assert_eq!(count, 2);

    let mut parser = ClaudeStreamJsonParser::new();
    let text = read_fixture("crlf_lines.jsonl");
    for line in text.lines() {
        let with_crlf = format!("{line}\r");
        let out = parser.parse_line(&with_crlf).unwrap();
        assert!(out.is_some());
    }
}

#[test]
fn parse_json_matches_parse_line_taxonomy_for_typedparse_and_normalize() {
    let mut parser = ClaudeStreamJsonParser::new();

    let typedparse_value = serde_json::json!({"type":"user"});
    let err = parser.parse_json(&typedparse_value).unwrap_err();
    assert_eq!(err.code, ClaudeStreamJsonErrorCode::TypedParse);

    let normalize_value =
        serde_json::json!({"type":"result","subtype":"success","session_id":"s","is_error":true});
    let err = parser.parse_json(&normalize_value).unwrap_err();
    assert_eq!(err.code, ClaudeStreamJsonErrorCode::Normalize);
}