Codex Handbook
core/src/tools/handlers/multi_agents_spec_tests.rs 422 lines
use super::*;use codex_protocol::openai_models::ModelPreset;use codex_protocol::openai_models::ModelServiceTier;use codex_protocol::openai_models::ReasoningEffort;use codex_protocol::openai_models::ReasoningEffortPreset;use codex_tools::JsonSchemaPrimitiveType;use codex_tools::JsonSchemaType;use pretty_assertions::assert_eq;use serde_json::json;fn model_preset(id: &str, show_in_picker: bool) -> ModelPreset {    ModelPreset {        id: id.to_string(),        model: format!("{id}-model"),        display_name: format!("{id} display"),        description: format!("{id} description"),        default_reasoning_effort: ReasoningEffort::Medium,        supported_reasoning_efforts: vec![ReasoningEffortPreset {            effort: ReasoningEffort::Medium,            description: "Balanced".to_string(),        }],        supports_personality: false,        additional_speed_tiers: Vec::new(),        service_tiers: vec![ModelServiceTier {            id: "priority".to_string(),            name: "Fast".to_string(),            description: "1.5x speed, increased usage".to_string(),        }],        default_service_tier: None,        is_default: false,        upgrade: None,        show_in_picker,        availability_nux: None,        supported_in_api: true,        input_modalities: Vec::new(),    }}#[test]fn spawn_agent_tool_v2_requires_task_name_and_lists_visible_models() {    let tool = create_spawn_agent_tool_v2(SpawnAgentToolOptions {        available_models: vec![            model_preset("visible", /*show_in_picker*/ true),            model_preset("hidden", /*show_in_picker*/ false),        ],        agent_type_description: "role help".to_string(),        hide_agent_type_model_reasoning: false,        include_usage_hint: true,        usage_hint_text: None,    });    let ToolSpec::Function(ResponsesApiTool {        description,        parameters,        output_schema,        ..    }) = tool    else {        panic!("spawn_agent should be a function tool");    };    assert_eq!(        parameters.schema_type,        Some(JsonSchemaType::Single(JsonSchemaPrimitiveType::Object))    );    let properties = parameters        .properties        .as_ref()        .expect("spawn_agent should use object params");    assert!(description.contains("Spawns an agent to work on the specified task."));    assert!(description.contains("The spawned agent will have the same tools as you"));    assert!(!description.contains("max_concurrent_threads_per_session"));    assert!(description.contains(SPAWN_AGENT_INHERITED_MODEL_GUIDANCE));    assert!(        description            .contains("Available model overrides (optional; inherited parent model is preferred):")    );    assert!(description.contains(        "- `visible-model`: visible description Reasoning efforts: medium (default). Service tiers: priority."    ));    assert!(!description.contains("hidden-model"));    assert!(properties.contains_key("task_name"));    assert!(properties.contains_key("message"));    assert_eq!(        properties            .get("message")            .and_then(|schema| schema.encrypted),        Some(true)    );    assert!(properties.contains_key("fork_turns"));    assert!(!properties.contains_key("items"));    assert!(!properties.contains_key("fork_context"));    assert_eq!(        properties.get("agent_type"),        Some(&JsonSchema::string(Some("role help".to_string())))    );    assert_eq!(        properties            .get("model")            .and_then(|schema| schema.description.as_deref()),        Some(SPAWN_AGENT_MODEL_OVERRIDE_DESCRIPTION)    );    assert_eq!(        properties            .get("service_tier")            .and_then(|schema| schema.description.as_deref()),        Some(SPAWN_AGENT_SERVICE_TIER_OVERRIDE_DESCRIPTION)    );    assert_eq!(        parameters.required.as_ref(),        Some(&vec!["task_name".to_string(), "message".to_string()])    );    assert_eq!(        output_schema.expect("spawn_agent output schema")["required"],        json!(["task_name", "nickname"])    );}#[test]fn spawn_agent_tool_v1_keeps_legacy_fork_context_field() {    let tool = create_spawn_agent_tool_v1(SpawnAgentToolOptions {        available_models: Vec::new(),        agent_type_description: "role help".to_string(),        hide_agent_type_model_reasoning: false,        include_usage_hint: true,        usage_hint_text: None,    });    let ToolSpec::Namespace(namespace) = tool else {        panic!("spawn_agent v1 should be a namespace tool");    };    assert_eq!(namespace.name, MULTI_AGENT_V1_NAMESPACE);    let Some(ResponsesApiNamespaceTool::Function(ResponsesApiTool { parameters, .. })) =        namespace.tools.first()    else {        panic!("spawn_agent should be a namespace function tool");    };    assert_eq!(        parameters.schema_type.clone(),        Some(JsonSchemaType::Single(JsonSchemaPrimitiveType::Object))    );    let properties = parameters        .properties        .as_ref()        .expect("spawn_agent should use object params");    assert!(properties.contains_key("fork_context"));    assert!(!properties.contains_key("fork_turns"));    assert_eq!(        properties            .get("message")            .and_then(|schema| schema.encrypted),        None    );    assert_eq!(        properties            .get("model")            .and_then(|schema| schema.description.as_deref()),        Some(SPAWN_AGENT_MODEL_OVERRIDE_DESCRIPTION)    );    assert_eq!(        properties            .get("service_tier")            .and_then(|schema| schema.description.as_deref()),        Some(SPAWN_AGENT_SERVICE_TIER_OVERRIDE_DESCRIPTION)    );}#[test]fn spawn_agent_tool_caps_visible_model_summaries() {    let tool = create_spawn_agent_tool_v2(SpawnAgentToolOptions {        available_models: vec![            model_preset("first", /*show_in_picker*/ true),            model_preset("second", /*show_in_picker*/ true),            model_preset("third", /*show_in_picker*/ true),            model_preset("fourth", /*show_in_picker*/ true),            model_preset("fifth", /*show_in_picker*/ true),            model_preset("sixth", /*show_in_picker*/ true),        ],        agent_type_description: "role help".to_string(),        hide_agent_type_model_reasoning: false,        include_usage_hint: true,        usage_hint_text: None,    });    let ToolSpec::Function(ResponsesApiTool { description, .. }) = tool else {        panic!("spawn_agent should be a function tool");    };    for model in ["first", "second", "third", "fourth", "fifth"] {        assert!(            description.contains(&format!("`{model}-model`")),            "expected {model} model summary in spawn_agent description: {description:?}"        );    }    assert!(!description.contains("`sixth-model`"));}#[test]fn spawn_agent_tool_caps_reasoning_effort_value_length() {    let mut model = model_preset("visible", /*show_in_picker*/ true);    let custom_effort = ReasoningEffort::Custom(        "é".repeat(MAX_REASONING_EFFORT_CHARS_IN_SPAWN_AGENT_DESCRIPTION + 1),    );    model.default_reasoning_effort = custom_effort.clone();    model.supported_reasoning_efforts = vec![ReasoningEffortPreset {        effort: custom_effort,        description: "Model-defined".to_string(),    }];    assert_eq!(        spawn_agent_models_description(&[model]),        format!(            "Available model overrides (optional; inherited parent model is preferred):\n- `visible-model`: visible description Reasoning efforts: {} (default). Service tiers: priority.",            "é".repeat(MAX_REASONING_EFFORT_CHARS_IN_SPAWN_AGENT_DESCRIPTION)        )    );}#[test]fn spawn_agent_tool_hides_service_tier_with_spawn_metadata() {    let tool = create_spawn_agent_tool_v2(SpawnAgentToolOptions {        available_models: vec![model_preset("visible", /*show_in_picker*/ true)],        agent_type_description: "role help".to_string(),        hide_agent_type_model_reasoning: true,        include_usage_hint: true,        usage_hint_text: None,    });    let ToolSpec::Function(ResponsesApiTool {        description,        parameters,        ..    }) = tool    else {        panic!("spawn_agent should be a function tool");    };    let properties = parameters        .properties        .as_ref()        .expect("spawn_agent should use object params");    assert!(!properties.contains_key("agent_type"));    assert!(!properties.contains_key("model"));    assert!(!properties.contains_key("reasoning_effort"));    assert!(!properties.contains_key("service_tier"));    assert!(!description.contains(SPAWN_AGENT_INHERITED_MODEL_GUIDANCE));    assert!(!description.contains("Available model overrides"));}#[test]fn send_message_tool_requires_message_and_has_no_output_schema() {    let ToolSpec::Function(ResponsesApiTool {        parameters,        output_schema,        ..    }) = create_send_message_tool()    else {        panic!("send_message should be a function tool");    };    assert_eq!(        parameters.schema_type,        Some(JsonSchemaType::Single(JsonSchemaPrimitiveType::Object))    );    let properties = parameters        .properties        .as_ref()        .expect("send_message should use object params");    assert!(properties.contains_key("target"));    assert!(properties.contains_key("message"));    assert_eq!(        properties            .get("message")            .and_then(|schema| schema.encrypted),        Some(true)    );    assert!(!properties.contains_key("interrupt"));    assert!(!properties.contains_key("items"));    assert_eq!(        properties            .get("target")            .and_then(|schema| schema.description.as_deref()),        Some("Relative or canonical task name to message (from spawn_agent).")    );    assert_eq!(        parameters.required.as_ref(),        Some(&vec!["target".to_string(), "message".to_string()])    );    assert_eq!(output_schema, None);}#[test]fn followup_task_tool_requires_message_and_has_no_output_schema() {    let ToolSpec::Function(ResponsesApiTool {        name,        description,        parameters,        output_schema,        ..    }) = create_followup_task_tool()    else {        panic!("followup_task should be a function tool");    };    assert_eq!(name, "followup_task");    assert_eq!(        description,        "Send a follow-up task to an existing non-root target agent and trigger a turn if it is idle. If the target is already running, deliver the task promptly at message boundaries while sampling, or after the pending tool call completes."    );    assert_eq!(        parameters.schema_type,        Some(JsonSchemaType::Single(JsonSchemaPrimitiveType::Object))    );    let properties = parameters        .properties        .as_ref()        .expect("followup_task should use object params");    assert!(properties.contains_key("target"));    assert!(properties.contains_key("message"));    assert_eq!(        properties            .get("message")            .and_then(|schema| schema.encrypted),        Some(true)    );    assert!(!properties.contains_key("items"));    assert_eq!(        parameters.required.as_ref(),        Some(&vec!["target".to_string(), "message".to_string()])    );    assert_eq!(output_schema, None);}#[test]fn wait_agent_tool_v2_uses_timeout_only_summary_output() {    let ToolSpec::Function(ResponsesApiTool {        description,        parameters,        output_schema,        ..    }) = create_wait_agent_tool_v2(WaitAgentTimeoutOptions {        default_timeout_ms: 30_000,        min_timeout_ms: 10_000,        max_timeout_ms: 3_600_000,    })    else {        panic!("wait_agent should be a function tool");    };    assert_eq!(        parameters.schema_type,        Some(JsonSchemaType::Single(JsonSchemaPrimitiveType::Object))    );    let properties = parameters        .properties        .as_ref()        .expect("wait_agent should use object params");    assert!(!properties.contains_key("targets"));    assert!(properties.contains_key("timeout_ms"));    assert!(description.contains(        "Does not return the content; returns either a summary of which agents have updates (if any)"    ));    assert_eq!(        properties            .get("timeout_ms")            .and_then(|schema| schema.description.as_deref()),        Some("Timeout in milliseconds. Defaults to 30000, min 10000, max 3600000.")    );    assert_eq!(parameters.required.as_ref(), None);    assert_eq!(        output_schema.expect("wait output schema")["properties"]["message"]["description"],        json!("Brief wait summary without the agent's final content.")    );}#[test]fn list_agents_tool_includes_path_prefix_and_agent_fields() {    let ToolSpec::Function(ResponsesApiTool {        parameters,        output_schema,        ..    }) = create_list_agents_tool()    else {        panic!("list_agents should be a function tool");    };    assert_eq!(        parameters.schema_type,        Some(JsonSchemaType::Single(JsonSchemaPrimitiveType::Object))    );    let properties = parameters        .properties        .as_ref()        .expect("list_agents should use object params");    assert!(properties.contains_key("path_prefix"));    assert_eq!(        properties            .get("path_prefix")            .and_then(|schema| schema.description.as_deref()),        Some("Task-path prefix filter without a trailing slash. Omit to list all live agents.")    );    assert_eq!(        output_schema.expect("list_agents output schema")["properties"]["agents"]["items"]["required"],        json!(["agent_name", "agent_status", "last_task_message"])    );}#[test]fn list_agents_tool_status_schema_includes_interrupted() {    let ToolSpec::Function(ResponsesApiTool { output_schema, .. }) = create_list_agents_tool()    else {        panic!("list_agents should be a function tool");    };    assert_eq!(        output_schema.expect("list_agents output schema")["properties"]["agents"]["items"]["properties"]            ["agent_status"]["allOf"][0]["oneOf"][0]["enum"],        json!([            "pending_init",            "running",            "interrupted",            "shutdown",            "not_found"        ])    );}