harn-vm 0.10.42

Async bytecode virtual machine for the Harn programming language
Documentation
use super::{
    build_assistant_response_message, build_assistant_tool_message, collect_tool_schemas, json,
    normalize_tool_args, sample_tool_registry, validate_tool_args, vm_bool, vm_dict, vm_list,
    vm_str, VmValue,
};
use std::collections::BTreeMap;

#[test]
fn validate_tool_args_enforces_required_parameters() {
    let tools = sample_tool_registry();
    let schemas = collect_tool_schemas(Some(&tools), None);
    for args in [
        json!({"action": "create"}),
        json!({"action": "create", "path": null}),
    ] {
        let error = validate_tool_args("edit", &args, &schemas).expect_err("path is required");
        assert!(error.contains("path"));
    }
    assert!(validate_tool_args(
        "edit",
        &json!({"action": "create", "path": "test.go"}),
        &schemas,
    )
    .is_ok());
    assert!(validate_tool_args("unknown", &json!({}), &schemas).is_ok());
}

fn normalization_registry() -> VmValue {
    let mut parameters = BTreeMap::new();
    parameters.insert(
        "action".to_string(),
        vm_dict(&[
            ("type", vm_str("string")),
            (
                "enum",
                vm_list(vec![vm_str("create"), vm_str("delete_range")]),
            ),
        ]),
    );
    parameters.insert(
        "overwrite".to_string(),
        vm_dict(&[("type", vm_str("bool"))]),
    );
    parameters.insert(
        "ops".to_string(),
        vm_dict(&[("type", vm_str("list")), ("required", vm_bool(false))]),
    );
    vm_dict(&[(
        "tools",
        vm_list(vec![vm_dict(&[
            ("name", vm_str("edit")),
            ("parameters", VmValue::dict(parameters)),
        ])]),
    )])
}

#[test]
fn normalize_tool_args_coerces_schema_guided_scalars_and_lists() {
    let tools = normalization_registry();
    for (raw, expected) in [
        ("True", true),
        ("true", true),
        ("FALSE", false),
        ("false", false),
    ] {
        let normalized = normalize_tool_args("edit", json!({"overwrite": raw}), Some(&tools));
        assert_eq!(normalized["overwrite"], json!(expected));
    }
    let normalized = normalize_tool_args(
        "edit",
        json!({"ops": "[{\"op\":\"replace_range\",\"range_start\":3}]"}),
        Some(&tools),
    );
    assert_eq!(
        normalized["ops"],
        json!([{"op": "replace_range", "range_start": 3}])
    );
    let malformed = "[{\"call_id\":\"1\",tool\":\"look\"";
    let normalized = normalize_tool_args("edit", json!({"ops": malformed}), Some(&tools));
    assert_eq!(normalized["ops"], json!(malformed));
    let conservative = normalize_tool_args(
        "edit",
        json!({"overwrite": "yes", "action": "true"}),
        Some(&tools),
    );
    assert_eq!(conservative["overwrite"], json!("yes"));
    assert_eq!(conservative["action"], json!("true"));
}

#[test]
fn normalize_tool_args_flattens_only_unambiguous_registered_actions() {
    let tools = normalization_registry();
    let normalized = normalize_tool_args(
        "edit",
        json!({"delete_range": {"path": "a.cpp", "range_start": 7, "range_end": 9}}),
        Some(&tools),
    );
    assert_eq!(
        normalized,
        json!({"action": "delete_range", "path": "a.cpp", "range_start": 7, "range_end": 9})
    );
    for arguments in [
        json!({"invent_action": {"path": "a.cpp"}}),
        json!({"delete_range": {"path": "a.cpp"}, "path": "outer.cpp"}),
        json!({"delete_range": "a.cpp"}),
        json!({"delete_range": {"action": "create", "path": "a.cpp"}}),
    ] {
        assert_eq!(
            normalize_tool_args("edit", arguments.clone(), Some(&tools)),
            arguments
        );
    }
}

#[test]
fn normalize_tool_args_unwraps_only_complete_heredoc_values() {
    let body = "fn f(a: u32) u32 {\n    return a << 2;\n}";
    for wrapped in [
        format!("<<EOF\n{body}\nEOF"),
        format!("  <<EOF\n{body}\nEOF\n  "),
    ] {
        let normalized = normalize_tool_args("edit", json!({"content": wrapped}), None);
        assert_eq!(normalized["content"], body);
    }
    let nested = normalize_tool_args(
        "edit",
        json!({"ops": [{"new_body": format!("<<EOF\n{body}\nEOF")}]}),
        None,
    );
    assert_eq!(nested["ops"][0]["new_body"], body);

    for value in [
        body.to_string(),
        "<<EOF\nbody\nEOF\ntrailing".to_string(),
        "# Title\n\n```rust\nfn main() {}\n```".to_string(),
    ] {
        let normalized = normalize_tool_args("edit", json!({"content": value.clone()}), None);
        assert_eq!(normalized["content"], value);
    }
}

#[test]
fn assistant_messages_preserve_provider_specific_shapes() {
    let calls = [json!({
        "id": "call_001",
        "name": "read",
        "arguments": {"path": "main.rs"},
        "thought_signature": "tool-signature",
    })];
    let openai = build_assistant_tool_message("", &calls, "together", "moonshotai/Kimi-K2.5");
    assert_eq!(openai["content"], "");
    assert_eq!(openai["tool_calls"][0]["id"], "call_001");

    let ollama = build_assistant_tool_message("", &calls, "ollama", "devstral-small-2:24b");
    let arguments = ollama["tool_calls"][0]["function"]["arguments"]
        .as_str()
        .expect("Ollama arguments are JSON text");
    assert_eq!(
        serde_json::from_str::<serde_json::Value>(arguments).expect("valid JSON")["path"],
        "main.rs"
    );

    let gemini = build_assistant_tool_message("checking", &calls, "gemini", "gemini-2.5-flash");
    assert_eq!(
        gemini["content"][1]["functionCall"],
        json!({"id": "call_001", "name": "read", "args": {"path": "main.rs"}})
    );
    assert_eq!(gemini["content"][1]["thoughtSignature"], "tool-signature");

    let bedrock = build_assistant_tool_message(
        "using a tool",
        &calls,
        "bedrock",
        "anthropic.claude-3-5-sonnet-20240620-v1:0",
    );
    assert_eq!(bedrock["content"][1]["type"], "tool_use");
}

#[test]
fn assistant_response_message_preserves_reasoning_and_gemini_signatures() {
    let calls = [json!({
        "id": "call_001",
        "name": "read",
        "arguments": {"path": "main.rs"},
        "thought_signature": "tool-signature",
    })];
    let reasoning = build_assistant_response_message(
        "",
        &[],
        &calls,
        Some("inspect first"),
        "together",
        "moonshotai/Kimi-K2.5",
    );
    assert_eq!(reasoning["reasoning"], "inspect first");

    let gemini = build_assistant_response_message(
        "checking",
        &[
            json!({
                "type": "output_text",
                "text": "checking",
                "provider_metadata": {"gemini": {"thought_signature": "text-signature"}},
            }),
            json!({
                "type": "tool_call",
                "id": "call_001",
                "name": "read",
                "arguments": {"path": "main.rs"},
                "thought_signature": "tool-signature",
            }),
        ],
        &calls,
        None,
        "gemini",
        "gemini-2.5-flash",
    );
    assert_eq!(gemini["content"][0]["thoughtSignature"], "text-signature");
    assert_eq!(gemini["content"][1]["thoughtSignature"], "tool-signature");
}

#[test]
fn local_read_file_honors_offset_and_limit() {
    use super::super::handle_tool_locally;
    use std::io::Write;

    let dir = tempfile::tempdir().expect("tempdir");
    let path = dir.path().join("offset.txt");
    let mut file = std::fs::File::create(&path).expect("create fixture");
    for line in 1..=20 {
        writeln!(file, "line {line}").expect("write fixture");
    }
    let path = path.to_str().expect("UTF-8 path");
    let result = handle_tool_locally("read_file", &json!({"path": path, "offset": 5, "limit": 3}))
        .expect("local read");
    assert!(result.contains("5\tline 5"));
    assert!(result.contains("7\tline 7"));
    assert!(!result.contains("8\tline 8"));
    assert!(result.contains("offset=8"));
}