lenso-app-plan 0.4.0

Immutable Lenso application plans with versioned dependency routing.
Documentation
use serde_json::{Value, json};

use super::resolve_configuration_layers;

fn transport_schema() -> Value {
    json!({
        "type": "object", "additionalProperties": false,
        "properties": {"transport": {"enum": ["stdio", "http"]}, "program": true, "endpoint": true},
        "required": ["transport"],
        "allOf": [{
            "if": {"properties": {"transport": {"const": "stdio"}}, "required": ["transport"]},
            "then": {"properties": {"program": {"type": "string"}}, "required": ["program"]},
            "else": {"properties": {"endpoint": {"type": "string"}}, "required": ["endpoint"]}
        }]
    })
}

#[test]
fn all_of_enforces_every_branch_even_for_secret_references() {
    let schema = json!({"properties": {"token": {
        "x-lenso-sensitive": true, "allOf": [true, false]
    }}});
    assert!(
        resolve_configuration_layers(
            &json!({"token": {"secret_ref": "credential"}}),
            &[],
            Some(&schema),
            "test"
        )
        .is_err()
    );
}

#[test]
fn schema_depth_is_bounded_even_in_inactive_branches() {
    let mut schema = json!(true);
    for _ in 0..66 {
        schema = json!({"if": false, "then": schema});
    }
    let error = resolve_configuration_layers(&json!({}), &[], Some(&schema), "test").unwrap_err();
    assert!(error.detail.contains("exceeds 64 levels"));
}

#[test]
fn conditions_use_effective_overlay_and_preserve_inactive_values() {
    let defaults = json!({"transport": "stdio", "program": "/bin/server"});
    let overlay = json!({"transport": "http", "endpoint": "https://example.com"});
    let effective =
        resolve_configuration_layers(&defaults, &[&overlay], Some(&transport_schema()), "test")
            .unwrap();
    assert_eq!(
        effective,
        json!({"transport": "http", "program": "/bin/server", "endpoint": "https://example.com"})
    );
}

#[test]
fn selected_branch_requires_its_fields() {
    let error = resolve_configuration_layers(
        &json!({"transport": "http"}),
        &[],
        Some(&transport_schema()),
        "test",
    )
    .unwrap_err();
    assert!(
        error
            .detail
            .contains("$.endpoint: required field is missing")
    );
}

#[test]
fn conditional_schema_does_not_relax_unknown_properties() {
    assert!(
        resolve_configuration_layers(
            &json!({"transport": "stdio", "program": "server", "unknown": true}),
            &[],
            Some(&transport_schema()),
            "test"
        )
        .is_err()
    );
}

#[test]
fn malformed_conditions_and_inactive_branches_fail_closed() {
    for schema in [
        json!({"if": {"unsupported": true}, "else": true}),
        json!({"if": false, "then": {"type": 42}}),
        json!({"if": {"required": [1]}, "else": true}),
        json!({"if": {"minimum": "bad"}, "else": true}),
        json!({"if": false, "then": {"properties": {"unused": {"unknown": true}}}}),
        json!({"allOf": []}),
    ] {
        assert!(
            resolve_configuration_layers(&json!({}), &[], Some(&schema), "test").is_err(),
            "{schema}"
        );
    }
}

#[test]
fn boolean_schemas_work_at_root_properties_and_items() {
    for schema in [
        json!(true),
        json!({"properties": {"values": {"items": true}}}),
    ] {
        assert!(
            resolve_configuration_layers(&json!({"values": [1]}), &[], Some(&schema), "test")
                .is_ok()
        );
    }
    for schema in [
        json!(false),
        json!({"properties": {"values": false}}),
        json!({"properties": {"values": {"items": false}}}),
    ] {
        assert!(
            resolve_configuration_layers(&json!({"values": [1]}), &[], Some(&schema), "test")
                .is_err()
        );
    }
}

#[test]
fn size_constraints_in_branches_are_enforced() {
    let schema = json!({"if": true, "then": {"properties": {
        "names": {"type": "array", "maxItems": 2, "uniqueItems": true, "items": {"type": "string", "minLength": 1, "maxLength": 2}},
        "limit": {"minimum": 1, "maximum": 5}
    }}});
    assert!(
        resolve_configuration_layers(
            &json!({"names": ["你好"], "limit": 5}),
            &[],
            Some(&schema),
            "test"
        )
        .is_ok()
    );
    for value in [
        json!({"names": ["a", "a"]}),
        json!({"names": ["abc"]}),
        json!({"names": [""]}),
        json!({"names": ["a", "b", "c"]}),
        json!({"limit": 6}),
    ] {
        assert!(resolve_configuration_layers(&value, &[], Some(&schema), "test").is_err());
    }
}

#[test]
fn object_conditions_ignore_non_objects_and_orphan_branches_are_not_applied() {
    let schema = json!({"then": false, "else": false, "properties": {"value": {
        "if": {"required": ["key"]}, "then": {"const": 1}, "else": false
    }}});
    assert!(resolve_configuration_layers(&json!({"value": 1}), &[], Some(&schema), "test").is_ok());
}