harn-vm 0.10.163

Async bytecode virtual machine for the Harn programming language
Documentation
use super::*;

fn throwing_closure(name: &str) -> VmClosure {
    let source = format!(
        "type LookupError = {{variant: \"NotFound\", message: string}}\nfn {name}(input: dict) throws LookupError {{ throw {{variant: \"NotFound\", message: \"PRIVATE-CUSTOMER-DIAGNOSTIC-123456\"}} }}"
    );
    let program = harn_parser::check_source_strict(&source).expect("valid throwing function");
    let chunk = crate::compiler::Compiler::new()
        .compile(&program)
        .expect("compile throwing function");
    let function = chunk
        .functions
        .iter()
        .find(|function| function.name.as_str() == name)
        .expect("compiled throwing function")
        .clone();
    VmClosure {
        func: function,
        env: VmEnv::new(),
        source_dir: None,
        module_functions: None,
        module_state: None,
        retained_module_scope: None,
    }
}

#[tokio::test]
async fn prepared_tool_is_refused_before_mcp_handler_or_task_execution() {
    for prepared in [false, true] {
        let mut tool = crate::value::DictMap::new();
        tool.put_str("name", "lookup");
        tool.put_str("description", "Look up a record");
        tool.insert(
            "parameters".into(),
            VmValue::dict(crate::value::DictMap::new()),
        );
        tool.insert(
            "handler".into(),
            VmValue::Closure(Arc::new(throwing_closure("lookup"))),
        );
        tool.insert(
            "execution".into(),
            crate::schema::json_to_vm_value(&serde_json::json!({"taskSupport": "optional"})),
        );
        if prepared {
            tool.insert(
                "prepare".into(),
                VmValue::Closure(Arc::new(empty_closure("prepare"))),
            );
        }
        tool.insert(
            "errorSchema".into(),
            crate::schema::json_to_vm_value(&serde_json::json!({
                "type": "object",
                "properties": {"variant": {"const": "NotFound"}, "message": {"type": "string"}},
                "required": ["variant", "message"], "additionalProperties": false
            })),
        );
        let mut registry = crate::value::DictMap::new();
        registry.put_str("_type", "tool_registry");
        registry.insert(
            "tools".into(),
            VmValue::List(Arc::new(vec![VmValue::dict(tool)])),
        );
        let tools = tool_registry_to_mcp_tools(&VmValue::dict(registry)).unwrap();
        let server = McpServer::new("test".into(), tools, Vec::new(), Vec::new(), Vec::new());
        let mut vm = crate::Vm::new();
        let response = server
            .handle_json_rpc(
                crate::jsonrpc::request(
                    1,
                    "tools/call",
                    stable_metadata_params(serde_json::json!({"name": "lookup", "arguments": {}})),
                ),
                &mut vm,
            )
            .await
            .unwrap();
        if !prepared {
            assert_eq!(
                response["result"]["_meta"][crate::tool_registry::HARN_MCP_TOOL_CONTRACT_META_KEY]
                    ["applicationError"]["data"]["message"],
                "PRIVATE-CUSTOMER-DIAGNOSTIC-123456"
            );
        } else {
            assert_eq!(response["error"]["code"], -32602);
            assert!(response["error"]["message"]
                .as_str()
                .unwrap()
                .contains("requires approved invocation preparation"));
            let task = server
                .handle_json_rpc(
                    crate::jsonrpc::request(
                        2,
                        "tools/call",
                        task_client_params(serde_json::json!({"name": "lookup", "arguments": {}})),
                    ),
                    &mut vm,
                )
                .await
                .unwrap();
            assert_eq!(task["error"]["code"], -32602);
            assert!(task["result"]["taskId"].is_null());
        }
    }
}

#[tokio::test]
async fn declared_application_error_has_the_same_payload_inline_and_as_a_task() {
    let mut tool = tool_def(
        "lookup",
        "Look up a record",
        None,
        crate::mcp_tasks::McpTaskSupport::Optional,
    );
    tool.catalog.error_schema = Some(serde_json::json!({
        "type": "object",
        "properties": {
            "variant": {"const": "NotFound"},
            "message": {"type": "string"}
        },
        "required": ["variant", "message"],
        "additionalProperties": false
    }));
    tool.handler = throwing_closure("lookup");
    let server = McpServer::new(
        "test".to_string(),
        tool_set(vec![tool]),
        Vec::new(),
        Vec::new(),
        Vec::new(),
    );
    let mut vm = crate::Vm::new();

    let inline = server
        .handle_json_rpc(
            crate::jsonrpc::request(
                1,
                "tools/call",
                stable_metadata_params(serde_json::json!({
                    "name": "lookup",
                    "arguments": {}
                })),
            ),
            &mut vm,
        )
        .await
        .expect("inline response");
    let inline_result = &inline["result"];
    assert_eq!(inline_result["isError"], true);
    assert_eq!(
        inline_result["_meta"][crate::tool_registry::HARN_MCP_TOOL_CONTRACT_META_KEY]
            ["applicationError"],
        serde_json::json!({
            "tool": "lookup",
            "data": {
                "variant": "NotFound",
                "message": "PRIVATE-CUSTOMER-DIAGNOSTIC-123456"
            }
        })
    );
    assert!(inline_result["structuredContent"].is_null());
    let inline_text = inline_result["content"][0]["text"]
        .as_str()
        .expect("safe inline text");
    assert!(inline_text.contains("declared application error"));
    assert!(!inline_text.contains("PRIVATE-CUSTOMER-DIAGNOSTIC"));

    let created = server
        .handle_json_rpc(
            crate::jsonrpc::request(
                2,
                "tools/call",
                task_client_params(serde_json::json!({
                    "name": "lookup",
                    "arguments": {}
                })),
            ),
            &mut vm,
        )
        .await
        .expect("task response");
    let task_id = created["result"]["taskId"].as_str().expect("task id");
    let read = server
        .handle_json_rpc(
            crate::jsonrpc::request(
                3,
                "tasks/get",
                task_client_params(serde_json::json!({"taskId": task_id})),
            ),
            &mut vm,
        )
        .await
        .expect("task read");
    assert_eq!(read["result"]["status"], "completed");
    assert_eq!(read["result"]["resultType"], "complete");
    let task_result = &read["result"]["result"];
    assert_eq!(task_result, inline_result);
    let task_text = task_result["content"][0]["text"]
        .as_str()
        .expect("safe task text");
    assert!(task_text.contains("declared application error"));
    assert!(!task_text.contains("PRIVATE-CUSTOMER-DIAGNOSTIC"));
}

#[tokio::test]
async fn wrong_application_error_shape_is_a_contract_failure_without_typed_metadata() {
    let mut tool = tool_def(
        "lookup",
        "Look up a record",
        None,
        crate::mcp_tasks::McpTaskSupport::Forbidden,
    );
    tool.catalog.error_schema = Some(serde_json::json!({
        "type": "object",
        "properties": {"variant": {"const": "Forbidden"}},
        "required": ["variant"],
        "additionalProperties": false
    }));
    tool.handler = throwing_closure("lookup");
    let server = McpServer::new(
        "test".to_string(),
        tool_set(vec![tool]),
        Vec::new(),
        Vec::new(),
        Vec::new(),
    );
    let mut vm = crate::Vm::new();

    let response = server
        .handle_json_rpc(
            crate::jsonrpc::request(
                1,
                "tools/call",
                stable_metadata_params(serde_json::json!({
                    "name": "lookup",
                    "arguments": {}
                })),
            ),
            &mut vm,
        )
        .await
        .expect("response");
    assert_eq!(response["result"]["isError"], true);
    assert!(
        response["result"]["_meta"][crate::tool_registry::HARN_MCP_TOOL_CONTRACT_META_KEY]
            ["applicationError"]
            .is_null()
    );
    let content = response["result"]["content"][0]["text"]
        .as_str()
        .expect("safe contract failure text");
    assert!(
        content.contains("violates its declared schema"),
        "{content}"
    );
    assert!(
        !content.contains("PRIVATE-CUSTOMER-DIAGNOSTIC"),
        "{content}"
    );
}