sim-lib-skill 0.1.0-rc.1

SIM workspace package for sim lib skill.
Documentation
use std::sync::Arc;

use sim_kernel::{Args, Cx, DefaultFactory, EagerPolicy, Expr, Symbol, Value};

use crate::{
    FixtureBehavior, FixtureMcpTransport, McpCallParams, McpToolDescriptor, SkillRole,
    SkillTransport, install_skill_lib, skill_call_capability, skill_call_symbol,
    skill_install_capability, skill_install_symbol, skill_specific_call_capability,
    skill_transport_value,
};

use super::{McpToolResult, skill_mcp_call_symbol, skill_mcp_tools_symbol};

#[test]
fn fixture_mcp_discovery_binds_tool_role_skill() {
    let mut cx = skill_cx();
    cx.grant(skill_install_capability());
    cx.grant(skill_call_capability());
    cx.grant(skill_specific_call_capability("math.add"));
    let fixture = Arc::new(fixture_mcp_transport());
    let cards = fixture.discover(&mut cx).unwrap();
    assert_eq!(cards.len(), 1);
    assert!(cards[0].roles.contains(&SkillRole::Tool));

    install_cards(&mut cx, fixture.clone(), cards);

    let target = cx.factory().string("math.add".to_owned()).unwrap();
    let left = number_value(&mut cx, 2);
    let right = number_value(&mut cx, 3);
    let result = cx
        .call_function(&skill_call_symbol(), Args::new(vec![target, left, right]))
        .unwrap();

    assert_eq!(number_expr(&mut cx, result), "5");
    assert_eq!(fixture.call_count(), 1);
}

#[test]
fn skill_mcp_tools_returns_descriptor_data_generated_from_skill_card() {
    let mut cx = installed_mcp_cx();
    let value = cx
        .call_function(&skill_mcp_tools_symbol(), Args::default())
        .unwrap();
    let Expr::List(items) = value.object().as_expr(&mut cx).unwrap() else {
        panic!("skill/mcp-tools should return descriptor list");
    };
    assert_eq!(items.len(), 1);
    let descriptor = McpToolDescriptor::from_expr(&items[0]).unwrap();
    assert_eq!(descriptor.name, "math.add");
    assert_eq!(descriptor.title.as_deref(), Some("Add Numbers"));
    assert_eq!(descriptor.description, "Add numbers through fixture MCP.");
}

#[test]
fn skill_mcp_call_executes_the_same_skill_callable_as_skill_call() {
    let mut cx = skill_cx();
    cx.grant(skill_install_capability());
    cx.grant(skill_call_capability());
    cx.grant(skill_specific_call_capability("math.add"));
    let fixture = Arc::new(fixture_mcp_transport());
    let cards = fixture.discover(&mut cx).unwrap();
    install_cards(&mut cx, fixture.clone(), cards);

    let params = McpCallParams {
        name: "math.add".to_owned(),
        arguments: vec![number_expr_literal(4), number_expr_literal(6)],
    };
    let params = cx.factory().expr(params.to_expr()).unwrap();
    let result = cx
        .call_function(&skill_mcp_call_symbol(), Args::new(vec![params]))
        .unwrap();
    let result = McpToolResult::from_expr(&result.object().as_expr(&mut cx).unwrap()).unwrap();
    assert_eq!(result.result, number_expr_literal(10));
    assert!(!result.is_error);

    let target = cx.factory().string("math.add".to_owned()).unwrap();
    let left = number_value(&mut cx, 1);
    let right = number_value(&mut cx, 2);
    let direct = cx
        .call_function(&skill_call_symbol(), Args::new(vec![target, left, right]))
        .unwrap();

    assert_eq!(number_expr(&mut cx, direct), "3");
    assert_eq!(fixture.call_count(), 2);
}

fn skill_cx() -> Cx {
    let mut cx = Cx::new(Arc::new(EagerPolicy), Arc::new(DefaultFactory));
    install_skill_lib(&mut cx).unwrap();
    cx
}

fn installed_mcp_cx() -> Cx {
    let mut cx = skill_cx();
    cx.grant(skill_install_capability());
    let fixture = Arc::new(fixture_mcp_transport());
    let cards = fixture.discover(&mut cx).unwrap();
    install_cards(&mut cx, fixture, cards);
    cx
}

fn fixture_mcp_transport() -> FixtureMcpTransport {
    let fixture = FixtureMcpTransport::new("mcp-fixture");
    fixture
        .insert_tool(
            McpToolDescriptor {
                name: "math.add".to_owned(),
                title: Some("Add Numbers".to_owned()),
                description: "Add numbers through fixture MCP.".to_owned(),
                input_schema: sum_args_schema(),
                output_schema: Some(number_schema()),
            },
            FixtureBehavior::SumNumbers,
        )
        .unwrap();
    fixture
}

fn install_cards(cx: &mut Cx, fixture: Arc<FixtureMcpTransport>, cards: Vec<crate::SkillCard>) {
    let mut values = vec![skill_transport_value(cx, fixture).unwrap()];
    values.extend(cards.into_iter().map(|card| card.value(cx).unwrap()));
    cx.call_function(&skill_install_symbol(), Args::new(values))
        .unwrap();
}

fn sum_args_schema() -> Expr {
    Expr::List(vec![number_schema(), number_schema()])
}

fn number_schema() -> Expr {
    Expr::Symbol(Symbol::qualified("core", "Number"))
}

fn number_value(cx: &mut Cx, value: u32) -> Value {
    cx.factory()
        .number_literal(Symbol::qualified("numbers", "f64"), value.to_string())
        .unwrap()
}

fn number_expr_literal(value: u32) -> Expr {
    Expr::Number(sim_kernel::NumberLiteral {
        domain: Symbol::qualified("numbers", "f64"),
        canonical: value.to_string(),
    })
}

fn number_expr(cx: &mut Cx, value: Value) -> String {
    match value.object().as_expr(cx).unwrap() {
        Expr::Number(number) => number.canonical,
        other => panic!("expected number, got {other:?}"),
    }
}