harn-vm 0.10.28

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

#[test]
fn tool_call_error_category_serializes_as_snake_case() {
    let pairs = [
        (ToolCallErrorCategory::SchemaValidation, "schema_validation"),
        (ToolCallErrorCategory::ToolError, "tool_error"),
        (ToolCallErrorCategory::McpServerError, "mcp_server_error"),
        (ToolCallErrorCategory::HostBridgeError, "host_bridge_error"),
        (ToolCallErrorCategory::PermissionDenied, "permission_denied"),
        (ToolCallErrorCategory::RejectedLoop, "rejected_loop"),
        (ToolCallErrorCategory::ParseAborted, "parse_aborted"),
        (ToolCallErrorCategory::Timeout, "timeout"),
        (ToolCallErrorCategory::Network, "network"),
        (ToolCallErrorCategory::ResourceBusy, "resource_busy"),
        (ToolCallErrorCategory::Cancelled, "cancelled"),
        (
            ToolCallErrorCategory::AbandonedAtLoopExit,
            "abandoned_at_loop_exit",
        ),
        (ToolCallErrorCategory::Unknown, "unknown"),
    ];
    for (variant, wire) in pairs {
        let encoded = serde_json::to_string(&variant).unwrap();
        assert_eq!(encoded, format!("\"{wire}\""));
        assert_eq!(variant.as_str(), wire);
        let decoded: ToolCallErrorCategory = serde_json::from_str(&encoded).unwrap();
        assert_eq!(decoded, variant);
    }
}

#[test]
fn denial_gate_serializes_as_snake_case() {
    let pairs = [
        (DenialGate::ToolCeiling, "tool_ceiling"),
        (DenialGate::MalformedToolWrapper, "malformed_tool_wrapper"),
        (DenialGate::CapabilityCeiling, "capability_ceiling"),
        (DenialGate::SideEffectCeiling, "side_effect_ceiling"),
        (DenialGate::ArgConstraint, "arg_constraint"),
        (DenialGate::DynamicPermission, "dynamic_permission"),
        (DenialGate::ApprovalPolicy, "approval_policy"),
        (DenialGate::ApprovalUnavailable, "approval_unavailable"),
        (DenialGate::HostRejected, "host_rejected"),
        (DenialGate::HookDeny, "hook_deny"),
        (DenialGate::Unknown, "unknown"),
    ];
    assert_eq!(pairs.len(), DenialGate::ALL.len());
    for (variant, wire) in pairs {
        let encoded = serde_json::to_string(&variant).unwrap();
        assert_eq!(encoded, format!("\"{wire}\""));
        assert_eq!(variant.as_str(), wire);
        let decoded: DenialGate = serde_json::from_str(&encoded).unwrap();
        assert_eq!(decoded, variant);
    }
}

#[test]
fn tool_denial_serializes_terminal_record_and_skips_empty_fields() {
    let denial = ToolDenial::terminal(
        DenialGate::CapabilityCeiling,
        Some("workspace.write_text".to_string()),
        "tool 'edit' exceeds capability ceiling: workspace.write_text",
    );
    let json = denial.to_json();
    assert_eq!(json["gate"], "capability_ceiling");
    assert_eq!(json["capability"], "workspace.write_text");
    assert_eq!(json["retryable"], false);
    assert!(json["reason"]
        .as_str()
        .unwrap()
        .contains("capability ceiling"));
    let bare = ToolDenial::terminal(DenialGate::ToolCeiling, None, "exceeds tool ceiling");
    let bare_json = bare.to_json();
    assert!(bare_json.get("denied_paths").is_none());
    assert!(bare_json.get("capability").is_none());
    let recovered: ToolDenial = serde_json::from_value(denial.to_json()).unwrap();
    assert_eq!(recovered, denial);
}

#[test]
fn tool_executor_round_trips_with_adjacent_tag() {
    for executor in [
        ToolExecutor::HarnBuiltin,
        ToolExecutor::HostBridge,
        ToolExecutor::McpServer {
            server_name: "linear".to_string(),
        },
        ToolExecutor::ProviderNative,
    ] {
        let json = serde_json::to_value(&executor).unwrap();
        let kind = json.get("kind").and_then(|v| v.as_str()).unwrap();
        match &executor {
            ToolExecutor::HarnBuiltin => assert_eq!(kind, "harn_builtin"),
            ToolExecutor::HostBridge => assert_eq!(kind, "host_bridge"),
            ToolExecutor::McpServer { server_name } => {
                assert_eq!(kind, "mcp_server");
                assert_eq!(json["server_name"], *server_name);
            }
            ToolExecutor::ProviderNative => assert_eq!(kind, "provider_native"),
        }
        let recovered: ToolExecutor = serde_json::from_value(json).unwrap();
        assert_eq!(recovered, executor);
    }
}

#[test]
fn tool_call_error_category_from_internal_collapses_transient_family() {
    use crate::value::ErrorCategory as Internal;
    assert_eq!(
        ToolCallErrorCategory::from_internal(&Internal::Timeout),
        ToolCallErrorCategory::Timeout
    );
    for net in [
        Internal::RateLimit,
        Internal::Overloaded,
        Internal::ServerError,
        Internal::TransientNetwork,
    ] {
        assert_eq!(
            ToolCallErrorCategory::from_internal(&net),
            ToolCallErrorCategory::Network,
            "{net:?} should map to Network",
        );
    }
    assert_eq!(
        ToolCallErrorCategory::from_internal(&Internal::ResourceBusy),
        ToolCallErrorCategory::ResourceBusy
    );
    assert_eq!(
        ToolCallErrorCategory::from_internal(&Internal::SchemaValidation),
        ToolCallErrorCategory::SchemaValidation
    );
    assert_eq!(
        ToolCallErrorCategory::from_internal(&Internal::ToolError),
        ToolCallErrorCategory::ToolError
    );
    assert_eq!(
        ToolCallErrorCategory::from_internal(&Internal::ToolRejected),
        ToolCallErrorCategory::PermissionDenied
    );
    assert_eq!(
        ToolCallErrorCategory::from_internal(&Internal::Cancelled),
        ToolCallErrorCategory::Cancelled
    );
    for bridge in [
        Internal::Auth,
        Internal::EgressBlocked,
        Internal::NotFound,
        Internal::CircuitOpen,
        Internal::Generic,
    ] {
        assert_eq!(
            ToolCallErrorCategory::from_internal(&bridge),
            ToolCallErrorCategory::HostBridgeError,
            "{bridge:?} should map to HostBridgeError",
        );
    }
}

#[test]
fn only_schema_validation_is_recoverable() {
    assert!(ToolCallErrorCategory::SchemaValidation.is_recoverable());
    for category in ToolCallErrorCategory::ALL {
        if matches!(category, ToolCallErrorCategory::SchemaValidation) {
            continue;
        }
        assert!(
            !category.is_recoverable(),
            "{category:?} must not be treated as recoverable"
        );
    }
}