crabmate 0.4.0

Rust AI agent: OpenAI-compatible chat/completions, function calling, HTTP serve, ops CLI
Documentation
use crate::cm_types::{FunctionCall, ToolCall};

use super::meta::{HandlerId, ToolExecutionClass, execution_class_for_tool, try_dispatch_meta};
use super::policy::{
    parallel_tool_wall_timeout_secs, sync_default_runs_inline,
    tool_calls_allow_parallel_sync_batch, web_search_outer_wall_secs,
};

fn tc(name: &str) -> ToolCall {
    ToolCall {
        id: "x".to_string(),
        typ: "function".to_string(),
        function: FunctionCall {
            name: name.to_string(),
            arguments: "{}".to_string(),
        },
    }
}

fn test_cfg() -> crate::cm_config::AgentConfig {
    crate::cm_config::load_config(None).expect("embed default")
}

fn default_lookup() -> super::HandlerLookupTable {
    super::HandlerLookupTable::default_dispatch()
}

#[test]
fn parallel_sync_batch_two_readonly_sync_tools() {
    let cfg = test_cfg();
    let batch = vec![tc("read_file"), tc("list_dir")];
    assert!(tool_calls_allow_parallel_sync_batch(
        &default_lookup(),
        &cfg,
        &batch
    ));
}

#[test]
fn parallel_sync_batch_mixed_readonly_http_and_search() {
    let cfg = test_cfg();
    assert!(tool_calls_allow_parallel_sync_batch(
        &default_lookup(),
        &cfg,
        &[tc("read_file"), tc("http_fetch")]
    ));
    assert!(tool_calls_allow_parallel_sync_batch(
        &default_lookup(),
        &cfg,
        &[tc("get_weather"), tc("web_search")]
    ));
}

#[test]
fn parallel_sync_batch_denied_for_cargo_or_workflow() {
    let cfg = test_cfg();
    assert!(!tool_calls_allow_parallel_sync_batch(
        &default_lookup(),
        &cfg,
        &[tc("read_file"), tc("cargo_check")]
    ));
    assert!(!tool_calls_allow_parallel_sync_batch(
        &default_lookup(),
        &cfg,
        &[tc("workflow_execute"), tc("read_file")]
    ));
}

#[test]
fn parallel_sync_batch_denied_for_http_request() {
    let cfg = test_cfg();
    assert!(!tool_calls_allow_parallel_sync_batch(
        &default_lookup(),
        &cfg,
        &[tc("read_file"), tc("http_request")]
    ));
}

#[test]
fn parallel_sync_batch_single_tool_false() {
    let cfg = test_cfg();
    assert!(!tool_calls_allow_parallel_sync_batch(
        &default_lookup(),
        &cfg,
        &[tc("read_file")]
    ));
}

#[test]
fn handler_map_resolves_known_tools() {
    let table = default_lookup();
    assert_eq!(table.id_for("workflow_execute"), HandlerId::Workflow);
    assert_eq!(table.id_for("run_command"), HandlerId::RunCommand);
    assert_eq!(table.id_for("terminal_session"), HandlerId::TerminalSession);
    assert_eq!(table.id_for("web_search"), HandlerId::WebSearch);
    assert_eq!(table.id_for("http_request"), HandlerId::HttpRequest);
    assert_eq!(table.id_for("unknown_xyz"), HandlerId::SyncDefault);
}

#[test]
fn try_dispatch_meta_unknown_is_none() {
    assert!(try_dispatch_meta("calc").is_none());
    assert_eq!(
        try_dispatch_meta("workflow_execute").map(|m| m.name),
        Some("workflow_execute")
    );
}

#[test]
fn sync_default_inline_tools() {
    let cfg = test_cfg();
    assert!(sync_default_runs_inline(&cfg, "get_current_time"));
    assert!(sync_default_runs_inline(&cfg, "convert_units"));
    assert!(!sync_default_runs_inline(&cfg, "read_file"));
    assert!(!sync_default_runs_inline(&cfg, "calc"));
}

#[test]
fn meta_fields_and_default_class() {
    let wf = try_dispatch_meta("workflow_execute").unwrap();
    assert!(!wf.requires_workspace);
    assert_eq!(wf.class, ToolExecutionClass::Workflow);
    let rc = try_dispatch_meta("run_command").unwrap();
    assert!(rc.requires_workspace);
    assert_eq!(rc.class, ToolExecutionClass::CommandSpawnTimeout);
    let ts = try_dispatch_meta("terminal_session").unwrap();
    assert!(ts.requires_workspace);
    assert_eq!(ts.class, ToolExecutionClass::CommandSpawnTimeout);
    assert_eq!(
        execution_class_for_tool("calc"),
        ToolExecutionClass::BlockingSync
    );
}

#[test]
fn parallel_tool_wall_timeout_secs_smoke() {
    let cfg = crate::cm_config::load_config(None).expect("embed default");
    let cmd_budget = parallel_tool_wall_timeout_secs(&cfg, "read_file");
    assert!(cmd_budget >= 1);
    let fetch_budget = parallel_tool_wall_timeout_secs(&cfg, "http_fetch");
    assert!(fetch_budget >= cmd_budget);
    assert_eq!(
        parallel_tool_wall_timeout_secs(&cfg, "get_weather"),
        cfg.weather_tool.weather_timeout_secs.max(1)
    );
    // 默认 worbrow:外圈 = 内层超时 + 浏览器收尾宽限
    assert_eq!(
        parallel_tool_wall_timeout_secs(&cfg, "web_search"),
        web_search_outer_wall_secs(&cfg)
    );
    assert!(
        web_search_outer_wall_secs(&cfg) > cfg.web_search.web_search_timeout_secs.max(1),
        "outer wall must exceed inner search timeout for teardown headroom"
    );
}