harn-vm 0.10.162

Async bytecode virtual machine for the Harn programming language
Documentation
//! Selector resolution and per-call option merging.

use super::super::selection_builtins::{
    llm_model_defaults_builtin, llm_model_ladder_builtin, llm_resolved_options_builtin,
    parse_complementary_reviewer_options,
};
use super::fixtures::build_dict;
use crate::llm_config;
use crate::value::VmValue;

#[test]
fn complementary_reviewer_budget_options_cross_builtin_boundary() {
    let options = build_dict(vec![
        ("author_model", VmValue::string("gpt-6-luna")),
        ("max_price_multiplier", VmValue::Float(3.0)),
        ("min_price_cap_per_mtok", VmValue::Float(6.0)),
        ("max_price_cap_per_mtok", VmValue::Float(15.0)),
    ]);
    let parsed = parse_complementary_reviewer_options(Some(&options)).expect("valid budget");
    assert_eq!(parsed.max_price_multiplier, Some(3.0));
    assert_eq!(parsed.min_price_cap_per_mtok, Some(6.0));
    assert_eq!(parsed.max_price_cap_per_mtok, Some(15.0));

    let inverted = build_dict(vec![
        ("author_model", VmValue::string("gpt-6-luna")),
        ("min_price_cap_per_mtok", VmValue::Float(16.0)),
        ("max_price_cap_per_mtok", VmValue::Float(15.0)),
    ]);
    assert!(parse_complementary_reviewer_options(Some(&inverted))
        .expect_err("inverted budget must fail")
        .to_string()
        .contains("min_price_cap_per_mtok must not exceed"),);
}

#[test]
fn test_llm_model_defaults_returns_empty_for_unknown_model() {
    llm_config::clear_user_overrides();
    let mut out = String::new();
    let args = vec![VmValue::String(arcstr::ArcStr::from(
        "definitely-not-a-real-model-id-zzzzz",
    ))];
    let result = llm_model_defaults_builtin(&args, &mut out).expect("builtin returned error");
    let dict = result.as_dict().expect("expected dict");
    assert!(
        dict.is_empty(),
        "unknown model should yield empty defaults dict, got {dict:?}"
    );
}

#[test]
fn test_llm_model_ladder_projects_catalog_owned_steps() {
    llm_config::clear_user_overrides();
    let mut out = String::new();
    let args = vec![VmValue::String(arcstr::ArcStr::from("agent_frontier"))];
    let result = llm_model_ladder_builtin(&args, &mut out).expect("catalog ladder");
    let ladder = result.as_dict().expect("ladder dict");
    let steps = match ladder.get("steps") {
        Some(VmValue::List(steps)) => steps,
        other => panic!("expected ladder steps, got {other:?}"),
    };
    assert_eq!(steps.len(), 2);
    assert_eq!(
        steps[0]
            .as_dict()
            .and_then(|step| step.get("provider"))
            .map(VmValue::display)
            .as_deref(),
        Some("anthropic")
    );
}

#[test]
fn test_llm_resolved_options_uses_dispatch_defaults_without_model() {
    let _guard = crate::llm::env_guard();
    llm_config::clear_user_overrides();
    let expected_provider = crate::llm::helpers::vm_resolve_provider(&None);
    let expected_model = crate::llm::helpers::vm_resolve_model(&None, &expected_provider).unwrap();
    let mut out = String::new();
    for options in [
        build_dict(vec![]),
        build_dict(vec![("model", VmValue::string(""))]),
    ] {
        let result = llm_resolved_options_builtin(&[options], &mut out).expect("default route");
        let fields = result.as_dict().expect("resolved options");
        assert_eq!(
            fields.get("provider").map(VmValue::display),
            Some(expected_provider.clone())
        );
        assert_eq!(
            fields.get("model").map(VmValue::display),
            Some(expected_model.clone())
        );
    }
}

#[test]
fn test_provider_without_default_requires_explicit_model() {
    let _guard = crate::llm::env_guard();
    let _env = crate::test_env::test_env_guard();
    llm_config::clear_user_overrides();
    let provider = "fixture-no-default";
    let model_id = "fixture-model";
    let mut overlay = llm_config::ProvidersConfig::default();
    overlay.providers.insert(
        provider.to_string(),
        llm_config::ProviderDef {
            base_url: "https://fixture.invalid/v1".to_string(),
            chat_endpoint: "/chat/completions".to_string(),
            ..Default::default()
        },
    );
    let mut model = llm_config::model_catalog_entries()
        .into_iter()
        .find(|(_, model)| {
            !model.deprecated
                && model.supports_operation(llm_config::ModelOperation::TextGeneration)
        })
        .expect("catalog has a text-generation model")
        .1;
    model.provider = provider.to_string();
    overlay.models.insert(model_id.to_string(), model);
    llm_config::set_user_overrides(Some(overlay));

    let mut out = String::new();
    let error = llm_resolved_options_builtin(
        &[build_dict(vec![("provider", VmValue::string(provider))])],
        &mut out,
    )
    .expect_err("provider without a default must fail before dispatch");
    let crate::value::VmError::Thrown(value) = error else {
        panic!("expected a typed model-resolution error");
    };
    let fields = value.as_dict().expect("typed error fields");
    assert_eq!(
        fields.get("code").map(VmValue::display).as_deref(),
        Some("model_resolution_failed")
    );
    assert!(fields
        .get("message")
        .is_some_and(|message| { message.display().contains("pass an explicit model") }));

    let result = llm_resolved_options_builtin(
        &[build_dict(vec![
            ("provider", VmValue::string(provider)),
            ("model", VmValue::string(model_id)),
        ])],
        &mut out,
    )
    .expect("explicit catalog model still resolves");
    let fields = result.as_dict().expect("resolved options");
    assert_eq!(
        fields.get("provider").map(VmValue::display).as_deref(),
        Some(provider)
    );
    assert_eq!(
        fields.get("model").map(VmValue::display).as_deref(),
        Some(model_id)
    );
    llm_config::clear_user_overrides();
}

#[test]
fn test_llm_resolved_options_user_wins_over_defaults() {
    let _guard = crate::llm::env_guard();
    llm_config::clear_user_overrides();
    let mut overlay = llm_config::ProvidersConfig::default();
    let mut model_defaults = std::collections::BTreeMap::new();
    model_defaults.insert(
        "fake-resolved-options-model".to_string(),
        toml::Value::Float(0.5),
    );
    overlay
        .model_defaults
        .insert("fake-resolved-options-model".to_string(), model_defaults);
    llm_config::set_user_overrides(Some(overlay));

    let mut out = String::new();
    let args = vec![build_dict(vec![
        (
            "model",
            VmValue::String(arcstr::ArcStr::from("fake-resolved-options-model")),
        ),
        ("temperature", VmValue::Float(0.9)),
    ])];
    let result = llm_resolved_options_builtin(&args, &mut out).expect("builtin returned error");
    let dict = result.as_dict().expect("expected dict");
    match dict.get("temperature") {
        Some(VmValue::Float(f)) => assert!((*f - 0.9).abs() < 1e-9, "user value lost: {f}"),
        other => panic!("expected Float(0.9), got {other:?}"),
    }
    match dict.get("model") {
        Some(VmValue::String(s)) => assert_eq!(s.as_str(), "fake-resolved-options-model"),
        other => panic!("expected model string, got {other:?}"),
    }

    llm_config::clear_user_overrides();
}

#[test]
fn test_llm_resolved_options_default_fills_unspecified() {
    let _guard = crate::llm::env_guard();
    llm_config::clear_user_overrides();
    let mut overlay = llm_config::ProvidersConfig::default();
    let mut model_defaults = std::collections::BTreeMap::new();
    model_defaults.insert("temperature".to_string(), toml::Value::Float(0.5));
    overlay
        .model_defaults
        .insert("fake-fill-defaults-model".to_string(), model_defaults);
    llm_config::set_user_overrides(Some(overlay));

    let mut out = String::new();
    let args = vec![build_dict(vec![(
        "model",
        VmValue::String(arcstr::ArcStr::from("fake-fill-defaults-model")),
    )])];
    let result = llm_resolved_options_builtin(&args, &mut out).expect("builtin returned error");
    let dict = result.as_dict().expect("expected dict");
    match dict.get("temperature") {
        Some(VmValue::Float(f)) => assert!((*f - 0.5).abs() < 1e-9, "default lost: {f}"),
        other => panic!("expected Float(0.5), got {other:?}"),
    }

    llm_config::clear_user_overrides();
}

#[test]
fn test_llm_resolved_options_resolves_provider() {
    let _guard = crate::llm::env_guard();
    let _env = crate::test_env::test_env_guard();
    llm_config::clear_user_overrides();

    let mut out = String::new();
    let args = vec![build_dict(vec![(
        "model",
        VmValue::String(arcstr::ArcStr::from("claude-sonnet-4-20250514")),
    )])];
    let result = llm_resolved_options_builtin(&args, &mut out).expect("builtin returned error");
    let dict = result.as_dict().expect("expected dict");
    match dict.get("provider") {
        Some(VmValue::String(s)) => {
            assert_eq!(s.as_str(), "anthropic", "provider mismatch: {s}");
        }
        other => panic!("expected provider string, got {other:?}"),
    }
}