decy-llm 2.2.0

LLM context builder for Decy C-to-Rust transpiler
Documentation
//! Tests for LLM context builder (DECY-098).
//!
//! Verifies that static analysis results are properly serialized
//! as structured JSON for LLM prompts.

use decy_llm::{AnalysisContext, ContextBuilder};

// ============================================================================
// TEST 1: Create empty context builder
// ============================================================================

#[test]
fn test_create_context_builder() {
    let builder = ContextBuilder::new();
    let context = builder.build();
    assert!(context.functions.is_empty(), "Empty builder should have no functions");
}

// ============================================================================
// TEST 2: Add function to context
// ============================================================================

#[test]
fn test_add_function_to_context() {
    let mut builder = ContextBuilder::new();
    builder.add_function("process", "void process(int* data, size_t len)");

    let context = builder.build();
    assert_eq!(context.functions.len(), 1);
    assert_eq!(context.functions[0].name, "process");
    assert_eq!(context.functions[0].c_signature, "void process(int* data, size_t len)");
}

// ============================================================================
// TEST 3: Add ownership inference
// ============================================================================

#[test]
fn test_add_ownership_inference() {
    let mut builder = ContextBuilder::new();
    builder
        .add_function("transfer", "void transfer(int* dest, int* src)")
        .add_ownership("transfer", "dest", "mutable_borrow", 0.95, "Mutated via assignment")
        .add_ownership("transfer", "src", "immutable_borrow", 0.9, "Read-only access");

    let context = builder.build();
    let func = &context.functions[0];

    assert!(func.ownership.contains_key("dest"));
    assert!(func.ownership.contains_key("src"));

    let dest_ownership = &func.ownership["dest"];
    assert_eq!(dest_ownership.kind, "mutable_borrow");
    assert!((dest_ownership.confidence - 0.95).abs() < 0.01);

    let src_ownership = &func.ownership["src"];
    assert_eq!(src_ownership.kind, "immutable_borrow");
}

// ============================================================================
// TEST 4: Add lifetime information
// ============================================================================

#[test]
fn test_add_lifetime_info() {
    let mut builder = ContextBuilder::new();
    builder.add_function("create", "int* create()").add_lifetime("create", "result", 0, true); // Escapes function

    let context = builder.build();
    let func = &context.functions[0];

    assert_eq!(func.lifetimes.len(), 1);
    assert_eq!(func.lifetimes[0].variable, "result");
    assert_eq!(func.lifetimes[0].scope_depth, 0);
    assert!(func.lifetimes[0].escapes);
}

// ============================================================================
// TEST 5: Add lock-to-data mapping
// ============================================================================

#[test]
fn test_add_lock_mapping() {
    let mut builder = ContextBuilder::new();
    builder.add_function("sync_update", "void sync_update()").add_lock_mapping(
        "sync_update",
        "counter_mutex",
        vec!["counter".to_string(), "total".to_string()],
    );

    let context = builder.build();
    let func = &context.functions[0];

    assert!(func.lock_mappings.contains_key("counter_mutex"));
    let protected = &func.lock_mappings["counter_mutex"];
    assert!(protected.contains(&"counter".to_string()));
    assert!(protected.contains(&"total".to_string()));
}

// ============================================================================
// TEST 6: Serialize to JSON
// ============================================================================

#[test]
fn test_serialize_to_json() {
    let mut builder = ContextBuilder::new();
    builder.add_function("example", "int example(int* ptr)").add_ownership(
        "example",
        "ptr",
        "owning",
        0.85,
        "Allocated via malloc",
    );

    let json = builder.to_json().expect("JSON serialization failed");

    // Should be valid JSON
    let parsed: serde_json::Value = serde_json::from_str(&json).expect("Invalid JSON");

    // Should have functions array
    assert!(parsed["functions"].is_array());
    assert_eq!(parsed["functions"][0]["name"], "example");
    assert_eq!(parsed["functions"][0]["ownership"]["ptr"]["kind"], "owning");
}

// ============================================================================
// TEST 7: Multiple functions
// ============================================================================

#[test]
fn test_multiple_functions() {
    let mut builder = ContextBuilder::new();
    builder
        .add_function("init", "void init()")
        .add_function("cleanup", "void cleanup()")
        .add_function("process", "int process(int* data)");

    let context = builder.build();
    assert_eq!(context.functions.len(), 3);

    let names: Vec<&str> = context.functions.iter().map(|f| f.name.as_str()).collect();
    assert!(names.contains(&"init"));
    assert!(names.contains(&"cleanup"));
    assert!(names.contains(&"process"));
}

// ============================================================================
// TEST 8: Complex function with all analysis types
// ============================================================================

#[test]
fn test_complex_function_context() {
    let mut builder = ContextBuilder::new();
    builder
        .add_function("thread_safe_update", "void thread_safe_update(Counter* c)")
        .add_ownership("thread_safe_update", "c", "mutable_borrow", 0.9, "Modified under lock")
        .add_lifetime("thread_safe_update", "c", 0, false)
        .add_lock_mapping("thread_safe_update", "mutex", vec!["c".to_string()]);

    let context = builder.build();
    let func = &context.functions[0];

    // All three analysis types should be present
    assert!(!func.ownership.is_empty());
    assert!(!func.lifetimes.is_empty());
    assert!(!func.lock_mappings.is_empty());
}

// ============================================================================
// TEST 9: JSON schema compliance
// ============================================================================

#[test]
fn test_json_schema_structure() {
    let mut builder = ContextBuilder::new();
    builder
        .add_function("test_fn", "void test_fn(int* ptr)")
        .add_ownership("test_fn", "ptr", "immutable_borrow", 0.8, "Read only")
        .add_lifetime("test_fn", "ptr", 1, false)
        .add_lock_mapping("test_fn", "lock", vec!["data".to_string()]);

    let json = builder.to_json().expect("Serialization failed");
    let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();

    // Verify schema structure
    let func = &parsed["functions"][0];

    // Function has required fields
    assert!(func["name"].is_string());
    assert!(func["c_signature"].is_string());
    assert!(func["ownership"].is_object());
    assert!(func["lifetimes"].is_array());
    assert!(func["lock_mappings"].is_object());

    // Ownership info has required fields
    let ownership = &func["ownership"]["ptr"];
    assert!(ownership["kind"].is_string());
    assert!(ownership["confidence"].is_number());
    assert!(ownership["reason"].is_string());

    // Lifetime info has required fields
    let lifetime = &func["lifetimes"][0];
    assert!(lifetime["variable"].is_string());
    assert!(lifetime["scope_depth"].is_number());
    assert!(lifetime["escapes"].is_boolean());
}

// ============================================================================
// TEST 10: Deserialization roundtrip
// ============================================================================

#[test]
fn test_json_roundtrip() {
    let mut builder = ContextBuilder::new();
    builder
        .add_function("roundtrip", "int roundtrip(int* x, int* y)")
        .add_ownership("roundtrip", "x", "mutable_borrow", 0.95, "Modified")
        .add_ownership("roundtrip", "y", "immutable_borrow", 0.88, "Read only")
        .add_lifetime("roundtrip", "x", 0, false)
        .add_lifetime("roundtrip", "y", 0, false);

    let json = builder.to_json().expect("Serialization failed");
    let deserialized: AnalysisContext =
        serde_json::from_str(&json).expect("Deserialization failed");

    assert_eq!(deserialized.functions.len(), 1);
    assert_eq!(deserialized.functions[0].name, "roundtrip");
    assert_eq!(deserialized.functions[0].ownership.len(), 2);
    assert_eq!(deserialized.functions[0].lifetimes.len(), 2);
}

// ============================================================================
// TEST 11: add_ownership on non-existent function (silent no-op)
// ============================================================================

#[test]
fn test_add_ownership_nonexistent_function() {
    let mut builder = ContextBuilder::new();
    builder.add_function("real_func", "void real_func()");

    // Add ownership to function that doesn't exist — should be silent no-op
    builder.add_ownership("nonexistent", "ptr", "owning", 0.9, "Should be ignored");

    let context = builder.build();
    assert_eq!(context.functions.len(), 1);
    assert!(context.functions[0].ownership.is_empty());
}

// ============================================================================
// TEST 12: add_lifetime on non-existent function (silent no-op)
// ============================================================================

#[test]
fn test_add_lifetime_nonexistent_function() {
    let mut builder = ContextBuilder::new();
    builder.add_function("real_func", "void real_func()");

    // Add lifetime to function that doesn't exist — should be silent no-op
    builder.add_lifetime("nonexistent", "ptr", 0, true);

    let context = builder.build();
    assert_eq!(context.functions.len(), 1);
    assert!(context.functions[0].lifetimes.is_empty());
}

// ============================================================================
// TEST 13: add_lock_mapping on non-existent function (silent no-op)
// ============================================================================

#[test]
fn test_add_lock_mapping_nonexistent_function() {
    let mut builder = ContextBuilder::new();
    builder.add_function("real_func", "void real_func()");

    // Add lock mapping to function that doesn't exist
    builder.add_lock_mapping("nonexistent", "mutex", vec!["data".to_string()]);

    let context = builder.build();
    assert_eq!(context.functions.len(), 1);
    assert!(context.functions[0].lock_mappings.is_empty());
}

// ============================================================================
// TEST 14: Multiple ownership entries for same variable (last wins)
// ============================================================================

#[test]
fn test_add_ownership_overwrites_same_variable() {
    let mut builder = ContextBuilder::new();
    builder
        .add_function("func", "void func(int* ptr)")
        .add_ownership("func", "ptr", "owning", 0.5, "First inference")
        .add_ownership("func", "ptr", "mutable_borrow", 0.95, "Refined inference");

    let context = builder.build();
    let func = &context.functions[0];

    // HashMap insert overwrites — last value wins
    assert_eq!(func.ownership.len(), 1);
    assert_eq!(func.ownership["ptr"].kind, "mutable_borrow");
    assert!((func.ownership["ptr"].confidence - 0.95).abs() < 0.01);
}

// ============================================================================
// TEST 15: Multiple lifetimes for same variable (both stored)
// ============================================================================

#[test]
fn test_add_multiple_lifetimes_same_variable() {
    let mut builder = ContextBuilder::new();
    builder
        .add_function("func", "void func(int* ptr)")
        .add_lifetime("func", "ptr", 0, false)
        .add_lifetime("func", "ptr", 1, true);

    let context = builder.build();
    let func = &context.functions[0];

    // Vec push stores both — no dedup
    assert_eq!(func.lifetimes.len(), 2);
    assert_eq!(func.lifetimes[0].scope_depth, 0);
    assert!(!func.lifetimes[0].escapes);
    assert_eq!(func.lifetimes[1].scope_depth, 1);
    assert!(func.lifetimes[1].escapes);
}

// ============================================================================
// TEST 16: Builder chaining returns self
// ============================================================================

#[test]
fn test_builder_chaining() {
    let mut builder = ContextBuilder::new();
    let result = builder
        .add_function("f1", "void f1()")
        .add_function("f2", "void f2()")
        .add_ownership("f1", "x", "owning", 0.9, "test")
        .add_lifetime("f1", "x", 0, false)
        .add_lock_mapping("f2", "mtx", vec!["data".to_string()])
        .build();

    assert_eq!(result.functions.len(), 2);
    assert!(!result.functions[0].ownership.is_empty());
    assert!(!result.functions[0].lifetimes.is_empty());
    assert!(!result.functions[1].lock_mappings.is_empty());
}