lenso-app-plan 0.3.0

Immutable application plans for the Lenso vNext runtime.
Documentation
use lenso_app_plan::{
    CapabilityEndpointPlan, CapabilityRequirementPlan,
    authoring::{
        HostBinding, HostCatalog, HostDefaultPlugin, HostPluginConfiguration, HostPluginRelease,
        HostSlot, PluginDescriptor, PluginInstanceId, PluginInstanceSource, PluginRootInstance,
        PluginRootResolutionError, PluginRootSnapshot, resolve_plugin_root,
    },
};
use serde_json::json;

fn model_descriptor(plugin_id: &str) -> PluginDescriptor {
    PluginDescriptor::new(plugin_id, "1.0.0", "model").with_capability(CapabilityEndpointPlan::new(
        "example.model@1",
        "1.0.0",
        ["complete"],
    ))
}

fn agent_descriptor() -> PluginDescriptor {
    PluginDescriptor::new("example.agent", "1.0.0", "agent")
        .with_requirement(CapabilityRequirementPlan::one("example.model@1", "1.0.0"))
}

fn host_with_default_model(replaceable: bool) -> HostCatalog {
    let model_slot = if replaceable {
        HostSlot::one("model").replaceable()
    } else {
        HostSlot::one("model")
    };
    HostCatalog::new(
        [model_slot, HostSlot::one("agent")],
        [
            HostPluginRelease::new(model_descriptor("example.model.fixture")),
            HostPluginRelease::new(agent_descriptor()),
        ],
        [
            HostDefaultPlugin::new("example.model.fixture", "default"),
            HostDefaultPlugin::new("example.agent", "default"),
        ],
    )
}

#[test]
fn empty_plugin_root_resolves_host_defaults_and_unique_binding() {
    let resolved = resolve_plugin_root(
        &host_with_default_model(true),
        &PluginRootSnapshot::default(),
    )
    .unwrap();

    assert_eq!(resolved.plan().plugin_instances().len(), 2);
    assert_eq!(resolved.plan().capability_bindings().len(), 1);
    assert_eq!(
        resolved.plan().capability_bindings()[0].provider_instance(),
        "example.model.fixture/default"
    );
}

#[test]
fn host_binding_scopes_one_requirement_to_the_selected_provider_instance() {
    let consumer = PluginDescriptor::new("example.consumer", "1.0.0", "consumer")
        .with_requirement(CapabilityRequirementPlan::one("example.model@1", "1.0.0"));
    let host = HostCatalog::new(
        [HostSlot::one("consumer"), HostSlot::many("model")],
        [
            HostPluginRelease::new(consumer),
            HostPluginRelease::new(model_descriptor("example.model.a")),
            HostPluginRelease::new(model_descriptor("example.model.b")),
        ],
        [HostDefaultPlugin::new("example.consumer", "default")],
    )
    .with_bindings([HostBinding::to_instance(
        PluginInstanceId::new("example.consumer", "default"),
        "example.model@1",
        PluginInstanceId::new("example.model.b", "primary"),
    )]);
    let root = PluginRootSnapshot::new(
        [],
        [
            PluginRootInstance::new("example.model.a", "primary"),
            PluginRootInstance::new("example.model.b", "primary"),
        ],
        [],
    );

    let resolved = resolve_plugin_root(&host, &root).unwrap();

    assert_eq!(resolved.plan().capability_bindings().len(), 1);
    assert_eq!(
        resolved.plan().capability_bindings()[0].provider_instance(),
        "example.model.b/primary"
    );
}

#[test]
fn host_binding_for_an_absent_optional_consumer_is_dormant() {
    let optional_consumer = PluginDescriptor::new("example.consumer", "1.0.0", "consumer")
        .with_requirement(CapabilityRequirementPlan::one("example.model@1", "1.0.0"));
    let host = HostCatalog::new(
        [HostSlot::optional("consumer"), HostSlot::one("model")],
        [
            HostPluginRelease::new(optional_consumer),
            HostPluginRelease::new(model_descriptor("example.model.fixture")),
        ],
        [HostDefaultPlugin::new("example.model.fixture", "default")],
    )
    .with_bindings([HostBinding::to_instance(
        PluginInstanceId::new("example.consumer", "default"),
        "example.model@1",
        PluginInstanceId::new("example.model.fixture", "default"),
    )]);

    let resolved = resolve_plugin_root(&host, &PluginRootSnapshot::default()).unwrap();

    assert_eq!(resolved.plan().plugin_instances().len(), 1);
    assert!(resolved.plan().capability_bindings().is_empty());
}

#[test]
fn root_configuration_overlays_host_and_package_defaults() {
    let descriptor = PluginDescriptor::new("example.agent", "1.0.0", "agent")
        .with_configuration_defaults(json!({"model": "small", "limits": {"steps": 8}}))
        .with_configuration_schema(json!({
            "type": "object",
            "required": ["model", "limits"],
            "properties": {
                "model": {"type": "string"},
                "limits": {
                    "type": "object",
                    "required": ["steps"],
                    "properties": {"steps": {"type": "integer"}},
                    "additionalProperties": false
                }
            },
            "additionalProperties": false
        }));
    let host = HostCatalog::new(
        [HostSlot::one("agent")],
        [HostPluginRelease::new(descriptor)],
        [HostDefaultPlugin::new("example.agent", "default")
            .with_configuration(json!({"limits": {"steps": 4}}))],
    );
    let root = PluginRootSnapshot::new(
        [],
        [PluginRootInstance::new("example.agent", "default")
            .with_configuration(json!({"model": "large"}))],
        [],
    );

    let resolved = resolve_plugin_root(&host, &root).unwrap();

    assert_eq!(
        resolved.plan().plugin_instances()[0].configuration(),
        r#"{"limits":{"steps":4},"model":"large"}"#
    );
    assert_eq!(
        resolved.instances()[0].source(),
        PluginInstanceSource::HostDefaultConfiguredByRoot
    );
}

#[test]
fn explicit_instance_uses_host_configuration_without_becoming_a_host_default() {
    let descriptor = PluginDescriptor::new("example.editor", "1.0.0", "tools")
        .with_configuration_defaults(json!({"max_bytes": 1024}))
        .with_configuration_schema(json!({
            "type": "object",
            "required": ["root", "max_bytes"],
            "properties": {
                "root": {"type": "string"},
                "max_bytes": {"type": "integer"}
            },
            "additionalProperties": false
        }));
    let host = HostCatalog::new(
        [HostSlot::many("tools")],
        [HostPluginRelease::new(descriptor)],
        [],
    )
    .with_configurations([HostPluginConfiguration::new(
        "example.editor",
        "default",
        json!({"root": "."}),
    )]);

    let empty = resolve_plugin_root(&host, &PluginRootSnapshot::default()).unwrap();
    assert!(empty.instances().is_empty());

    let enabled = resolve_plugin_root(
        &host,
        &PluginRootSnapshot::new(
            [],
            [PluginRootInstance::new("example.editor", "default")],
            [],
        ),
    )
    .unwrap();
    assert_eq!(
        enabled.plan().plugin_instances()[0].configuration(),
        r#"{"max_bytes":1024,"root":"."}"#
    );
    assert_eq!(
        enabled.instances()[0].source(),
        PluginInstanceSource::PluginRoot
    );
}

#[test]
fn one_explicit_plugin_replaces_a_replaceable_default() {
    let root = PluginRootSnapshot::new(
        [model_descriptor("company.model")],
        [PluginRootInstance::new("company.model", "primary")],
        [],
    );

    let resolved = resolve_plugin_root(&host_with_default_model(true), &root).unwrap();

    assert!(
        resolved
            .instances()
            .iter()
            .any(|instance| instance.id() == &PluginInstanceId::new("company.model", "primary"))
    );
    assert!(resolved.instances().iter().all(|instance| {
        instance.id() != &PluginInstanceId::new("example.model.fixture", "default")
    }));
}

#[test]
fn explicit_plugin_cannot_replace_a_fixed_default() {
    let root = PluginRootSnapshot::new(
        [model_descriptor("company.model")],
        [PluginRootInstance::new("company.model", "primary")],
        [],
    );

    assert_eq!(
        resolve_plugin_root(&host_with_default_model(false), &root),
        Err(PluginRootResolutionError::ExplicitProviderDenied {
            slot: "model".to_owned(),
            instance: PluginInstanceId::new("company.model", "primary"),
        })
    );
}

#[test]
fn multiple_explicit_plugins_for_one_slot_fail_without_binding_escape_hatch() {
    let root = PluginRootSnapshot::new(
        [model_descriptor("company.a"), model_descriptor("company.b")],
        [
            PluginRootInstance::new("company.a", "default"),
            PluginRootInstance::new("company.b", "default"),
        ],
        [],
    );

    assert_eq!(
        resolve_plugin_root(&host_with_default_model(true), &root),
        Err(PluginRootResolutionError::AmbiguousSlot {
            slot: "model".to_owned(),
            instances: vec![
                "company.a/default".to_owned(),
                "company.b/default".to_owned()
            ],
        })
    );
}

#[test]
fn required_host_default_cannot_be_disabled() {
    let root = PluginRootSnapshot::new([], [], [PluginInstanceId::new("example.agent", "default")]);

    assert_eq!(
        resolve_plugin_root(&host_with_default_model(true), &root),
        Err(PluginRootResolutionError::RequiredInstanceDisabled(
            PluginInstanceId::new("example.agent", "default")
        ))
    );
}

#[test]
fn many_slot_collects_plugins_in_identity_order() {
    let host = HostCatalog::new([HostSlot::many("tools")], [], []);
    let root = PluginRootSnapshot::new(
        [
            PluginDescriptor::new("example.z", "1.0.0", "tools"),
            PluginDescriptor::new("example.a", "1.0.0", "tools"),
        ],
        [
            PluginRootInstance::new("example.z", "default"),
            PluginRootInstance::new("example.a", "default"),
        ],
        [],
    );

    let resolved = resolve_plugin_root(&host, &root).unwrap();

    assert_eq!(
        resolved
            .instances()
            .iter()
            .map(|instance| instance.id().to_string())
            .collect::<Vec<_>>(),
        ["example.a/default", "example.z/default"]
    );
}

#[test]
fn instance_identity_includes_plugin_id() {
    let host = HostCatalog::new([HostSlot::many("tools")], [], []);
    let root = PluginRootSnapshot::new(
        [
            PluginDescriptor::new("example.a", "1.0.0", "tools"),
            PluginDescriptor::new("example.b", "1.0.0", "tools"),
        ],
        [
            PluginRootInstance::new("example.a", "default"),
            PluginRootInstance::new("example.b", "default"),
        ],
        [],
    );

    let resolved = resolve_plugin_root(&host, &root).unwrap();

    assert_eq!(resolved.plan().plugin_instances().len(), 2);
    assert_ne!(
        resolved.plan().plugin_instances()[0].instance_key(),
        resolved.plan().plugin_instances()[1].instance_key()
    );
}

#[test]
fn ambiguous_capability_names_plugins_instead_of_requesting_a_binding_document() {
    let provider = |plugin_id: &str| {
        PluginDescriptor::new(plugin_id, "1.0.0", "models").with_capability(
            CapabilityEndpointPlan::new("example.model@1", "1.0.0", ["complete"]),
        )
    };
    let host = HostCatalog::new(
        [HostSlot::one("agent"), HostSlot::many("models")],
        [HostPluginRelease::new(agent_descriptor())],
        [HostDefaultPlugin::new("example.agent", "default")],
    );
    let root = PluginRootSnapshot::new(
        [provider("example.a"), provider("example.b")],
        [
            PluginRootInstance::new("example.a", "default"),
            PluginRootInstance::new("example.b", "default"),
        ],
        [],
    );

    assert_eq!(
        resolve_plugin_root(&host, &root),
        Err(PluginRootResolutionError::AmbiguousCapability {
            consumer: PluginInstanceId::new("example.agent", "default"),
            capability_id: "example.model@1".to_owned(),
            candidates: vec![
                PluginInstanceId::new("example.a", "default"),
                PluginInstanceId::new("example.b", "default"),
            ],
        })
    );
}

#[test]
fn missing_capability_names_the_affected_plugin() {
    let host = HostCatalog::new(
        [HostSlot::one("agent")],
        [HostPluginRelease::new(agent_descriptor())],
        [HostDefaultPlugin::new("example.agent", "default")],
    );

    assert_eq!(
        resolve_plugin_root(&host, &PluginRootSnapshot::default()),
        Err(PluginRootResolutionError::MissingCapability {
            consumer: PluginInstanceId::new("example.agent", "default"),
            capability_id: "example.model@1".to_owned(),
            descriptor_version: "1.0.0".to_owned(),
        })
    );
}

#[test]
fn duplicate_root_release_fails_instead_of_using_discovery_order() {
    let host = HostCatalog::new([HostSlot::many("models")], [], []);
    let root = PluginRootSnapshot::new(
        [
            model_descriptor("example.model"),
            model_descriptor("example.model"),
        ],
        [],
        [],
    );

    assert_eq!(
        resolve_plugin_root(&host, &root),
        Err(PluginRootResolutionError::DuplicatePluginRelease(
            "example.model".to_owned()
        ))
    );
}