multi-llm 1.0.0

Unified multi-provider LLM client with support for OpenAI, Anthropic, Ollama, and LMStudio
Documentation
//! Unit Tests for Response Structured Response Conversion
//!
//! UNIT UNDER TEST: Response conversion and validation
//!
//! BUSINESS RESPONSIBILITY:
//!   - Convert raw LLM JSON responses to typed StructuredResponse objects
//!   - Validate JSON schema compliance and handle parsing errors gracefully  
//!   - Preserve LLM metadata (token usage, model info) during conversion
//!   - Support multiple conversion patterns (into_structured, TryInto trait)
//!   - Provide meaningful error messages for malformed JSON responses
//!
//! TEST COVERAGE:
//!   - Successful conversion of valid structured JSON to StructuredResponse
//!   - Error handling for invalid/incomplete JSON structure  
//!   - Missing structured_response field handling with appropriate errors
//!   - TryInto trait implementation for ergonomic conversion patterns
//!   - Metadata preservation during structured response conversion
//!   - JSON schema validation failure scenarios and error reporting

use crate::provider::{Response, TokenUsage};
// Note: Structured response types no longer needed since we work with JSON directly
use serde_json::json;

/// Helper function to create a complete, valid structured response JSON for testing
fn create_valid_structured_json() -> serde_json::Value {
    json!({
        "conversation_response": {
            "message": "That sounds like a wonderful memory! Tell me more.",
            "confidence": 0.92,
            "response_type": "story_prompt"
        },
        "user_analysis": {
            "engagement_level": 0.85,
            "emotional_state": "nostalgic",
            "story_quality_score": 0.78,
            "frustration_level": 0.05,
            "coherence_score": 0.90
        },
        "story_elements": {
            "characters": [],
            "locations": [],
            "time_period": null,
            "themes": ["childhood"],
            "events": [],
            "emotions": [],
            "sensory_details": null
        },
        "response_metadata": {
            "suggested_follow_ups": ["What happened next?"],
            "conversation_phase": "story_development",
            "needs_clarification": false
        }
    })
}

/// Helper function to create Response with structured data for testing
fn create_llm_response_with_structured_data() -> Response {
    Response {
        content: "That sounds like a wonderful memory! Tell me more.".to_string(),
        structured_response: Some(create_valid_structured_json()),
        tool_calls: vec![],
        usage: Some(TokenUsage {
            prompt_tokens: 100,
            completion_tokens: 50,
            total_tokens: 150,
        }),
        model: Some("test-model".to_string()),
        raw_body: None,
    }
}

#[test]
fn test_executor_response_contains_valid_structured_json() {
    // Test verifies Response properly contains structured JSON data
    // with expected fields and values for conversation processing

    // Arrange - Create Response with valid structured JSON using helper
    let llm_response = create_llm_response_with_structured_data();

    // Act - Access structured response directly
    let structured_data = &llm_response.structured_response;

    // Assert - Should have structured data
    assert!(
        structured_data.is_some(),
        "Should have structured JSON data"
    );

    let json_data = structured_data.as_ref().unwrap();
    // Validate the structure contains expected fields
    assert!(json_data["conversation_response"]["response_type"].is_string());
    assert!(json_data["response_metadata"]["conversation_phase"].is_string());
    assert!(json_data["user_analysis"]["emotional_state"].is_string());

    // Verify business logic content
    assert_eq!(
        json_data["conversation_response"]["response_type"],
        "story_prompt"
    );
    assert_eq!(
        json_data["response_metadata"]["conversation_phase"],
        "story_development"
    );
    assert_eq!(
        json_data["conversation_response"]["message"],
        "That sounds like a wonderful memory! Tell me more."
    );
    assert_eq!(json_data["conversation_response"]["confidence"], 0.92);
    assert_eq!(
        json_data["conversation_response"]["response_type"],
        "story_prompt"
    );

    // Verify user analysis business metrics are present
    assert_eq!(json_data["user_analysis"]["engagement_level"], 0.85);

    // Verify story elements business data
    let themes = &json_data["story_elements"]["themes"];
    assert!(themes.is_array());
    assert_eq!(themes[0], "childhood");
}

// Temporarily commented out - these tests relied on conversion methods that are no longer needed
// since Response directly contains structured_response field
/*
#[test]
fn test_llm_response_into_structured_handles_invalid_json() {
    // RED: This should fail because into_structured() method doesn't exist yet

    // Arrange - Create Response with invalid structured JSON
    let invalid_json = json!({
        "conversation_response": {
            "message": "Valid message",
            // Missing required fields like confidence and response_type
        }
        // Missing required sections like user_analysis
    });

    let llm_response = Response {
        content: "Valid message".to_string(),
        structured_response: Some(invalid_json),
        tool_calls: vec![],
        usage: None,
        model: Some("test-model".to_string()),
            raw_body: None,
    };

    // Act - Try to convert (should fail because method doesn't exist)
    let result = llm_response.into_structured();

    // Assert - Should return meaningful error
    assert!(result.is_err(), "Should fail with invalid JSON structure");
    let error = result.unwrap_err();
    let error_msg = error.to_string();
    assert!(
        error_msg.contains("missing field") || error_msg.contains("required"),
        "Error should mention missing required fields: {}",
        error_msg
    );
}

#[test]
fn test_llm_response_into_structured_no_structured_data() {
    // RED: This should fail because into_structured() method doesn't exist yet

    // Arrange - Create Response without structured_response
    let llm_response = Response {
        content: "Plain text response".to_string(),
        structured_response: None,
        tool_calls: vec![],
        usage: None,
        model: Some("test-model".to_string()),
            raw_body: None,
    };

    // Act - Try to convert (should fail because method doesn't exist)
    let result = llm_response.into_structured();

    // Assert - Should return error about missing structured data
    assert!(
        result.is_err(),
        "Should fail when no structured response available"
    );
    let error = result.unwrap_err();
    let error_msg = error.to_string();
    assert!(
        error_msg.contains("No structured response") || error_msg.contains("not available"),
        "Error should mention missing structured response: {}",
        error_msg
    );
}

#[test]
fn test_try_into_structured_response_trait() {
    // RED: This should fail because TryInto<StructuredResponse> isn't implemented

    // Arrange
    let structured_json = json!({
        "conversation_response": {
            "message": "Test message",
            "confidence": 0.95,
            "response_type": "acknowledgment"
        },
        "user_analysis": {
            "engagement_level": 0.7,
            "emotional_state": "happy",
            "story_quality_score": 0.6,
            "frustration_level": 0.2,
            "coherence_score": 0.8
        },
        "story_elements": {
            "characters": [],
            "locations": [],
            "time_period": null,
            "themes": [],
            "events": [],
            "emotions": [],
            "sensory_details": null
        },
        "response_metadata": {
            "suggested_follow_ups": [],
            "conversation_phase": "introduction",
            "needs_clarification": false
        }
    });

    let llm_response = Response {
        content: "Test message".to_string(),
        structured_response: Some(structured_json),
        tool_calls: vec![],
        usage: None,
        model: Some("test-model".to_string()),
            raw_body: None,
    };

    // Act - Try to use TryInto (should fail because trait isn't implemented)
    let result: Result<StructuredResponse, _> = llm_response.try_into();

    // Assert - Should succeed
    assert!(
        result.is_ok(),
        "TryInto should work for valid structured JSON"
    );
    let structured = result.unwrap();
    assert_eq!(structured.conversation_response.message, "Test message");
    assert_eq!(structured.user_analysis.engagement_level, 0.7);
}

#[test]
fn test_structured_response_preserves_llm_metadata() {
    // RED: This should fail because conversion doesn't preserve metadata yet

    // Arrange
    let structured_json = json!({
        "conversation_response": {
            "message": "Metadata test",
            "confidence": 0.88,
            "response_type": "greeting"
        },
        "user_analysis": {
            "engagement_level": 0.75,
            "emotional_state": "excited",
            "story_quality_score": 0.65,
            "frustration_level": 0.1,
            "coherence_score": 0.9
        },
        "story_elements": {
            "characters": [],
            "locations": [],
            "time_period": null,
            "themes": [],
            "events": [],
            "emotions": [],
            "sensory_details": null
        },
        "response_metadata": {
            "suggested_follow_ups": ["How are you feeling?"],
            "conversation_phase": "introduction",
            "needs_clarification": false
        }
    });

    let llm_response = Response {
        content: "Metadata test".to_string(),
        structured_response: Some(structured_json),
        tool_calls: vec![],
        usage: Some(TokenUsage {
            prompt_tokens: 150,
            completion_tokens: 85,
            total_tokens: 235,
        }),
        raw_body: Some("raw response body".to_string()),
    };

    // Act - Try to convert with metadata preservation (should fail because method doesn't exist)
    let (structured, metadata) = llm_response.into_structured_with_metadata().unwrap();

    // Assert - Metadata should be preserved
    assert_eq!(structured.conversation_response.message, "Metadata test");
    assert_eq!(metadata.usage.unwrap().total_tokens, 235);
    assert_eq!(metadata.raw_body.unwrap(), "raw response body");
}
*/