claude-code-sdk 0.0.3

Rust SDK for Claude Code
Documentation
use claude_code_sdk::{ClaudeCodeOptions, SafetyLimits};
use claude_code_sdk::transport::subprocess_cli::SubprocessCLITransport;

#[tokio::test]
async fn test_single_line_json_parsing() {
    // Test that single-line JSON continues to work as expected
    let options = ClaudeCodeOptions::default();
    let mut transport = SubprocessCLITransport::new("test", options, Some("nonexistent"))
        .expect("Should create transport");

    // Test single-line JSON processing
    let single_line_json = r#"{"type": "message", "content": "Hello World"}"#;
    
    let result = transport.process_line(single_line_json.to_string());
    assert!(result.is_some(), "Should parse single-line JSON");
    
    let parsed_result = result.unwrap();
    assert!(parsed_result.is_ok(), "Single-line JSON should parse successfully");
    
    let data = parsed_result.unwrap();
    assert_eq!(data.get("type").unwrap().as_str().unwrap(), "message");
    assert_eq!(data.get("content").unwrap().as_str().unwrap(), "Hello World");
}

#[tokio::test]
async fn test_multiline_json_reconstruction() {
    // Test that multiline JSON gets properly reconstructed
    let options = ClaudeCodeOptions::default();
    let mut transport = SubprocessCLITransport::new("test", options, Some("nonexistent"))
        .expect("Should create transport");

    // Simulate multiline JSON as it might come from CLI
    let json_lines = vec![
        r#"{"#,
        r#"  "type": "assistant_message","#,
        r#"  "content": ["#,
        r#"    {"#,
        r#"      "type": "text","#,
        r#"      "text": "This is a multi-line response""#,
        r#"    }"#,
        r#"  ],"#,
        r#"  "turn": 1"#,
        r#"}"#,
    ];

    let mut final_result = None;

    // Process each line
    for (i, line) in json_lines.iter().enumerate() {
        println!("Processing line {}: {}", i, line);
        let result = transport.process_line(line.to_string());
        println!("Result for line {}: {:?}", i, result.is_some());
        
        if i < json_lines.len() - 1 {
            // Intermediate lines should not return a complete result
            if result.is_some() {
                println!("Unexpected result at line {}: {:?}", i, result);
            }
            assert!(result.is_none(), "Line {} should not yet produce a complete JSON result", i);
        } else {
            // Final line should complete the JSON
            if result.is_none() {
                println!("No result for final line, trying to manually parse buffer");
                let manual_result = transport.try_parse_json_buffer();
                println!("Manual parse result: {:?}", manual_result.is_some());
                final_result = manual_result;
            } else {
                final_result = result;
            }
            assert!(final_result.is_some(), "Final line should complete the JSON");
        }
    }

    // Verify the final parsed result
    let parsed_result = final_result.unwrap();
    assert!(parsed_result.is_ok(), "Multiline JSON should parse successfully");
    
    let data = parsed_result.unwrap();
    assert_eq!(data.get("type").unwrap().as_str().unwrap(), "assistant_message");
    assert_eq!(data.get("turn").unwrap().as_u64().unwrap(), 1);
}

#[tokio::test]
async fn test_malformed_json_handling() {
    // Test handling of malformed JSON
    let options = ClaudeCodeOptions::default();
    let mut transport = SubprocessCLITransport::new("test", options, Some("nonexistent"))
        .expect("Should create transport");

    // Start with valid JSON opening
    let result1 = transport.process_line(r#"{"#.to_string());
    assert!(result1.is_none(), "Should wait for more JSON data");

    // Add malformed content
    let result2 = transport.process_line(r#"  "type": "message""#.to_string());
    assert!(result2.is_none(), "Should still wait for more data");

    // Add invalid closing (missing comma)
    let result3 = transport.process_line(r#"  "invalid": syntax"#.to_string());
    assert!(result3.is_none(), "Should still be accumulating");

    // Force parsing by trying to parse current buffer directly
    let parse_result = transport.try_parse_json_buffer();
    assert!(parse_result.is_none(), "Malformed JSON should not parse");
}

#[tokio::test]
async fn test_non_json_lines_ignored() {
    // Test that non-JSON lines are properly ignored
    let options = ClaudeCodeOptions::default();
    let mut transport = SubprocessCLITransport::new("test", options, Some("nonexistent"))
        .expect("Should create transport");

    // Send non-JSON lines
    let result1 = transport.process_line("Some debug output".to_string());
    assert!(result1.is_none(), "Non-JSON line should be ignored");

    let result2 = transport.process_line("Another log message".to_string());
    assert!(result2.is_none(), "Non-JSON line should be ignored");

    // Send valid JSON
    let result3 = transport.process_line(r#"{"type": "test"}"#.to_string());
    assert!(result3.is_some(), "Valid JSON should be parsed");
    assert!(result3.unwrap().is_ok(), "Valid JSON should parse successfully");
}

#[tokio::test]
async fn test_multiple_json_objects() {
    // Test handling multiple separate JSON objects
    let options = ClaudeCodeOptions::default();
    let mut transport = SubprocessCLITransport::new("test", options, Some("nonexistent"))
        .expect("Should create transport");

    // First JSON object
    let result1 = transport.process_line(r#"{"type": "start", "id": 1}"#.to_string());
    assert!(result1.is_some(), "First JSON should be parsed");
    let data1 = result1.unwrap().unwrap();
    assert_eq!(data1.get("type").unwrap().as_str().unwrap(), "start");

    // Second JSON object
    let result2 = transport.process_line(r#"{"type": "end", "id": 2}"#.to_string());
    assert!(result2.is_some(), "Second JSON should be parsed");
    let data2 = result2.unwrap().unwrap();
    assert_eq!(data2.get("type").unwrap().as_str().unwrap(), "end");
}

#[tokio::test]
async fn test_json_buffer_safety_limits() {
    // Test that safety limits are enforced on JSON buffer
    let options = ClaudeCodeOptions::default();
    let conservative_limits = SafetyLimits::conservative();
    let mut transport = SubprocessCLITransport::new("test", options, Some("nonexistent"))
        .expect("Should create transport")
        .with_safety_limits(conservative_limits);

    // Start JSON object
    let result1 = transport.process_line(r#"{"#.to_string());
    assert!(result1.is_none(), "Should start accumulating JSON");

    // Add a very large text field that exceeds conservative limits
    let large_text = "A".repeat(2_000_000); // 2MB, exceeds conservative 1MB limit
    let large_line = format!(r#"  "large_field": "{}""#, large_text);
    
    let result2 = transport.process_line(large_line);
    // This should trigger a safety limit error
    assert!(result2.is_some(), "Should trigger safety limit");
    
    let error_result = result2.unwrap();
    assert!(error_result.is_err(), "Should be a safety error");
}

#[tokio::test]
async fn test_empty_lines_handling() {
    // Test that empty lines are properly ignored
    let options = ClaudeCodeOptions::default();
    let mut transport = SubprocessCLITransport::new("test", options, Some("nonexistent"))
        .expect("Should create transport");

    // Send empty lines
    let result1 = transport.process_line("".to_string());
    assert!(result1.is_none(), "Empty line should be ignored");

    let result2 = transport.process_line("   ".to_string()); // Whitespace only
    assert!(result2.is_none(), "Whitespace-only line should be ignored");

    // Send valid JSON after empty lines
    let result3 = transport.process_line(r#"{"test": true}"#.to_string());
    assert!(result3.is_some(), "Valid JSON should be parsed after empty lines");
}

#[tokio::test]
async fn test_mixed_single_and_multiline_json() {
    // Test mixing single-line and multiline JSON
    let options = ClaudeCodeOptions::default();
    let mut transport = SubprocessCLITransport::new("test", options, Some("nonexistent"))
        .expect("Should create transport");

    // Single-line JSON
    let result1 = transport.process_line(r#"{"type": "single", "line": true}"#.to_string());
    assert!(result1.is_some(), "Single-line JSON should parse");
    
    // Start multiline JSON
    let result2 = transport.process_line(r#"{"#.to_string());
    assert!(result2.is_none(), "Should start multiline accumulation");
    
    let result3 = transport.process_line(r#"  "type": "multi","#.to_string());
    assert!(result3.is_none(), "Should continue accumulation");
    
    let result4 = transport.process_line(r#"  "line": false"#.to_string());
    assert!(result4.is_none(), "Should continue accumulation");
    
    let result5 = transport.process_line(r#"}"#.to_string());
    assert!(result5.is_some(), "Multiline JSON should complete");
    
    // Another single-line JSON
    let result6 = transport.process_line(r#"{"type": "another", "single": true}"#.to_string());
    assert!(result6.is_some(), "Another single-line JSON should parse");
}