mirror of
https://github.com/openai/codex.git
synced 2026-04-24 14:45:27 +00:00
- [x] Add resource_uri and other things to _meta to shortcut resource lookup and speed things up.
2796 lines
100 KiB
Rust
2796 lines
100 KiB
Rust
use crate::client_common::tools::FreeformTool;
|
|
use crate::config::test_config;
|
|
use crate::models_manager::manager::ModelsManager;
|
|
use crate::models_manager::model_info::with_config_overrides;
|
|
use crate::shell::Shell;
|
|
use crate::shell::ShellType;
|
|
use crate::tools::ToolRouter;
|
|
use crate::tools::registry::ConfiguredToolSpec;
|
|
use crate::tools::router::ToolRouterParams;
|
|
use codex_app_server_protocol::AppInfo;
|
|
use codex_protocol::openai_models::InputModality;
|
|
use codex_protocol::openai_models::ModelInfo;
|
|
use codex_protocol::openai_models::ModelsResponse;
|
|
use codex_utils_absolute_path::AbsolutePathBuf;
|
|
use pretty_assertions::assert_eq;
|
|
use std::path::PathBuf;
|
|
|
|
use super::*;
|
|
|
|
fn mcp_tool(name: &str, description: &str, input_schema: serde_json::Value) -> rmcp::model::Tool {
|
|
rmcp::model::Tool {
|
|
name: name.to_string().into(),
|
|
title: None,
|
|
description: Some(description.to_string().into()),
|
|
input_schema: std::sync::Arc::new(rmcp::model::object(input_schema)),
|
|
output_schema: None,
|
|
annotations: None,
|
|
execution: None,
|
|
icons: None,
|
|
meta: None,
|
|
}
|
|
}
|
|
|
|
fn discoverable_connector(id: &str, name: &str, description: &str) -> DiscoverableTool {
|
|
let slug = name.replace(' ', "-").to_lowercase();
|
|
DiscoverableTool::Connector(Box::new(AppInfo {
|
|
id: id.to_string(),
|
|
name: name.to_string(),
|
|
description: Some(description.to_string()),
|
|
logo_url: None,
|
|
logo_url_dark: None,
|
|
distribution_channel: None,
|
|
branding: None,
|
|
app_metadata: None,
|
|
labels: None,
|
|
install_url: Some(format!("https://chatgpt.com/apps/{slug}/{id}")),
|
|
is_accessible: false,
|
|
is_enabled: true,
|
|
plugin_display_names: Vec::new(),
|
|
}))
|
|
}
|
|
|
|
fn search_capable_model_info() -> ModelInfo {
|
|
let config = test_config();
|
|
let mut model_info =
|
|
ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config);
|
|
model_info.supports_search_tool = true;
|
|
model_info
|
|
}
|
|
|
|
#[test]
|
|
fn mcp_tool_to_openai_tool_inserts_empty_properties() {
|
|
let mut schema = rmcp::model::JsonObject::new();
|
|
schema.insert("type".to_string(), serde_json::json!("object"));
|
|
|
|
let tool = rmcp::model::Tool {
|
|
name: "no_props".to_string().into(),
|
|
title: None,
|
|
description: Some("No properties".to_string().into()),
|
|
input_schema: std::sync::Arc::new(schema),
|
|
output_schema: None,
|
|
annotations: None,
|
|
execution: None,
|
|
icons: None,
|
|
meta: None,
|
|
};
|
|
|
|
let openai_tool =
|
|
mcp_tool_to_openai_tool("server/no_props".to_string(), tool).expect("convert tool");
|
|
let parameters = serde_json::to_value(openai_tool.parameters).expect("serialize schema");
|
|
|
|
assert_eq!(parameters.get("properties"), Some(&serde_json::json!({})));
|
|
}
|
|
|
|
#[test]
|
|
fn mcp_tool_to_openai_tool_preserves_top_level_output_schema() {
|
|
let mut input_schema = rmcp::model::JsonObject::new();
|
|
input_schema.insert("type".to_string(), serde_json::json!("object"));
|
|
|
|
let mut output_schema = rmcp::model::JsonObject::new();
|
|
output_schema.insert(
|
|
"properties".to_string(),
|
|
serde_json::json!({
|
|
"result": {
|
|
"properties": {
|
|
"nested": {}
|
|
}
|
|
}
|
|
}),
|
|
);
|
|
output_schema.insert("required".to_string(), serde_json::json!(["result"]));
|
|
|
|
let tool = rmcp::model::Tool {
|
|
name: "with_output".to_string().into(),
|
|
title: None,
|
|
description: Some("Has output schema".to_string().into()),
|
|
input_schema: std::sync::Arc::new(input_schema),
|
|
output_schema: Some(std::sync::Arc::new(output_schema)),
|
|
annotations: None,
|
|
execution: None,
|
|
icons: None,
|
|
meta: None,
|
|
};
|
|
|
|
let openai_tool = mcp_tool_to_openai_tool("mcp__server__with_output".to_string(), tool)
|
|
.expect("convert tool");
|
|
|
|
assert_eq!(
|
|
openai_tool.output_schema,
|
|
Some(serde_json::json!({
|
|
"type": "object",
|
|
"properties": {
|
|
"content": {
|
|
"type": "array",
|
|
"items": {}
|
|
},
|
|
"structuredContent": {
|
|
"properties": {
|
|
"result": {
|
|
"properties": {
|
|
"nested": {}
|
|
}
|
|
}
|
|
},
|
|
"required": ["result"]
|
|
},
|
|
"isError": {
|
|
"type": "boolean"
|
|
},
|
|
"_meta": {}
|
|
},
|
|
"required": ["content"],
|
|
"additionalProperties": false
|
|
}))
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn mcp_tool_to_openai_tool_preserves_output_schema_without_inferred_type() {
|
|
let mut input_schema = rmcp::model::JsonObject::new();
|
|
input_schema.insert("type".to_string(), serde_json::json!("object"));
|
|
|
|
let mut output_schema = rmcp::model::JsonObject::new();
|
|
output_schema.insert("enum".to_string(), serde_json::json!(["ok", "error"]));
|
|
|
|
let tool = rmcp::model::Tool {
|
|
name: "with_enum_output".to_string().into(),
|
|
title: None,
|
|
description: Some("Has enum output schema".to_string().into()),
|
|
input_schema: std::sync::Arc::new(input_schema),
|
|
output_schema: Some(std::sync::Arc::new(output_schema)),
|
|
annotations: None,
|
|
execution: None,
|
|
icons: None,
|
|
meta: None,
|
|
};
|
|
|
|
let openai_tool = mcp_tool_to_openai_tool("mcp__server__with_enum_output".to_string(), tool)
|
|
.expect("convert tool");
|
|
|
|
assert_eq!(
|
|
openai_tool.output_schema,
|
|
Some(serde_json::json!({
|
|
"type": "object",
|
|
"properties": {
|
|
"content": {
|
|
"type": "array",
|
|
"items": {}
|
|
},
|
|
"structuredContent": {
|
|
"enum": ["ok", "error"]
|
|
},
|
|
"isError": {
|
|
"type": "boolean"
|
|
},
|
|
"_meta": {}
|
|
},
|
|
"required": ["content"],
|
|
"additionalProperties": false
|
|
}))
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn search_tool_deferred_tools_always_set_defer_loading_true() {
|
|
let tool = mcp_tool(
|
|
"lookup_order",
|
|
"Look up an order",
|
|
serde_json::json!({
|
|
"type": "object",
|
|
"properties": {
|
|
"order_id": {"type": "string"}
|
|
},
|
|
"required": ["order_id"],
|
|
"additionalProperties": false,
|
|
}),
|
|
);
|
|
|
|
let openai_tool =
|
|
mcp_tool_to_deferred_openai_tool("mcp__codex_apps__lookup_order".to_string(), tool)
|
|
.expect("convert deferred tool");
|
|
|
|
assert_eq!(openai_tool.defer_loading, Some(true));
|
|
}
|
|
|
|
#[test]
|
|
fn deferred_responses_api_tool_serializes_with_defer_loading() {
|
|
let tool = mcp_tool(
|
|
"lookup_order",
|
|
"Look up an order",
|
|
serde_json::json!({
|
|
"type": "object",
|
|
"properties": {
|
|
"order_id": {"type": "string"}
|
|
},
|
|
"required": ["order_id"],
|
|
"additionalProperties": false,
|
|
}),
|
|
);
|
|
|
|
let serialized = serde_json::to_value(ToolSpec::Function(
|
|
mcp_tool_to_deferred_openai_tool("mcp__codex_apps__lookup_order".to_string(), tool)
|
|
.expect("convert deferred tool"),
|
|
))
|
|
.expect("serialize deferred tool");
|
|
|
|
assert_eq!(
|
|
serialized,
|
|
serde_json::json!({
|
|
"type": "function",
|
|
"name": "mcp__codex_apps__lookup_order",
|
|
"description": "Look up an order",
|
|
"strict": false,
|
|
"defer_loading": true,
|
|
"parameters": {
|
|
"type": "object",
|
|
"properties": {
|
|
"order_id": {"type": "string"}
|
|
},
|
|
"required": ["order_id"],
|
|
"additionalProperties": false,
|
|
}
|
|
})
|
|
);
|
|
}
|
|
|
|
fn tool_name(tool: &ToolSpec) -> &str {
|
|
match tool {
|
|
ToolSpec::Function(ResponsesApiTool { name, .. }) => name,
|
|
ToolSpec::ToolSearch { .. } => "tool_search",
|
|
ToolSpec::LocalShell {} => "local_shell",
|
|
ToolSpec::ImageGeneration { .. } => "image_generation",
|
|
ToolSpec::WebSearch { .. } => "web_search",
|
|
ToolSpec::Freeform(FreeformTool { name, .. }) => name,
|
|
}
|
|
}
|
|
|
|
// Avoid order-based assertions; compare via set containment instead.
|
|
fn assert_contains_tool_names(tools: &[ConfiguredToolSpec], expected_subset: &[&str]) {
|
|
use std::collections::HashSet;
|
|
let mut names = HashSet::new();
|
|
let mut duplicates = Vec::new();
|
|
for name in tools.iter().map(|t| tool_name(&t.spec)) {
|
|
if !names.insert(name) {
|
|
duplicates.push(name);
|
|
}
|
|
}
|
|
assert!(
|
|
duplicates.is_empty(),
|
|
"duplicate tool entries detected: {duplicates:?}"
|
|
);
|
|
for expected in expected_subset {
|
|
assert!(
|
|
names.contains(expected),
|
|
"expected tool {expected} to be present; had: {names:?}"
|
|
);
|
|
}
|
|
}
|
|
|
|
fn assert_lacks_tool_name(tools: &[ConfiguredToolSpec], expected_absent: &str) {
|
|
let names = tools
|
|
.iter()
|
|
.map(|tool| tool_name(&tool.spec))
|
|
.collect::<Vec<_>>();
|
|
assert!(
|
|
!names.contains(&expected_absent),
|
|
"expected tool {expected_absent} to be absent; had: {names:?}"
|
|
);
|
|
}
|
|
|
|
fn shell_tool_name(config: &ToolsConfig) -> Option<&'static str> {
|
|
match config.shell_type {
|
|
ConfigShellToolType::Default => Some("shell"),
|
|
ConfigShellToolType::Local => Some("local_shell"),
|
|
ConfigShellToolType::UnifiedExec => None,
|
|
ConfigShellToolType::Disabled => None,
|
|
ConfigShellToolType::ShellCommand => Some("shell_command"),
|
|
}
|
|
}
|
|
|
|
fn find_tool<'a>(tools: &'a [ConfiguredToolSpec], expected_name: &str) -> &'a ConfiguredToolSpec {
|
|
tools
|
|
.iter()
|
|
.find(|tool| tool_name(&tool.spec) == expected_name)
|
|
.unwrap_or_else(|| panic!("expected tool {expected_name}"))
|
|
}
|
|
|
|
fn strip_descriptions_schema(schema: &mut JsonSchema) {
|
|
match schema {
|
|
JsonSchema::Boolean { description }
|
|
| JsonSchema::String { description }
|
|
| JsonSchema::Number { description } => {
|
|
*description = None;
|
|
}
|
|
JsonSchema::Array { items, description } => {
|
|
strip_descriptions_schema(items);
|
|
*description = None;
|
|
}
|
|
JsonSchema::Object {
|
|
properties,
|
|
required: _,
|
|
additional_properties,
|
|
} => {
|
|
for v in properties.values_mut() {
|
|
strip_descriptions_schema(v);
|
|
}
|
|
if let Some(AdditionalProperties::Schema(s)) = additional_properties {
|
|
strip_descriptions_schema(s);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn strip_descriptions_tool(spec: &mut ToolSpec) {
|
|
match spec {
|
|
ToolSpec::ToolSearch { parameters, .. } => strip_descriptions_schema(parameters),
|
|
ToolSpec::Function(ResponsesApiTool { parameters, .. }) => {
|
|
strip_descriptions_schema(parameters);
|
|
}
|
|
ToolSpec::Freeform(_)
|
|
| ToolSpec::LocalShell {}
|
|
| ToolSpec::ImageGeneration { .. }
|
|
| ToolSpec::WebSearch { .. } => {}
|
|
}
|
|
}
|
|
|
|
fn model_info_from_models_json(slug: &str) -> ModelInfo {
|
|
let config = test_config();
|
|
let response: ModelsResponse =
|
|
serde_json::from_str(include_str!("../../models.json")).expect("valid models.json");
|
|
let model = response
|
|
.models
|
|
.into_iter()
|
|
.find(|candidate| candidate.slug == slug)
|
|
.unwrap_or_else(|| panic!("model slug {slug} is missing from models.json"));
|
|
with_config_overrides(model, &config)
|
|
}
|
|
|
|
#[test]
|
|
fn unified_exec_is_blocked_for_windows_sandboxed_policies_only() {
|
|
assert!(!unified_exec_allowed_in_environment(
|
|
true,
|
|
&SandboxPolicy::new_read_only_policy(),
|
|
WindowsSandboxLevel::RestrictedToken,
|
|
));
|
|
assert!(!unified_exec_allowed_in_environment(
|
|
true,
|
|
&SandboxPolicy::new_workspace_write_policy(),
|
|
WindowsSandboxLevel::RestrictedToken,
|
|
));
|
|
assert!(unified_exec_allowed_in_environment(
|
|
true,
|
|
&SandboxPolicy::DangerFullAccess,
|
|
WindowsSandboxLevel::RestrictedToken,
|
|
));
|
|
assert!(unified_exec_allowed_in_environment(
|
|
true,
|
|
&SandboxPolicy::DangerFullAccess,
|
|
WindowsSandboxLevel::Disabled,
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn model_provided_unified_exec_is_blocked_for_windows_sandboxed_policies() {
|
|
let mut model_info = model_info_from_models_json("gpt-5-codex");
|
|
model_info.shell_type = ConfigShellToolType::UnifiedExec;
|
|
let features = Features::with_defaults();
|
|
let available_models = Vec::new();
|
|
let config = ToolsConfig::new(&ToolsConfigParams {
|
|
model_info: &model_info,
|
|
available_models: &available_models,
|
|
features: &features,
|
|
web_search_mode: Some(WebSearchMode::Cached),
|
|
session_source: SessionSource::Cli,
|
|
sandbox_policy: &SandboxPolicy::new_workspace_write_policy(),
|
|
windows_sandbox_level: WindowsSandboxLevel::RestrictedToken,
|
|
});
|
|
|
|
let expected_shell_type = if cfg!(target_os = "windows") {
|
|
ConfigShellToolType::ShellCommand
|
|
} else {
|
|
ConfigShellToolType::UnifiedExec
|
|
};
|
|
assert_eq!(config.shell_type, expected_shell_type);
|
|
}
|
|
|
|
#[test]
|
|
fn test_full_toolset_specs_for_gpt5_codex_unified_exec_web_search() {
|
|
let model_info = model_info_from_models_json("gpt-5-codex");
|
|
let mut features = Features::with_defaults();
|
|
features.enable(Feature::UnifiedExec);
|
|
let available_models = Vec::new();
|
|
let config = ToolsConfig::new(&ToolsConfigParams {
|
|
model_info: &model_info,
|
|
available_models: &available_models,
|
|
features: &features,
|
|
web_search_mode: Some(WebSearchMode::Live),
|
|
session_source: SessionSource::Cli,
|
|
sandbox_policy: &SandboxPolicy::DangerFullAccess,
|
|
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
|
});
|
|
let (tools, _) = build_specs(&config, None, None, &[]).build();
|
|
|
|
// Build actual map name -> spec
|
|
use std::collections::BTreeMap;
|
|
use std::collections::HashSet;
|
|
let mut actual: BTreeMap<String, ToolSpec> = BTreeMap::from([]);
|
|
let mut duplicate_names = Vec::new();
|
|
for t in &tools {
|
|
let name = tool_name(&t.spec).to_string();
|
|
if actual.insert(name.clone(), t.spec.clone()).is_some() {
|
|
duplicate_names.push(name);
|
|
}
|
|
}
|
|
assert!(
|
|
duplicate_names.is_empty(),
|
|
"duplicate tool entries detected: {duplicate_names:?}"
|
|
);
|
|
|
|
// Build expected from the same helpers used by the builder.
|
|
let mut expected: BTreeMap<String, ToolSpec> = BTreeMap::from([]);
|
|
for spec in [
|
|
create_exec_command_tool(true, false),
|
|
create_write_stdin_tool(),
|
|
PLAN_TOOL.clone(),
|
|
create_request_user_input_tool(CollaborationModesConfig::default()),
|
|
create_apply_patch_freeform_tool(),
|
|
ToolSpec::WebSearch {
|
|
external_web_access: Some(true),
|
|
filters: None,
|
|
user_location: None,
|
|
search_context_size: None,
|
|
search_content_types: None,
|
|
},
|
|
create_view_image_tool(config.can_request_original_image_detail),
|
|
create_spawn_agent_tool(&config),
|
|
create_send_input_tool(),
|
|
create_resume_agent_tool(),
|
|
create_wait_agent_tool(),
|
|
create_close_agent_tool(),
|
|
] {
|
|
expected.insert(tool_name(&spec).to_string(), spec);
|
|
}
|
|
|
|
if config.exec_permission_approvals_enabled {
|
|
let spec = create_request_permissions_tool();
|
|
expected.insert(tool_name(&spec).to_string(), spec);
|
|
}
|
|
|
|
// Exact name set match — this is the only test allowed to fail when tools change.
|
|
let actual_names: HashSet<_> = actual.keys().cloned().collect();
|
|
let expected_names: HashSet<_> = expected.keys().cloned().collect();
|
|
assert_eq!(actual_names, expected_names, "tool name set mismatch");
|
|
|
|
// Compare specs ignoring human-readable descriptions.
|
|
for name in expected.keys() {
|
|
let mut a = actual.get(name).expect("present").clone();
|
|
let mut e = expected.get(name).expect("present").clone();
|
|
strip_descriptions_tool(&mut a);
|
|
strip_descriptions_tool(&mut e);
|
|
assert_eq!(a, e, "spec mismatch for {name}");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_build_specs_collab_tools_enabled() {
|
|
let config = test_config();
|
|
let model_info = ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config);
|
|
let mut features = Features::with_defaults();
|
|
features.enable(Feature::Collab);
|
|
let available_models = Vec::new();
|
|
let tools_config = ToolsConfig::new(&ToolsConfigParams {
|
|
model_info: &model_info,
|
|
available_models: &available_models,
|
|
features: &features,
|
|
web_search_mode: Some(WebSearchMode::Cached),
|
|
session_source: SessionSource::Cli,
|
|
sandbox_policy: &SandboxPolicy::DangerFullAccess,
|
|
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
|
});
|
|
let (tools, _) = build_specs(&tools_config, None, None, &[]).build();
|
|
assert_contains_tool_names(
|
|
&tools,
|
|
&["spawn_agent", "send_input", "wait_agent", "close_agent"],
|
|
);
|
|
assert_lacks_tool_name(&tools, "spawn_agents_on_csv");
|
|
}
|
|
|
|
#[test]
|
|
fn test_build_specs_enable_fanout_enables_agent_jobs_and_collab_tools() {
|
|
let config = test_config();
|
|
let model_info = ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config);
|
|
let mut features = Features::with_defaults();
|
|
features.enable(Feature::SpawnCsv);
|
|
features.normalize_dependencies();
|
|
let available_models = Vec::new();
|
|
let tools_config = ToolsConfig::new(&ToolsConfigParams {
|
|
model_info: &model_info,
|
|
available_models: &available_models,
|
|
features: &features,
|
|
web_search_mode: Some(WebSearchMode::Cached),
|
|
session_source: SessionSource::Cli,
|
|
sandbox_policy: &SandboxPolicy::DangerFullAccess,
|
|
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
|
});
|
|
let (tools, _) = build_specs(&tools_config, None, None, &[]).build();
|
|
assert_contains_tool_names(
|
|
&tools,
|
|
&[
|
|
"spawn_agent",
|
|
"send_input",
|
|
"wait_agent",
|
|
"close_agent",
|
|
"spawn_agents_on_csv",
|
|
],
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn view_image_tool_omits_detail_without_original_detail_feature() {
|
|
let config = test_config();
|
|
let mut model_info =
|
|
ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config);
|
|
model_info.supports_image_detail_original = true;
|
|
let features = Features::with_defaults();
|
|
let available_models = Vec::new();
|
|
let tools_config = ToolsConfig::new(&ToolsConfigParams {
|
|
model_info: &model_info,
|
|
available_models: &available_models,
|
|
features: &features,
|
|
web_search_mode: Some(WebSearchMode::Cached),
|
|
session_source: SessionSource::Cli,
|
|
sandbox_policy: &SandboxPolicy::DangerFullAccess,
|
|
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
|
});
|
|
let (tools, _) = build_specs(&tools_config, None, None, &[]).build();
|
|
let view_image = find_tool(&tools, VIEW_IMAGE_TOOL_NAME);
|
|
let ToolSpec::Function(ResponsesApiTool { parameters, .. }) = &view_image.spec else {
|
|
panic!("view_image should be a function tool");
|
|
};
|
|
let JsonSchema::Object { properties, .. } = parameters else {
|
|
panic!("view_image should use an object schema");
|
|
};
|
|
assert!(!properties.contains_key("detail"));
|
|
}
|
|
|
|
#[test]
|
|
fn view_image_tool_includes_detail_with_original_detail_feature() {
|
|
let config = test_config();
|
|
let mut model_info =
|
|
ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config);
|
|
model_info.supports_image_detail_original = true;
|
|
let mut features = Features::with_defaults();
|
|
features.enable(Feature::ImageDetailOriginal);
|
|
let available_models = Vec::new();
|
|
let tools_config = ToolsConfig::new(&ToolsConfigParams {
|
|
model_info: &model_info,
|
|
available_models: &available_models,
|
|
features: &features,
|
|
web_search_mode: Some(WebSearchMode::Cached),
|
|
session_source: SessionSource::Cli,
|
|
sandbox_policy: &SandboxPolicy::DangerFullAccess,
|
|
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
|
});
|
|
let (tools, _) = build_specs(&tools_config, None, None, &[]).build();
|
|
let view_image = find_tool(&tools, VIEW_IMAGE_TOOL_NAME);
|
|
let ToolSpec::Function(ResponsesApiTool { parameters, .. }) = &view_image.spec else {
|
|
panic!("view_image should be a function tool");
|
|
};
|
|
let JsonSchema::Object { properties, .. } = parameters else {
|
|
panic!("view_image should use an object schema");
|
|
};
|
|
assert!(properties.contains_key("detail"));
|
|
let Some(JsonSchema::String {
|
|
description: Some(description),
|
|
}) = properties.get("detail")
|
|
else {
|
|
panic!("view_image detail should include a description");
|
|
};
|
|
assert!(description.contains("only supported value is `original`"));
|
|
assert!(description.contains("omit this field for default resized behavior"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_build_specs_artifact_tool_enabled() {
|
|
let mut config = test_config();
|
|
let runtime_root = tempfile::TempDir::new().expect("create temp codex home");
|
|
config.codex_home = runtime_root.path().to_path_buf();
|
|
let model_info = ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config);
|
|
let mut features = Features::with_defaults();
|
|
features.enable(Feature::Artifact);
|
|
let available_models = Vec::new();
|
|
let tools_config = ToolsConfig::new(&ToolsConfigParams {
|
|
model_info: &model_info,
|
|
available_models: &available_models,
|
|
features: &features,
|
|
web_search_mode: Some(WebSearchMode::Cached),
|
|
session_source: SessionSource::Cli,
|
|
sandbox_policy: &SandboxPolicy::DangerFullAccess,
|
|
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
|
});
|
|
let (tools, _) = build_specs(&tools_config, None, None, &[]).build();
|
|
assert_contains_tool_names(&tools, &["artifacts"]);
|
|
}
|
|
|
|
#[test]
|
|
fn test_build_specs_agent_job_worker_tools_enabled() {
|
|
let config = test_config();
|
|
let model_info = ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config);
|
|
let mut features = Features::with_defaults();
|
|
features.enable(Feature::SpawnCsv);
|
|
features.normalize_dependencies();
|
|
features.enable(Feature::Sqlite);
|
|
let available_models = Vec::new();
|
|
let tools_config = ToolsConfig::new(&ToolsConfigParams {
|
|
model_info: &model_info,
|
|
available_models: &available_models,
|
|
features: &features,
|
|
web_search_mode: Some(WebSearchMode::Cached),
|
|
session_source: SessionSource::SubAgent(SubAgentSource::Other(
|
|
"agent_job:test".to_string(),
|
|
)),
|
|
sandbox_policy: &SandboxPolicy::DangerFullAccess,
|
|
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
|
});
|
|
let (tools, _) = build_specs(&tools_config, None, None, &[]).build();
|
|
assert_contains_tool_names(
|
|
&tools,
|
|
&[
|
|
"spawn_agent",
|
|
"send_input",
|
|
"resume_agent",
|
|
"wait_agent",
|
|
"close_agent",
|
|
"spawn_agents_on_csv",
|
|
"report_agent_job_result",
|
|
],
|
|
);
|
|
assert_lacks_tool_name(&tools, "request_user_input");
|
|
}
|
|
|
|
#[test]
|
|
fn request_user_input_description_reflects_default_mode_feature_flag() {
|
|
let config = test_config();
|
|
let model_info = ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config);
|
|
let mut features = Features::with_defaults();
|
|
let available_models = Vec::new();
|
|
let tools_config = ToolsConfig::new(&ToolsConfigParams {
|
|
model_info: &model_info,
|
|
available_models: &available_models,
|
|
features: &features,
|
|
web_search_mode: Some(WebSearchMode::Cached),
|
|
session_source: SessionSource::Cli,
|
|
sandbox_policy: &SandboxPolicy::DangerFullAccess,
|
|
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
|
});
|
|
let (tools, _) = build_specs(&tools_config, None, None, &[]).build();
|
|
let request_user_input_tool = find_tool(&tools, "request_user_input");
|
|
assert_eq!(
|
|
request_user_input_tool.spec,
|
|
create_request_user_input_tool(CollaborationModesConfig::default())
|
|
);
|
|
|
|
features.enable(Feature::DefaultModeRequestUserInput);
|
|
let available_models = Vec::new();
|
|
let tools_config = ToolsConfig::new(&ToolsConfigParams {
|
|
model_info: &model_info,
|
|
available_models: &available_models,
|
|
features: &features,
|
|
web_search_mode: Some(WebSearchMode::Cached),
|
|
session_source: SessionSource::Cli,
|
|
sandbox_policy: &SandboxPolicy::DangerFullAccess,
|
|
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
|
});
|
|
let (tools, _) = build_specs(&tools_config, None, None, &[]).build();
|
|
let request_user_input_tool = find_tool(&tools, "request_user_input");
|
|
assert_eq!(
|
|
request_user_input_tool.spec,
|
|
create_request_user_input_tool(CollaborationModesConfig {
|
|
default_mode_request_user_input: true,
|
|
})
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn request_permissions_requires_feature_flag() {
|
|
let config = test_config();
|
|
let model_info = ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config);
|
|
let features = Features::with_defaults();
|
|
let available_models = Vec::new();
|
|
let tools_config = ToolsConfig::new(&ToolsConfigParams {
|
|
model_info: &model_info,
|
|
available_models: &available_models,
|
|
features: &features,
|
|
web_search_mode: Some(WebSearchMode::Cached),
|
|
session_source: SessionSource::Cli,
|
|
sandbox_policy: &SandboxPolicy::DangerFullAccess,
|
|
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
|
});
|
|
let (tools, _) = build_specs(&tools_config, None, None, &[]).build();
|
|
assert_lacks_tool_name(&tools, "request_permissions");
|
|
|
|
let mut features = Features::with_defaults();
|
|
features.enable(Feature::RequestPermissionsTool);
|
|
let available_models = Vec::new();
|
|
let tools_config = ToolsConfig::new(&ToolsConfigParams {
|
|
model_info: &model_info,
|
|
available_models: &available_models,
|
|
features: &features,
|
|
web_search_mode: Some(WebSearchMode::Cached),
|
|
session_source: SessionSource::Cli,
|
|
sandbox_policy: &SandboxPolicy::DangerFullAccess,
|
|
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
|
});
|
|
let (tools, _) = build_specs(&tools_config, None, None, &[]).build();
|
|
let request_permissions_tool = find_tool(&tools, "request_permissions");
|
|
assert_eq!(
|
|
request_permissions_tool.spec,
|
|
create_request_permissions_tool()
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn request_permissions_tool_is_independent_from_additional_permissions() {
|
|
let config = test_config();
|
|
let model_info = ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config);
|
|
let mut features = Features::with_defaults();
|
|
features.enable(Feature::ExecPermissionApprovals);
|
|
let available_models = Vec::new();
|
|
let tools_config = ToolsConfig::new(&ToolsConfigParams {
|
|
model_info: &model_info,
|
|
available_models: &available_models,
|
|
features: &features,
|
|
web_search_mode: Some(WebSearchMode::Cached),
|
|
session_source: SessionSource::Cli,
|
|
sandbox_policy: &SandboxPolicy::DangerFullAccess,
|
|
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
|
});
|
|
let (tools, _) = build_specs(&tools_config, None, None, &[]).build();
|
|
|
|
assert_lacks_tool_name(&tools, "request_permissions");
|
|
}
|
|
|
|
#[test]
|
|
fn get_memory_requires_feature_flag() {
|
|
let config = test_config();
|
|
let model_info = ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config);
|
|
let mut features = Features::with_defaults();
|
|
features.disable(Feature::MemoryTool);
|
|
let available_models = Vec::new();
|
|
let tools_config = ToolsConfig::new(&ToolsConfigParams {
|
|
model_info: &model_info,
|
|
available_models: &available_models,
|
|
features: &features,
|
|
web_search_mode: Some(WebSearchMode::Cached),
|
|
session_source: SessionSource::Cli,
|
|
sandbox_policy: &SandboxPolicy::DangerFullAccess,
|
|
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
|
});
|
|
let (tools, _) = build_specs(&tools_config, None, None, &[]).build();
|
|
assert!(
|
|
!tools.iter().any(|t| t.spec.name() == "get_memory"),
|
|
"get_memory should be disabled when memory_tool feature is off"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn js_repl_requires_feature_flag() {
|
|
let config = test_config();
|
|
let model_info = ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config);
|
|
let features = Features::with_defaults();
|
|
|
|
let available_models = Vec::new();
|
|
let tools_config = ToolsConfig::new(&ToolsConfigParams {
|
|
model_info: &model_info,
|
|
available_models: &available_models,
|
|
features: &features,
|
|
web_search_mode: Some(WebSearchMode::Cached),
|
|
session_source: SessionSource::Cli,
|
|
sandbox_policy: &SandboxPolicy::DangerFullAccess,
|
|
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
|
});
|
|
let (tools, _) = build_specs(&tools_config, None, None, &[]).build();
|
|
|
|
assert!(
|
|
!tools.iter().any(|tool| tool.spec.name() == "js_repl"),
|
|
"js_repl should be disabled when the feature is off"
|
|
);
|
|
assert!(
|
|
!tools.iter().any(|tool| tool.spec.name() == "js_repl_reset"),
|
|
"js_repl_reset should be disabled when the feature is off"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn js_repl_enabled_adds_tools() {
|
|
let config = test_config();
|
|
let model_info = ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config);
|
|
let mut features = Features::with_defaults();
|
|
features.enable(Feature::JsRepl);
|
|
|
|
let available_models = Vec::new();
|
|
let tools_config = ToolsConfig::new(&ToolsConfigParams {
|
|
model_info: &model_info,
|
|
available_models: &available_models,
|
|
features: &features,
|
|
web_search_mode: Some(WebSearchMode::Cached),
|
|
session_source: SessionSource::Cli,
|
|
sandbox_policy: &SandboxPolicy::DangerFullAccess,
|
|
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
|
});
|
|
let (tools, _) = build_specs(&tools_config, None, None, &[]).build();
|
|
assert_contains_tool_names(&tools, &["js_repl", "js_repl_reset"]);
|
|
}
|
|
|
|
#[test]
|
|
fn image_generation_tools_require_feature_and_supported_model() {
|
|
let config = test_config();
|
|
let mut supported_model_info =
|
|
ModelsManager::construct_model_info_offline_for_tests("gpt-5.2", &config);
|
|
supported_model_info.slug = "custom/gpt-5.2-variant".to_string();
|
|
let mut unsupported_model_info = supported_model_info.clone();
|
|
unsupported_model_info.input_modalities = vec![InputModality::Text];
|
|
let default_features = Features::with_defaults();
|
|
let mut image_generation_features = default_features.clone();
|
|
image_generation_features.enable(Feature::ImageGeneration);
|
|
|
|
let available_models = Vec::new();
|
|
let default_tools_config = ToolsConfig::new(&ToolsConfigParams {
|
|
model_info: &supported_model_info,
|
|
available_models: &available_models,
|
|
features: &default_features,
|
|
web_search_mode: Some(WebSearchMode::Cached),
|
|
session_source: SessionSource::Cli,
|
|
sandbox_policy: &SandboxPolicy::DangerFullAccess,
|
|
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
|
});
|
|
let (default_tools, _) = build_specs(&default_tools_config, None, None, &[]).build();
|
|
assert!(
|
|
!default_tools
|
|
.iter()
|
|
.any(|tool| tool.spec.name() == "image_generation"),
|
|
"image_generation should be disabled by default"
|
|
);
|
|
|
|
let supported_tools_config = ToolsConfig::new(&ToolsConfigParams {
|
|
model_info: &supported_model_info,
|
|
available_models: &available_models,
|
|
features: &image_generation_features,
|
|
web_search_mode: Some(WebSearchMode::Cached),
|
|
session_source: SessionSource::Cli,
|
|
sandbox_policy: &SandboxPolicy::DangerFullAccess,
|
|
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
|
});
|
|
let (supported_tools, _) = build_specs(&supported_tools_config, None, None, &[]).build();
|
|
assert_contains_tool_names(&supported_tools, &["image_generation"]);
|
|
let image_generation_tool = find_tool(&supported_tools, "image_generation");
|
|
assert_eq!(
|
|
serde_json::to_value(&image_generation_tool.spec).expect("serialize image tool"),
|
|
serde_json::json!({
|
|
"type": "image_generation",
|
|
"output_format": "png"
|
|
})
|
|
);
|
|
|
|
let tools_config = ToolsConfig::new(&ToolsConfigParams {
|
|
model_info: &unsupported_model_info,
|
|
available_models: &available_models,
|
|
features: &image_generation_features,
|
|
web_search_mode: Some(WebSearchMode::Cached),
|
|
session_source: SessionSource::Cli,
|
|
sandbox_policy: &SandboxPolicy::DangerFullAccess,
|
|
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
|
});
|
|
let (tools, _) = build_specs(&tools_config, None, None, &[]).build();
|
|
assert!(
|
|
!tools
|
|
.iter()
|
|
.any(|tool| tool.spec.name() == "image_generation"),
|
|
"image_generation should be disabled for unsupported models"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn js_repl_freeform_grammar_blocks_common_non_js_prefixes() {
|
|
let ToolSpec::Freeform(FreeformTool { format, .. }) = create_js_repl_tool() else {
|
|
panic!("js_repl should use a freeform tool spec");
|
|
};
|
|
|
|
assert_eq!(format.syntax, "lark");
|
|
assert!(format.definition.contains("PRAGMA_LINE"));
|
|
assert!(format.definition.contains("`[^`]"));
|
|
assert!(format.definition.contains("``[^`]"));
|
|
assert!(format.definition.contains("PLAIN_JS_SOURCE"));
|
|
assert!(format.definition.contains("codex-js-repl:"));
|
|
assert!(!format.definition.contains("(?!"));
|
|
}
|
|
|
|
fn assert_model_tools(
|
|
model_slug: &str,
|
|
features: &Features,
|
|
web_search_mode: Option<WebSearchMode>,
|
|
expected_tools: &[&str],
|
|
) {
|
|
let _config = test_config();
|
|
let model_info = model_info_from_models_json(model_slug);
|
|
let available_models = Vec::new();
|
|
let tools_config = ToolsConfig::new(&ToolsConfigParams {
|
|
model_info: &model_info,
|
|
available_models: &available_models,
|
|
features,
|
|
web_search_mode,
|
|
session_source: SessionSource::Cli,
|
|
sandbox_policy: &SandboxPolicy::DangerFullAccess,
|
|
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
|
});
|
|
let router = ToolRouter::from_config(
|
|
&tools_config,
|
|
ToolRouterParams {
|
|
mcp_tools: None,
|
|
app_tools: None,
|
|
discoverable_tools: None,
|
|
dynamic_tools: &[],
|
|
},
|
|
);
|
|
let model_visible_specs = router.model_visible_specs();
|
|
let tool_names = model_visible_specs
|
|
.iter()
|
|
.map(ToolSpec::name)
|
|
.collect::<Vec<_>>();
|
|
assert_eq!(&tool_names, &expected_tools,);
|
|
}
|
|
|
|
fn assert_default_model_tools(
|
|
model_slug: &str,
|
|
features: &Features,
|
|
web_search_mode: Option<WebSearchMode>,
|
|
shell_tool: &'static str,
|
|
expected_tail: &[&str],
|
|
) {
|
|
let mut expected = if features.enabled(Feature::UnifiedExec) {
|
|
vec!["exec_command", "write_stdin"]
|
|
} else {
|
|
vec![shell_tool]
|
|
};
|
|
expected.extend(expected_tail);
|
|
assert_model_tools(model_slug, features, web_search_mode, &expected);
|
|
}
|
|
|
|
#[test]
|
|
fn web_search_mode_cached_sets_external_web_access_false() {
|
|
let config = test_config();
|
|
let model_info = ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config);
|
|
let features = Features::with_defaults();
|
|
|
|
let available_models = Vec::new();
|
|
let tools_config = ToolsConfig::new(&ToolsConfigParams {
|
|
model_info: &model_info,
|
|
available_models: &available_models,
|
|
features: &features,
|
|
web_search_mode: Some(WebSearchMode::Cached),
|
|
session_source: SessionSource::Cli,
|
|
sandbox_policy: &SandboxPolicy::DangerFullAccess,
|
|
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
|
});
|
|
let (tools, _) = build_specs(&tools_config, None, None, &[]).build();
|
|
|
|
let tool = find_tool(&tools, "web_search");
|
|
assert_eq!(
|
|
tool.spec,
|
|
ToolSpec::WebSearch {
|
|
external_web_access: Some(false),
|
|
filters: None,
|
|
user_location: None,
|
|
search_context_size: None,
|
|
search_content_types: None,
|
|
}
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn web_search_mode_live_sets_external_web_access_true() {
|
|
let config = test_config();
|
|
let model_info = ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config);
|
|
let features = Features::with_defaults();
|
|
|
|
let available_models = Vec::new();
|
|
let tools_config = ToolsConfig::new(&ToolsConfigParams {
|
|
model_info: &model_info,
|
|
available_models: &available_models,
|
|
features: &features,
|
|
web_search_mode: Some(WebSearchMode::Live),
|
|
session_source: SessionSource::Cli,
|
|
sandbox_policy: &SandboxPolicy::DangerFullAccess,
|
|
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
|
});
|
|
let (tools, _) = build_specs(&tools_config, None, None, &[]).build();
|
|
|
|
let tool = find_tool(&tools, "web_search");
|
|
assert_eq!(
|
|
tool.spec,
|
|
ToolSpec::WebSearch {
|
|
external_web_access: Some(true),
|
|
filters: None,
|
|
user_location: None,
|
|
search_context_size: None,
|
|
search_content_types: None,
|
|
}
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn web_search_config_is_forwarded_to_tool_spec() {
|
|
let config = test_config();
|
|
let model_info = ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config);
|
|
let features = Features::with_defaults();
|
|
let web_search_config = WebSearchConfig {
|
|
filters: Some(codex_protocol::config_types::WebSearchFilters {
|
|
allowed_domains: Some(vec!["example.com".to_string()]),
|
|
}),
|
|
user_location: Some(codex_protocol::config_types::WebSearchUserLocation {
|
|
r#type: codex_protocol::config_types::WebSearchUserLocationType::Approximate,
|
|
country: Some("US".to_string()),
|
|
region: Some("California".to_string()),
|
|
city: Some("San Francisco".to_string()),
|
|
timezone: Some("America/Los_Angeles".to_string()),
|
|
}),
|
|
search_context_size: Some(codex_protocol::config_types::WebSearchContextSize::High),
|
|
};
|
|
|
|
let available_models = Vec::new();
|
|
let tools_config = ToolsConfig::new(&ToolsConfigParams {
|
|
model_info: &model_info,
|
|
available_models: &available_models,
|
|
features: &features,
|
|
web_search_mode: Some(WebSearchMode::Live),
|
|
session_source: SessionSource::Cli,
|
|
sandbox_policy: &SandboxPolicy::DangerFullAccess,
|
|
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
|
})
|
|
.with_web_search_config(Some(web_search_config.clone()));
|
|
let (tools, _) = build_specs(&tools_config, None, None, &[]).build();
|
|
|
|
let tool = find_tool(&tools, "web_search");
|
|
assert_eq!(
|
|
tool.spec,
|
|
ToolSpec::WebSearch {
|
|
external_web_access: Some(true),
|
|
filters: web_search_config
|
|
.filters
|
|
.map(crate::client_common::tools::ResponsesApiWebSearchFilters::from),
|
|
user_location: web_search_config
|
|
.user_location
|
|
.map(crate::client_common::tools::ResponsesApiWebSearchUserLocation::from),
|
|
search_context_size: web_search_config.search_context_size,
|
|
search_content_types: None,
|
|
}
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn web_search_tool_type_text_and_image_sets_search_content_types() {
|
|
let config = test_config();
|
|
let mut model_info =
|
|
ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config);
|
|
model_info.web_search_tool_type = WebSearchToolType::TextAndImage;
|
|
let features = Features::with_defaults();
|
|
|
|
let available_models = Vec::new();
|
|
let tools_config = ToolsConfig::new(&ToolsConfigParams {
|
|
model_info: &model_info,
|
|
available_models: &available_models,
|
|
features: &features,
|
|
web_search_mode: Some(WebSearchMode::Live),
|
|
session_source: SessionSource::Cli,
|
|
sandbox_policy: &SandboxPolicy::DangerFullAccess,
|
|
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
|
});
|
|
let (tools, _) = build_specs(&tools_config, None, None, &[]).build();
|
|
|
|
let tool = find_tool(&tools, "web_search");
|
|
assert_eq!(
|
|
tool.spec,
|
|
ToolSpec::WebSearch {
|
|
external_web_access: Some(true),
|
|
filters: None,
|
|
user_location: None,
|
|
search_context_size: None,
|
|
search_content_types: Some(
|
|
WEB_SEARCH_CONTENT_TYPES
|
|
.into_iter()
|
|
.map(str::to_string)
|
|
.collect()
|
|
),
|
|
}
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn mcp_resource_tools_are_hidden_without_mcp_servers() {
|
|
let config = test_config();
|
|
let model_info = ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config);
|
|
let features = Features::with_defaults();
|
|
let available_models = Vec::new();
|
|
let tools_config = ToolsConfig::new(&ToolsConfigParams {
|
|
model_info: &model_info,
|
|
available_models: &available_models,
|
|
features: &features,
|
|
web_search_mode: Some(WebSearchMode::Cached),
|
|
session_source: SessionSource::Cli,
|
|
sandbox_policy: &SandboxPolicy::DangerFullAccess,
|
|
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
|
});
|
|
let (tools, _) = build_specs(&tools_config, None, None, &[]).build();
|
|
|
|
assert!(
|
|
!tools.iter().any(|tool| matches!(
|
|
tool.spec.name(),
|
|
"list_mcp_resources" | "list_mcp_resource_templates" | "read_mcp_resource"
|
|
)),
|
|
"MCP resource tools should be omitted when no MCP servers are configured"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn mcp_resource_tools_are_included_when_mcp_servers_are_present() {
|
|
let config = test_config();
|
|
let model_info = ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config);
|
|
let features = Features::with_defaults();
|
|
let available_models = Vec::new();
|
|
let tools_config = ToolsConfig::new(&ToolsConfigParams {
|
|
model_info: &model_info,
|
|
available_models: &available_models,
|
|
features: &features,
|
|
web_search_mode: Some(WebSearchMode::Cached),
|
|
session_source: SessionSource::Cli,
|
|
sandbox_policy: &SandboxPolicy::DangerFullAccess,
|
|
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
|
});
|
|
let (tools, _) = build_specs(&tools_config, Some(HashMap::new()), None, &[]).build();
|
|
|
|
assert_contains_tool_names(
|
|
&tools,
|
|
&[
|
|
"list_mcp_resources",
|
|
"list_mcp_resource_templates",
|
|
"read_mcp_resource",
|
|
],
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_build_specs_gpt5_codex_default() {
|
|
let features = Features::with_defaults();
|
|
assert_default_model_tools(
|
|
"gpt-5-codex",
|
|
&features,
|
|
Some(WebSearchMode::Cached),
|
|
"shell_command",
|
|
&[
|
|
"update_plan",
|
|
"request_user_input",
|
|
"apply_patch",
|
|
"web_search",
|
|
"view_image",
|
|
"spawn_agent",
|
|
"send_input",
|
|
"resume_agent",
|
|
"wait_agent",
|
|
"close_agent",
|
|
],
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_build_specs_gpt51_codex_default() {
|
|
let features = Features::with_defaults();
|
|
assert_default_model_tools(
|
|
"gpt-5.1-codex",
|
|
&features,
|
|
Some(WebSearchMode::Cached),
|
|
"shell_command",
|
|
&[
|
|
"update_plan",
|
|
"request_user_input",
|
|
"apply_patch",
|
|
"web_search",
|
|
"view_image",
|
|
"spawn_agent",
|
|
"send_input",
|
|
"resume_agent",
|
|
"wait_agent",
|
|
"close_agent",
|
|
],
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_build_specs_gpt5_codex_unified_exec_web_search() {
|
|
let mut features = Features::with_defaults();
|
|
features.enable(Feature::UnifiedExec);
|
|
assert_model_tools(
|
|
"gpt-5-codex",
|
|
&features,
|
|
Some(WebSearchMode::Live),
|
|
&[
|
|
"exec_command",
|
|
"write_stdin",
|
|
"update_plan",
|
|
"request_user_input",
|
|
"apply_patch",
|
|
"web_search",
|
|
"view_image",
|
|
"spawn_agent",
|
|
"send_input",
|
|
"resume_agent",
|
|
"wait_agent",
|
|
"close_agent",
|
|
],
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_build_specs_gpt51_codex_unified_exec_web_search() {
|
|
let mut features = Features::with_defaults();
|
|
features.enable(Feature::UnifiedExec);
|
|
assert_model_tools(
|
|
"gpt-5.1-codex",
|
|
&features,
|
|
Some(WebSearchMode::Live),
|
|
&[
|
|
"exec_command",
|
|
"write_stdin",
|
|
"update_plan",
|
|
"request_user_input",
|
|
"apply_patch",
|
|
"web_search",
|
|
"view_image",
|
|
"spawn_agent",
|
|
"send_input",
|
|
"resume_agent",
|
|
"wait_agent",
|
|
"close_agent",
|
|
],
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_gpt_5_1_codex_max_defaults() {
|
|
let features = Features::with_defaults();
|
|
assert_default_model_tools(
|
|
"gpt-5.1-codex-max",
|
|
&features,
|
|
Some(WebSearchMode::Cached),
|
|
"shell_command",
|
|
&[
|
|
"update_plan",
|
|
"request_user_input",
|
|
"apply_patch",
|
|
"web_search",
|
|
"view_image",
|
|
"spawn_agent",
|
|
"send_input",
|
|
"resume_agent",
|
|
"wait_agent",
|
|
"close_agent",
|
|
],
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_codex_5_1_mini_defaults() {
|
|
let features = Features::with_defaults();
|
|
assert_default_model_tools(
|
|
"gpt-5.1-codex-mini",
|
|
&features,
|
|
Some(WebSearchMode::Cached),
|
|
"shell_command",
|
|
&[
|
|
"update_plan",
|
|
"request_user_input",
|
|
"apply_patch",
|
|
"web_search",
|
|
"view_image",
|
|
"spawn_agent",
|
|
"send_input",
|
|
"resume_agent",
|
|
"wait_agent",
|
|
"close_agent",
|
|
],
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_gpt_5_defaults() {
|
|
let features = Features::with_defaults();
|
|
assert_default_model_tools(
|
|
"gpt-5",
|
|
&features,
|
|
Some(WebSearchMode::Cached),
|
|
"shell",
|
|
&[
|
|
"update_plan",
|
|
"request_user_input",
|
|
"web_search",
|
|
"view_image",
|
|
"spawn_agent",
|
|
"send_input",
|
|
"resume_agent",
|
|
"wait_agent",
|
|
"close_agent",
|
|
],
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_gpt_5_1_defaults() {
|
|
let features = Features::with_defaults();
|
|
assert_default_model_tools(
|
|
"gpt-5.1",
|
|
&features,
|
|
Some(WebSearchMode::Cached),
|
|
"shell_command",
|
|
&[
|
|
"update_plan",
|
|
"request_user_input",
|
|
"apply_patch",
|
|
"web_search",
|
|
"view_image",
|
|
"spawn_agent",
|
|
"send_input",
|
|
"resume_agent",
|
|
"wait_agent",
|
|
"close_agent",
|
|
],
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_gpt_5_1_codex_max_unified_exec_web_search() {
|
|
let mut features = Features::with_defaults();
|
|
features.enable(Feature::UnifiedExec);
|
|
assert_model_tools(
|
|
"gpt-5.1-codex-max",
|
|
&features,
|
|
Some(WebSearchMode::Live),
|
|
&[
|
|
"exec_command",
|
|
"write_stdin",
|
|
"update_plan",
|
|
"request_user_input",
|
|
"apply_patch",
|
|
"web_search",
|
|
"view_image",
|
|
"spawn_agent",
|
|
"send_input",
|
|
"resume_agent",
|
|
"wait_agent",
|
|
"close_agent",
|
|
],
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_build_specs_default_shell_present() {
|
|
let config = test_config();
|
|
let model_info = ModelsManager::construct_model_info_offline_for_tests("o3", &config);
|
|
let mut features = Features::with_defaults();
|
|
features.enable(Feature::UnifiedExec);
|
|
let available_models = Vec::new();
|
|
let tools_config = ToolsConfig::new(&ToolsConfigParams {
|
|
model_info: &model_info,
|
|
available_models: &available_models,
|
|
features: &features,
|
|
web_search_mode: Some(WebSearchMode::Live),
|
|
session_source: SessionSource::Cli,
|
|
sandbox_policy: &SandboxPolicy::DangerFullAccess,
|
|
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
|
});
|
|
let (tools, _) = build_specs(&tools_config, Some(HashMap::new()), None, &[]).build();
|
|
|
|
// Only check the shell variant and a couple of core tools.
|
|
let mut subset = vec!["exec_command", "write_stdin", "update_plan"];
|
|
if let Some(shell_tool) = shell_tool_name(&tools_config) {
|
|
subset.push(shell_tool);
|
|
}
|
|
assert_contains_tool_names(&tools, &subset);
|
|
}
|
|
|
|
#[test]
|
|
fn shell_zsh_fork_prefers_shell_command_over_unified_exec() {
|
|
let config = test_config();
|
|
let model_info = ModelsManager::construct_model_info_offline_for_tests("o3", &config);
|
|
let mut features = Features::with_defaults();
|
|
features.enable(Feature::UnifiedExec);
|
|
features.enable(Feature::ShellZshFork);
|
|
|
|
let available_models = Vec::new();
|
|
let tools_config = ToolsConfig::new(&ToolsConfigParams {
|
|
model_info: &model_info,
|
|
available_models: &available_models,
|
|
features: &features,
|
|
web_search_mode: Some(WebSearchMode::Live),
|
|
session_source: SessionSource::Cli,
|
|
sandbox_policy: &SandboxPolicy::DangerFullAccess,
|
|
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
|
});
|
|
let user_shell = Shell {
|
|
shell_type: ShellType::Zsh,
|
|
shell_path: PathBuf::from("/bin/zsh"),
|
|
shell_snapshot: crate::shell::empty_shell_snapshot_receiver(),
|
|
};
|
|
|
|
assert_eq!(tools_config.shell_type, ConfigShellToolType::ShellCommand);
|
|
assert_eq!(
|
|
tools_config.shell_command_backend,
|
|
ShellCommandBackendConfig::ZshFork
|
|
);
|
|
assert_eq!(
|
|
tools_config.unified_exec_shell_mode,
|
|
UnifiedExecShellMode::Direct
|
|
);
|
|
assert_eq!(
|
|
tools_config
|
|
.with_unified_exec_shell_mode_for_session(
|
|
&user_shell,
|
|
Some(&PathBuf::from(if cfg!(windows) {
|
|
r"C:\opt\codex\zsh"
|
|
} else {
|
|
"/opt/codex/zsh"
|
|
})),
|
|
Some(&PathBuf::from(if cfg!(windows) {
|
|
r"C:\opt\codex\codex-execve-wrapper"
|
|
} else {
|
|
"/opt/codex/codex-execve-wrapper"
|
|
})),
|
|
)
|
|
.unified_exec_shell_mode,
|
|
if cfg!(unix) {
|
|
UnifiedExecShellMode::ZshFork(ZshForkConfig {
|
|
shell_zsh_path: AbsolutePathBuf::from_absolute_path("/opt/codex/zsh").unwrap(),
|
|
main_execve_wrapper_exe: AbsolutePathBuf::from_absolute_path(
|
|
"/opt/codex/codex-execve-wrapper",
|
|
)
|
|
.unwrap(),
|
|
})
|
|
} else {
|
|
UnifiedExecShellMode::Direct
|
|
}
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
#[ignore]
|
|
fn test_parallel_support_flags() {
|
|
let config = test_config();
|
|
let model_info = ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config);
|
|
let mut features = Features::with_defaults();
|
|
features.enable(Feature::UnifiedExec);
|
|
let available_models = Vec::new();
|
|
let tools_config = ToolsConfig::new(&ToolsConfigParams {
|
|
model_info: &model_info,
|
|
available_models: &available_models,
|
|
features: &features,
|
|
web_search_mode: Some(WebSearchMode::Cached),
|
|
session_source: SessionSource::Cli,
|
|
sandbox_policy: &SandboxPolicy::DangerFullAccess,
|
|
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
|
});
|
|
let (tools, _) = build_specs(&tools_config, None, None, &[]).build();
|
|
|
|
assert!(find_tool(&tools, "exec_command").supports_parallel_tool_calls);
|
|
assert!(!find_tool(&tools, "write_stdin").supports_parallel_tool_calls);
|
|
assert!(find_tool(&tools, "grep_files").supports_parallel_tool_calls);
|
|
assert!(find_tool(&tools, "list_dir").supports_parallel_tool_calls);
|
|
assert!(find_tool(&tools, "read_file").supports_parallel_tool_calls);
|
|
}
|
|
|
|
#[test]
|
|
fn test_test_model_info_includes_sync_tool() {
|
|
let _config = test_config();
|
|
let mut model_info = model_info_from_models_json("gpt-5-codex");
|
|
model_info.experimental_supported_tools = vec![
|
|
"test_sync_tool".to_string(),
|
|
"read_file".to_string(),
|
|
"grep_files".to_string(),
|
|
"list_dir".to_string(),
|
|
];
|
|
let features = Features::with_defaults();
|
|
let available_models = Vec::new();
|
|
let tools_config = ToolsConfig::new(&ToolsConfigParams {
|
|
model_info: &model_info,
|
|
available_models: &available_models,
|
|
features: &features,
|
|
web_search_mode: Some(WebSearchMode::Cached),
|
|
session_source: SessionSource::Cli,
|
|
sandbox_policy: &SandboxPolicy::DangerFullAccess,
|
|
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
|
});
|
|
let (tools, _) = build_specs(&tools_config, None, None, &[]).build();
|
|
|
|
assert!(
|
|
tools
|
|
.iter()
|
|
.any(|tool| tool_name(&tool.spec) == "test_sync_tool")
|
|
);
|
|
assert!(
|
|
tools
|
|
.iter()
|
|
.any(|tool| tool_name(&tool.spec) == "read_file")
|
|
);
|
|
assert!(
|
|
tools
|
|
.iter()
|
|
.any(|tool| tool_name(&tool.spec) == "grep_files")
|
|
);
|
|
assert!(tools.iter().any(|tool| tool_name(&tool.spec) == "list_dir"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_build_specs_mcp_tools_converted() {
|
|
let config = test_config();
|
|
let model_info = ModelsManager::construct_model_info_offline_for_tests("o3", &config);
|
|
let mut features = Features::with_defaults();
|
|
features.enable(Feature::UnifiedExec);
|
|
let available_models = Vec::new();
|
|
let tools_config = ToolsConfig::new(&ToolsConfigParams {
|
|
model_info: &model_info,
|
|
available_models: &available_models,
|
|
features: &features,
|
|
web_search_mode: Some(WebSearchMode::Live),
|
|
session_source: SessionSource::Cli,
|
|
sandbox_policy: &SandboxPolicy::DangerFullAccess,
|
|
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
|
});
|
|
let (tools, _) = build_specs(
|
|
&tools_config,
|
|
Some(HashMap::from([(
|
|
"test_server/do_something_cool".to_string(),
|
|
mcp_tool(
|
|
"do_something_cool",
|
|
"Do something cool",
|
|
serde_json::json!({
|
|
"type": "object",
|
|
"properties": {
|
|
"string_argument": { "type": "string" },
|
|
"number_argument": { "type": "number" },
|
|
"object_argument": {
|
|
"type": "object",
|
|
"properties": {
|
|
"string_property": { "type": "string" },
|
|
"number_property": { "type": "number" },
|
|
},
|
|
"required": ["string_property", "number_property"],
|
|
"additionalProperties": false,
|
|
},
|
|
},
|
|
}),
|
|
),
|
|
)])),
|
|
None,
|
|
&[],
|
|
)
|
|
.build();
|
|
|
|
let tool = find_tool(&tools, "test_server/do_something_cool");
|
|
assert_eq!(
|
|
&tool.spec,
|
|
&ToolSpec::Function(ResponsesApiTool {
|
|
name: "test_server/do_something_cool".to_string(),
|
|
parameters: JsonSchema::Object {
|
|
properties: BTreeMap::from([
|
|
(
|
|
"string_argument".to_string(),
|
|
JsonSchema::String { description: None }
|
|
),
|
|
(
|
|
"number_argument".to_string(),
|
|
JsonSchema::Number { description: None }
|
|
),
|
|
(
|
|
"object_argument".to_string(),
|
|
JsonSchema::Object {
|
|
properties: BTreeMap::from([
|
|
(
|
|
"string_property".to_string(),
|
|
JsonSchema::String { description: None }
|
|
),
|
|
(
|
|
"number_property".to_string(),
|
|
JsonSchema::Number { description: None }
|
|
),
|
|
]),
|
|
required: Some(vec![
|
|
"string_property".to_string(),
|
|
"number_property".to_string(),
|
|
]),
|
|
additional_properties: Some(false.into()),
|
|
},
|
|
),
|
|
]),
|
|
required: None,
|
|
additional_properties: None,
|
|
},
|
|
description: "Do something cool".to_string(),
|
|
strict: false,
|
|
output_schema: Some(mcp_call_tool_result_output_schema(serde_json::json!({}))),
|
|
defer_loading: None,
|
|
})
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_build_specs_mcp_tools_sorted_by_name() {
|
|
let config = test_config();
|
|
let model_info = ModelsManager::construct_model_info_offline_for_tests("o3", &config);
|
|
let mut features = Features::with_defaults();
|
|
features.enable(Feature::UnifiedExec);
|
|
let available_models = Vec::new();
|
|
let tools_config = ToolsConfig::new(&ToolsConfigParams {
|
|
model_info: &model_info,
|
|
available_models: &available_models,
|
|
features: &features,
|
|
web_search_mode: Some(WebSearchMode::Cached),
|
|
session_source: SessionSource::Cli,
|
|
sandbox_policy: &SandboxPolicy::DangerFullAccess,
|
|
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
|
});
|
|
|
|
// Intentionally construct a map with keys that would sort alphabetically.
|
|
let tools_map: HashMap<String, rmcp::model::Tool> = HashMap::from([
|
|
(
|
|
"test_server/do".to_string(),
|
|
mcp_tool("a", "a", serde_json::json!({"type": "object"})),
|
|
),
|
|
(
|
|
"test_server/something".to_string(),
|
|
mcp_tool("b", "b", serde_json::json!({"type": "object"})),
|
|
),
|
|
(
|
|
"test_server/cool".to_string(),
|
|
mcp_tool("c", "c", serde_json::json!({"type": "object"})),
|
|
),
|
|
]);
|
|
|
|
let (tools, _) = build_specs(&tools_config, Some(tools_map), None, &[]).build();
|
|
|
|
// Only assert that the MCP tools themselves are sorted by fully-qualified name.
|
|
let mcp_names: Vec<_> = tools
|
|
.iter()
|
|
.map(|t| tool_name(&t.spec).to_string())
|
|
.filter(|n| n.starts_with("test_server/"))
|
|
.collect();
|
|
let expected = vec![
|
|
"test_server/cool".to_string(),
|
|
"test_server/do".to_string(),
|
|
"test_server/something".to_string(),
|
|
];
|
|
assert_eq!(mcp_names, expected);
|
|
}
|
|
|
|
#[test]
|
|
fn search_tool_description_lists_each_codex_apps_connector_once() {
|
|
let model_info = search_capable_model_info();
|
|
let mut features = Features::with_defaults();
|
|
features.enable(Feature::Apps);
|
|
let available_models = Vec::new();
|
|
let tools_config = ToolsConfig::new(&ToolsConfigParams {
|
|
model_info: &model_info,
|
|
available_models: &available_models,
|
|
features: &features,
|
|
web_search_mode: Some(WebSearchMode::Cached),
|
|
session_source: SessionSource::Cli,
|
|
sandbox_policy: &SandboxPolicy::DangerFullAccess,
|
|
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
|
});
|
|
|
|
let (tools, _) = build_specs(
|
|
&tools_config,
|
|
Some(HashMap::from([
|
|
(
|
|
"mcp__codex_apps__calendar_create_event".to_string(),
|
|
mcp_tool(
|
|
"calendar_create_event",
|
|
"Create calendar event",
|
|
serde_json::json!({"type": "object"}),
|
|
),
|
|
),
|
|
(
|
|
"mcp__rmcp__echo".to_string(),
|
|
mcp_tool("echo", "Echo", serde_json::json!({"type": "object"})),
|
|
),
|
|
])),
|
|
Some(HashMap::from([
|
|
(
|
|
"mcp__codex_apps__calendar_create_event".to_string(),
|
|
ToolInfo {
|
|
server_name: crate::mcp::CODEX_APPS_MCP_SERVER_NAME.to_string(),
|
|
tool_name: "_create_event".to_string(),
|
|
tool_namespace: "mcp__codex_apps__calendar".to_string(),
|
|
tool: mcp_tool(
|
|
"calendar-create-event",
|
|
"Create calendar event",
|
|
serde_json::json!({"type": "object"}),
|
|
),
|
|
connector_id: Some("calendar".to_string()),
|
|
connector_name: Some("Calendar".to_string()),
|
|
plugin_display_names: Vec::new(),
|
|
connector_description: Some(
|
|
"Plan events and manage your calendar.".to_string(),
|
|
),
|
|
},
|
|
),
|
|
(
|
|
"mcp__codex_apps__calendar_list_events".to_string(),
|
|
ToolInfo {
|
|
server_name: crate::mcp::CODEX_APPS_MCP_SERVER_NAME.to_string(),
|
|
tool_name: "_list_events".to_string(),
|
|
tool_namespace: "mcp__codex_apps__calendar".to_string(),
|
|
tool: mcp_tool(
|
|
"calendar-list-events",
|
|
"List calendar events",
|
|
serde_json::json!({"type": "object"}),
|
|
),
|
|
connector_id: Some("calendar".to_string()),
|
|
connector_name: Some("Calendar".to_string()),
|
|
plugin_display_names: Vec::new(),
|
|
connector_description: Some(
|
|
"Plan events and manage your calendar.".to_string(),
|
|
),
|
|
},
|
|
),
|
|
(
|
|
"mcp__codex_apps__gmail_search_threads".to_string(),
|
|
ToolInfo {
|
|
server_name: crate::mcp::CODEX_APPS_MCP_SERVER_NAME.to_string(),
|
|
tool_name: "_search_threads".to_string(),
|
|
tool_namespace: "mcp__codex_apps__gmail".to_string(),
|
|
tool: mcp_tool(
|
|
"gmail-search-threads",
|
|
"Search email threads",
|
|
serde_json::json!({"type": "object"}),
|
|
),
|
|
connector_id: Some("gmail".to_string()),
|
|
connector_name: Some("Gmail".to_string()),
|
|
plugin_display_names: Vec::new(),
|
|
connector_description: Some("Find and summarize email threads.".to_string()),
|
|
},
|
|
),
|
|
(
|
|
"mcp__rmcp__echo".to_string(),
|
|
ToolInfo {
|
|
server_name: "rmcp".to_string(),
|
|
tool_name: "echo".to_string(),
|
|
tool_namespace: "rmcp".to_string(),
|
|
tool: mcp_tool("echo", "Echo", serde_json::json!({"type": "object"})),
|
|
connector_id: None,
|
|
connector_name: None,
|
|
plugin_display_names: Vec::new(),
|
|
connector_description: None,
|
|
},
|
|
),
|
|
])),
|
|
&[],
|
|
)
|
|
.build();
|
|
|
|
let search_tool = find_tool(&tools, TOOL_SEARCH_TOOL_NAME);
|
|
let ToolSpec::ToolSearch { description, .. } = &search_tool.spec else {
|
|
panic!("expected tool_search tool");
|
|
};
|
|
let description = description.as_str();
|
|
assert!(description.contains("- Calendar: Plan events and manage your calendar."));
|
|
assert!(description.contains("- Gmail: Find and summarize email threads."));
|
|
assert_eq!(
|
|
description
|
|
.matches("- Calendar: Plan events and manage your calendar.")
|
|
.count(),
|
|
1
|
|
);
|
|
assert!(!description.contains("mcp__rmcp__echo"));
|
|
}
|
|
|
|
#[test]
|
|
fn search_tool_requires_model_capability_only() {
|
|
let model_info = search_capable_model_info();
|
|
let app_tools = Some(HashMap::from([(
|
|
"mcp__codex_apps__calendar_create_event".to_string(),
|
|
ToolInfo {
|
|
server_name: crate::mcp::CODEX_APPS_MCP_SERVER_NAME.to_string(),
|
|
tool_name: "calendar_create_event".to_string(),
|
|
tool_namespace: "mcp__codex_apps__calendar".to_string(),
|
|
tool: mcp_tool(
|
|
"calendar_create_event",
|
|
"Create calendar event",
|
|
serde_json::json!({"type": "object"}),
|
|
),
|
|
connector_id: Some("calendar".to_string()),
|
|
connector_name: Some("Calendar".to_string()),
|
|
connector_description: None,
|
|
plugin_display_names: Vec::new(),
|
|
},
|
|
)]));
|
|
|
|
let features = Features::with_defaults();
|
|
let available_models = Vec::new();
|
|
let tools_config = ToolsConfig::new(&ToolsConfigParams {
|
|
model_info: &ModelInfo {
|
|
supports_search_tool: false,
|
|
..model_info.clone()
|
|
},
|
|
available_models: &available_models,
|
|
features: &features,
|
|
web_search_mode: Some(WebSearchMode::Cached),
|
|
session_source: SessionSource::Cli,
|
|
sandbox_policy: &SandboxPolicy::DangerFullAccess,
|
|
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
|
});
|
|
let (tools, _) = build_specs(&tools_config, None, app_tools.clone(), &[]).build();
|
|
assert_lacks_tool_name(&tools, TOOL_SEARCH_TOOL_NAME);
|
|
let available_models = Vec::new();
|
|
let tools_config = ToolsConfig::new(&ToolsConfigParams {
|
|
model_info: &model_info,
|
|
available_models: &available_models,
|
|
features: &features,
|
|
web_search_mode: Some(WebSearchMode::Cached),
|
|
session_source: SessionSource::Cli,
|
|
sandbox_policy: &SandboxPolicy::DangerFullAccess,
|
|
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
|
});
|
|
let (tools, _) = build_specs(&tools_config, None, app_tools, &[]).build();
|
|
assert_contains_tool_names(&tools, &[TOOL_SEARCH_TOOL_NAME]);
|
|
}
|
|
|
|
#[test]
|
|
fn tool_suggest_is_not_registered_without_feature_flag() {
|
|
let model_info = search_capable_model_info();
|
|
let mut features = Features::with_defaults();
|
|
features.enable(Feature::Apps);
|
|
let available_models = Vec::new();
|
|
let tools_config = ToolsConfig::new(&ToolsConfigParams {
|
|
model_info: &model_info,
|
|
available_models: &available_models,
|
|
features: &features,
|
|
web_search_mode: Some(WebSearchMode::Cached),
|
|
session_source: SessionSource::Cli,
|
|
sandbox_policy: &SandboxPolicy::DangerFullAccess,
|
|
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
|
});
|
|
let (tools, _) = build_specs_with_discoverable_tools(
|
|
&tools_config,
|
|
None,
|
|
None,
|
|
Some(vec![discoverable_connector(
|
|
"connector_2128aebfecb84f64a069897515042a44",
|
|
"Google Calendar",
|
|
"Plan events and schedules.",
|
|
)]),
|
|
&[],
|
|
)
|
|
.build();
|
|
|
|
assert!(
|
|
!tools
|
|
.iter()
|
|
.any(|tool| tool_name(&tool.spec) == TOOL_SUGGEST_TOOL_NAME)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn search_tool_description_handles_no_enabled_apps() {
|
|
let model_info = search_capable_model_info();
|
|
let mut features = Features::with_defaults();
|
|
features.enable(Feature::Apps);
|
|
let available_models = Vec::new();
|
|
let tools_config = ToolsConfig::new(&ToolsConfigParams {
|
|
model_info: &model_info,
|
|
available_models: &available_models,
|
|
features: &features,
|
|
web_search_mode: Some(WebSearchMode::Cached),
|
|
session_source: SessionSource::Cli,
|
|
sandbox_policy: &SandboxPolicy::DangerFullAccess,
|
|
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
|
});
|
|
|
|
let (tools, _) = build_specs(&tools_config, None, Some(HashMap::new()), &[]).build();
|
|
let search_tool = find_tool(&tools, TOOL_SEARCH_TOOL_NAME);
|
|
let ToolSpec::ToolSearch { description, .. } = &search_tool.spec else {
|
|
panic!("expected tool_search tool");
|
|
};
|
|
|
|
assert!(description.contains("None currently enabled."));
|
|
assert!(!description.contains("{{app_descriptions}}"));
|
|
}
|
|
|
|
#[test]
|
|
fn search_tool_description_falls_back_to_connector_name_without_description() {
|
|
let model_info = search_capable_model_info();
|
|
let mut features = Features::with_defaults();
|
|
features.enable(Feature::Apps);
|
|
let available_models = Vec::new();
|
|
let tools_config = ToolsConfig::new(&ToolsConfigParams {
|
|
model_info: &model_info,
|
|
available_models: &available_models,
|
|
features: &features,
|
|
web_search_mode: Some(WebSearchMode::Cached),
|
|
session_source: SessionSource::Cli,
|
|
sandbox_policy: &SandboxPolicy::DangerFullAccess,
|
|
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
|
});
|
|
|
|
let (tools, _) = build_specs(
|
|
&tools_config,
|
|
None,
|
|
Some(HashMap::from([(
|
|
"mcp__codex_apps__calendar_create_event".to_string(),
|
|
ToolInfo {
|
|
server_name: crate::mcp::CODEX_APPS_MCP_SERVER_NAME.to_string(),
|
|
tool_name: "_create_event".to_string(),
|
|
tool_namespace: "mcp__codex_apps__calendar".to_string(),
|
|
tool: mcp_tool(
|
|
"calendar_create_event",
|
|
"Create calendar event",
|
|
serde_json::json!({"type": "object"}),
|
|
),
|
|
connector_id: Some("calendar".to_string()),
|
|
connector_name: Some("Calendar".to_string()),
|
|
plugin_display_names: Vec::new(),
|
|
connector_description: None,
|
|
},
|
|
)])),
|
|
&[],
|
|
)
|
|
.build();
|
|
let search_tool = find_tool(&tools, TOOL_SEARCH_TOOL_NAME);
|
|
let ToolSpec::ToolSearch { description, .. } = &search_tool.spec else {
|
|
panic!("expected tool_search tool");
|
|
};
|
|
|
|
assert!(description.contains("- Calendar"));
|
|
assert!(!description.contains("- Calendar:"));
|
|
}
|
|
|
|
#[test]
|
|
fn search_tool_registers_namespaced_app_tool_aliases() {
|
|
let model_info = search_capable_model_info();
|
|
let mut features = Features::with_defaults();
|
|
features.enable(Feature::Apps);
|
|
let available_models = Vec::new();
|
|
let tools_config = ToolsConfig::new(&ToolsConfigParams {
|
|
model_info: &model_info,
|
|
available_models: &available_models,
|
|
features: &features,
|
|
web_search_mode: Some(WebSearchMode::Cached),
|
|
session_source: SessionSource::Cli,
|
|
sandbox_policy: &SandboxPolicy::DangerFullAccess,
|
|
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
|
});
|
|
|
|
let (_, registry) = build_specs(
|
|
&tools_config,
|
|
None,
|
|
Some(HashMap::from([
|
|
(
|
|
"mcp__codex_apps__calendar_create_event".to_string(),
|
|
ToolInfo {
|
|
server_name: crate::mcp::CODEX_APPS_MCP_SERVER_NAME.to_string(),
|
|
tool_name: "_create_event".to_string(),
|
|
tool_namespace: "mcp__codex_apps__calendar".to_string(),
|
|
tool: mcp_tool(
|
|
"calendar-create-event",
|
|
"Create calendar event",
|
|
serde_json::json!({"type": "object"}),
|
|
),
|
|
connector_id: Some("calendar".to_string()),
|
|
connector_name: Some("Calendar".to_string()),
|
|
connector_description: None,
|
|
plugin_display_names: Vec::new(),
|
|
},
|
|
),
|
|
(
|
|
"mcp__codex_apps__calendar_list_events".to_string(),
|
|
ToolInfo {
|
|
server_name: crate::mcp::CODEX_APPS_MCP_SERVER_NAME.to_string(),
|
|
tool_name: "_list_events".to_string(),
|
|
tool_namespace: "mcp__codex_apps__calendar".to_string(),
|
|
tool: mcp_tool(
|
|
"calendar-list-events",
|
|
"List calendar events",
|
|
serde_json::json!({"type": "object"}),
|
|
),
|
|
connector_id: Some("calendar".to_string()),
|
|
connector_name: Some("Calendar".to_string()),
|
|
connector_description: None,
|
|
plugin_display_names: Vec::new(),
|
|
},
|
|
),
|
|
])),
|
|
&[],
|
|
)
|
|
.build();
|
|
|
|
let alias = tool_handler_key("_create_event", Some("mcp__codex_apps__calendar"));
|
|
|
|
assert!(registry.has_handler(TOOL_SEARCH_TOOL_NAME, None));
|
|
assert!(registry.has_handler(alias.as_str(), None));
|
|
}
|
|
|
|
#[test]
|
|
fn tool_suggest_description_lists_discoverable_tools() {
|
|
let model_info = search_capable_model_info();
|
|
let mut features = Features::with_defaults();
|
|
features.enable(Feature::Apps);
|
|
features.enable(Feature::ToolSuggest);
|
|
let available_models = Vec::new();
|
|
let tools_config = ToolsConfig::new(&ToolsConfigParams {
|
|
model_info: &model_info,
|
|
available_models: &available_models,
|
|
features: &features,
|
|
web_search_mode: Some(WebSearchMode::Cached),
|
|
session_source: SessionSource::Cli,
|
|
sandbox_policy: &SandboxPolicy::DangerFullAccess,
|
|
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
|
});
|
|
|
|
let discoverable_tools = vec![
|
|
discoverable_connector(
|
|
"connector_2128aebfecb84f64a069897515042a44",
|
|
"Google Calendar",
|
|
"Plan events and schedules.",
|
|
),
|
|
discoverable_connector(
|
|
"connector_68df038e0ba48191908c8434991bbac2",
|
|
"Gmail",
|
|
"Find and summarize email threads.",
|
|
),
|
|
DiscoverableTool::Plugin(Box::new(DiscoverablePluginInfo {
|
|
id: "sample@test".to_string(),
|
|
name: "Sample Plugin".to_string(),
|
|
description: None,
|
|
has_skills: true,
|
|
mcp_server_names: vec!["sample-docs".to_string()],
|
|
app_connector_ids: vec!["connector_sample".to_string()],
|
|
})),
|
|
];
|
|
|
|
let (tools, _) = build_specs_with_discoverable_tools(
|
|
&tools_config,
|
|
None,
|
|
None,
|
|
Some(discoverable_tools),
|
|
&[],
|
|
)
|
|
.build();
|
|
|
|
let tool_suggest = find_tool(&tools, TOOL_SUGGEST_TOOL_NAME);
|
|
let ToolSpec::Function(ResponsesApiTool {
|
|
description,
|
|
parameters,
|
|
..
|
|
}) = &tool_suggest.spec
|
|
else {
|
|
panic!("expected function tool");
|
|
};
|
|
assert!(description.contains("Google Calendar"));
|
|
assert!(description.contains("Gmail"));
|
|
assert!(description.contains("Sample Plugin"));
|
|
assert!(description.contains("Plan events and schedules."));
|
|
assert!(description.contains("Find and summarize email threads."));
|
|
assert!(description.contains("id: `sample@test`, type: plugin, action: enable"));
|
|
assert!(
|
|
description.contains("skills; MCP servers: sample-docs; app connectors: connector_sample")
|
|
);
|
|
assert!(description.contains("DO NOT explore or recommend tools that are not on this list."));
|
|
let JsonSchema::Object { required, .. } = parameters else {
|
|
panic!("expected object parameters");
|
|
};
|
|
assert_eq!(
|
|
required.as_ref(),
|
|
Some(&vec![
|
|
"tool_type".to_string(),
|
|
"action_type".to_string(),
|
|
"tool_id".to_string(),
|
|
"suggest_reason".to_string(),
|
|
])
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_mcp_tool_property_missing_type_defaults_to_string() {
|
|
let config = test_config();
|
|
let model_info = ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config);
|
|
let mut features = Features::with_defaults();
|
|
features.enable(Feature::UnifiedExec);
|
|
let available_models = Vec::new();
|
|
let tools_config = ToolsConfig::new(&ToolsConfigParams {
|
|
model_info: &model_info,
|
|
available_models: &available_models,
|
|
features: &features,
|
|
web_search_mode: Some(WebSearchMode::Cached),
|
|
session_source: SessionSource::Cli,
|
|
sandbox_policy: &SandboxPolicy::DangerFullAccess,
|
|
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
|
});
|
|
|
|
let (tools, _) = build_specs(
|
|
&tools_config,
|
|
Some(HashMap::from([(
|
|
"dash/search".to_string(),
|
|
mcp_tool(
|
|
"search",
|
|
"Search docs",
|
|
serde_json::json!({
|
|
"type": "object",
|
|
"properties": {
|
|
"query": {"description": "search query"}
|
|
}
|
|
}),
|
|
),
|
|
)])),
|
|
None,
|
|
&[],
|
|
)
|
|
.build();
|
|
|
|
let tool = find_tool(&tools, "dash/search");
|
|
assert_eq!(
|
|
tool.spec,
|
|
ToolSpec::Function(ResponsesApiTool {
|
|
name: "dash/search".to_string(),
|
|
parameters: JsonSchema::Object {
|
|
properties: BTreeMap::from([(
|
|
"query".to_string(),
|
|
JsonSchema::String {
|
|
description: Some("search query".to_string())
|
|
}
|
|
)]),
|
|
required: None,
|
|
additional_properties: None,
|
|
},
|
|
description: "Search docs".to_string(),
|
|
strict: false,
|
|
output_schema: Some(mcp_call_tool_result_output_schema(serde_json::json!({}))),
|
|
defer_loading: None,
|
|
})
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_mcp_tool_integer_normalized_to_number() {
|
|
let config = test_config();
|
|
let model_info = ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config);
|
|
let mut features = Features::with_defaults();
|
|
features.enable(Feature::UnifiedExec);
|
|
let available_models = Vec::new();
|
|
let tools_config = ToolsConfig::new(&ToolsConfigParams {
|
|
model_info: &model_info,
|
|
available_models: &available_models,
|
|
features: &features,
|
|
web_search_mode: Some(WebSearchMode::Cached),
|
|
session_source: SessionSource::Cli,
|
|
sandbox_policy: &SandboxPolicy::DangerFullAccess,
|
|
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
|
});
|
|
|
|
let (tools, _) = build_specs(
|
|
&tools_config,
|
|
Some(HashMap::from([(
|
|
"dash/paginate".to_string(),
|
|
mcp_tool(
|
|
"paginate",
|
|
"Pagination",
|
|
serde_json::json!({
|
|
"type": "object",
|
|
"properties": {"page": {"type": "integer"}}
|
|
}),
|
|
),
|
|
)])),
|
|
None,
|
|
&[],
|
|
)
|
|
.build();
|
|
|
|
let tool = find_tool(&tools, "dash/paginate");
|
|
assert_eq!(
|
|
tool.spec,
|
|
ToolSpec::Function(ResponsesApiTool {
|
|
name: "dash/paginate".to_string(),
|
|
parameters: JsonSchema::Object {
|
|
properties: BTreeMap::from([(
|
|
"page".to_string(),
|
|
JsonSchema::Number { description: None }
|
|
)]),
|
|
required: None,
|
|
additional_properties: None,
|
|
},
|
|
description: "Pagination".to_string(),
|
|
strict: false,
|
|
output_schema: Some(mcp_call_tool_result_output_schema(serde_json::json!({}))),
|
|
defer_loading: None,
|
|
})
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_mcp_tool_array_without_items_gets_default_string_items() {
|
|
let config = test_config();
|
|
let model_info = ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config);
|
|
let mut features = Features::with_defaults();
|
|
features.enable(Feature::UnifiedExec);
|
|
features.enable(Feature::ApplyPatchFreeform);
|
|
let available_models = Vec::new();
|
|
let tools_config = ToolsConfig::new(&ToolsConfigParams {
|
|
model_info: &model_info,
|
|
available_models: &available_models,
|
|
features: &features,
|
|
web_search_mode: Some(WebSearchMode::Cached),
|
|
session_source: SessionSource::Cli,
|
|
sandbox_policy: &SandboxPolicy::DangerFullAccess,
|
|
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
|
});
|
|
|
|
let (tools, _) = build_specs(
|
|
&tools_config,
|
|
Some(HashMap::from([(
|
|
"dash/tags".to_string(),
|
|
mcp_tool(
|
|
"tags",
|
|
"Tags",
|
|
serde_json::json!({
|
|
"type": "object",
|
|
"properties": {"tags": {"type": "array"}}
|
|
}),
|
|
),
|
|
)])),
|
|
None,
|
|
&[],
|
|
)
|
|
.build();
|
|
|
|
let tool = find_tool(&tools, "dash/tags");
|
|
assert_eq!(
|
|
tool.spec,
|
|
ToolSpec::Function(ResponsesApiTool {
|
|
name: "dash/tags".to_string(),
|
|
parameters: JsonSchema::Object {
|
|
properties: BTreeMap::from([(
|
|
"tags".to_string(),
|
|
JsonSchema::Array {
|
|
items: Box::new(JsonSchema::String { description: None }),
|
|
description: None
|
|
}
|
|
)]),
|
|
required: None,
|
|
additional_properties: None,
|
|
},
|
|
description: "Tags".to_string(),
|
|
strict: false,
|
|
output_schema: Some(mcp_call_tool_result_output_schema(serde_json::json!({}))),
|
|
defer_loading: None,
|
|
})
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_mcp_tool_anyof_defaults_to_string() {
|
|
let config = test_config();
|
|
let model_info = ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config);
|
|
let mut features = Features::with_defaults();
|
|
features.enable(Feature::UnifiedExec);
|
|
let available_models = Vec::new();
|
|
let tools_config = ToolsConfig::new(&ToolsConfigParams {
|
|
model_info: &model_info,
|
|
available_models: &available_models,
|
|
features: &features,
|
|
web_search_mode: Some(WebSearchMode::Cached),
|
|
session_source: SessionSource::Cli,
|
|
sandbox_policy: &SandboxPolicy::DangerFullAccess,
|
|
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
|
});
|
|
|
|
let (tools, _) = build_specs(
|
|
&tools_config,
|
|
Some(HashMap::from([(
|
|
"dash/value".to_string(),
|
|
mcp_tool(
|
|
"value",
|
|
"AnyOf Value",
|
|
serde_json::json!({
|
|
"type": "object",
|
|
"properties": {
|
|
"value": {"anyOf": [{"type": "string"}, {"type": "number"}]}
|
|
}
|
|
}),
|
|
),
|
|
)])),
|
|
None,
|
|
&[],
|
|
)
|
|
.build();
|
|
|
|
let tool = find_tool(&tools, "dash/value");
|
|
assert_eq!(
|
|
tool.spec,
|
|
ToolSpec::Function(ResponsesApiTool {
|
|
name: "dash/value".to_string(),
|
|
parameters: JsonSchema::Object {
|
|
properties: BTreeMap::from([(
|
|
"value".to_string(),
|
|
JsonSchema::String { description: None }
|
|
)]),
|
|
required: None,
|
|
additional_properties: None,
|
|
},
|
|
description: "AnyOf Value".to_string(),
|
|
strict: false,
|
|
output_schema: Some(mcp_call_tool_result_output_schema(serde_json::json!({}))),
|
|
defer_loading: None,
|
|
})
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_shell_tool() {
|
|
let tool = super::create_shell_tool(false);
|
|
let ToolSpec::Function(ResponsesApiTool {
|
|
description, name, ..
|
|
}) = &tool
|
|
else {
|
|
panic!("expected function tool");
|
|
};
|
|
assert_eq!(name, "shell");
|
|
|
|
let expected = if cfg!(windows) {
|
|
r#"Runs a Powershell command (Windows) and returns its output. Arguments to `shell` will be passed to CreateProcessW(). Most commands should be prefixed with ["powershell.exe", "-Command"].
|
|
|
|
Examples of valid command strings:
|
|
|
|
- ls -a (show hidden): ["powershell.exe", "-Command", "Get-ChildItem -Force"]
|
|
- recursive find by name: ["powershell.exe", "-Command", "Get-ChildItem -Recurse -Filter *.py"]
|
|
- recursive grep: ["powershell.exe", "-Command", "Get-ChildItem -Path C:\\myrepo -Recurse | Select-String -Pattern 'TODO' -CaseSensitive"]
|
|
- ps aux | grep python: ["powershell.exe", "-Command", "Get-Process | Where-Object { $_.ProcessName -like '*python*' }"]
|
|
- setting an env var: ["powershell.exe", "-Command", "$env:FOO='bar'; echo $env:FOO"]
|
|
- running an inline Python script: ["powershell.exe", "-Command", "@'\\nprint('Hello, world!')\\n'@ | python -"]"#
|
|
} else {
|
|
r#"Runs a shell command and returns its output.
|
|
- The arguments to `shell` will be passed to execvp(). Most terminal commands should be prefixed with ["bash", "-lc"].
|
|
- Always set the `workdir` param when using the shell function. Do not use `cd` unless absolutely necessary."#
|
|
}.to_string();
|
|
assert_eq!(description, &expected);
|
|
}
|
|
|
|
#[test]
|
|
fn shell_tool_with_request_permission_includes_additional_permissions() {
|
|
let tool = super::create_shell_tool(true);
|
|
let ToolSpec::Function(ResponsesApiTool { parameters, .. }) = tool else {
|
|
panic!("expected function tool");
|
|
};
|
|
let JsonSchema::Object { properties, .. } = parameters else {
|
|
panic!("expected object parameters");
|
|
};
|
|
|
|
assert!(properties.contains_key("additional_permissions"));
|
|
|
|
let Some(JsonSchema::String {
|
|
description: Some(description),
|
|
}) = properties.get("sandbox_permissions")
|
|
else {
|
|
panic!("expected sandbox_permissions description");
|
|
};
|
|
assert!(description.contains("with_additional_permissions"));
|
|
assert!(description.contains("filesystem or network permissions"));
|
|
|
|
let Some(JsonSchema::Object {
|
|
properties: additional_properties,
|
|
..
|
|
}) = properties.get("additional_permissions")
|
|
else {
|
|
panic!("expected additional_permissions schema");
|
|
};
|
|
assert!(additional_properties.contains_key("network"));
|
|
assert!(additional_properties.contains_key("file_system"));
|
|
assert!(!additional_properties.contains_key("macos"));
|
|
}
|
|
|
|
#[test]
|
|
fn request_permissions_tool_includes_full_permission_schema() {
|
|
let tool = super::create_request_permissions_tool();
|
|
let ToolSpec::Function(ResponsesApiTool { parameters, .. }) = tool else {
|
|
panic!("expected function tool");
|
|
};
|
|
let JsonSchema::Object { properties, .. } = parameters else {
|
|
panic!("expected object parameters");
|
|
};
|
|
let Some(JsonSchema::Object {
|
|
properties: permission_properties,
|
|
additional_properties,
|
|
..
|
|
}) = properties.get("permissions")
|
|
else {
|
|
panic!("expected permissions object");
|
|
};
|
|
|
|
assert_eq!(additional_properties, &Some(false.into()));
|
|
assert!(permission_properties.contains_key("network"));
|
|
assert!(permission_properties.contains_key("file_system"));
|
|
assert!(!permission_properties.contains_key("macos"));
|
|
|
|
let Some(JsonSchema::Object {
|
|
properties: network_properties,
|
|
additional_properties,
|
|
..
|
|
}) = permission_properties.get("network")
|
|
else {
|
|
panic!("expected network object");
|
|
};
|
|
assert_eq!(additional_properties, &Some(false.into()));
|
|
assert!(network_properties.contains_key("enabled"));
|
|
|
|
let Some(JsonSchema::Object {
|
|
properties: file_system_properties,
|
|
additional_properties,
|
|
..
|
|
}) = permission_properties.get("file_system")
|
|
else {
|
|
panic!("expected file_system object");
|
|
};
|
|
assert_eq!(additional_properties, &Some(false.into()));
|
|
assert!(file_system_properties.contains_key("read"));
|
|
assert!(file_system_properties.contains_key("write"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_shell_command_tool() {
|
|
let tool = super::create_shell_command_tool(true, false);
|
|
let ToolSpec::Function(ResponsesApiTool {
|
|
description, name, ..
|
|
}) = &tool
|
|
else {
|
|
panic!("expected function tool");
|
|
};
|
|
assert_eq!(name, "shell_command");
|
|
|
|
let expected = if cfg!(windows) {
|
|
r#"Runs a Powershell command (Windows) and returns its output.
|
|
|
|
Examples of valid command strings:
|
|
|
|
- ls -a (show hidden): "Get-ChildItem -Force"
|
|
- recursive find by name: "Get-ChildItem -Recurse -Filter *.py"
|
|
- recursive grep: "Get-ChildItem -Path C:\\myrepo -Recurse | Select-String -Pattern 'TODO' -CaseSensitive"
|
|
- ps aux | grep python: "Get-Process | Where-Object { $_.ProcessName -like '*python*' }"
|
|
- setting an env var: "$env:FOO='bar'; echo $env:FOO"
|
|
- running an inline Python script: "@'\\nprint('Hello, world!')\\n'@ | python -"#.to_string()
|
|
} else {
|
|
r#"Runs a shell command and returns its output.
|
|
- Always set the `workdir` param when using the shell_command function. Do not use `cd` unless absolutely necessary."#.to_string()
|
|
};
|
|
assert_eq!(description, &expected);
|
|
}
|
|
|
|
#[test]
|
|
fn test_get_openai_tools_mcp_tools_with_additional_properties_schema() {
|
|
let config = test_config();
|
|
let model_info = ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config);
|
|
let mut features = Features::with_defaults();
|
|
features.enable(Feature::UnifiedExec);
|
|
let available_models = Vec::new();
|
|
let tools_config = ToolsConfig::new(&ToolsConfigParams {
|
|
model_info: &model_info,
|
|
available_models: &available_models,
|
|
features: &features,
|
|
web_search_mode: Some(WebSearchMode::Cached),
|
|
session_source: SessionSource::Cli,
|
|
sandbox_policy: &SandboxPolicy::DangerFullAccess,
|
|
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
|
});
|
|
let (tools, _) = build_specs(
|
|
&tools_config,
|
|
Some(HashMap::from([(
|
|
"test_server/do_something_cool".to_string(),
|
|
mcp_tool(
|
|
"do_something_cool",
|
|
"Do something cool",
|
|
serde_json::json!({
|
|
"type": "object",
|
|
"properties": {
|
|
"string_argument": {"type": "string"},
|
|
"number_argument": {"type": "number"},
|
|
"object_argument": {
|
|
"type": "object",
|
|
"properties": {
|
|
"string_property": {"type": "string"},
|
|
"number_property": {"type": "number"}
|
|
},
|
|
"required": ["string_property", "number_property"],
|
|
"additionalProperties": {
|
|
"type": "object",
|
|
"properties": {
|
|
"addtl_prop": {"type": "string"}
|
|
},
|
|
"required": ["addtl_prop"],
|
|
"additionalProperties": false
|
|
}
|
|
}
|
|
}
|
|
}),
|
|
),
|
|
)])),
|
|
None,
|
|
&[],
|
|
)
|
|
.build();
|
|
|
|
let tool = find_tool(&tools, "test_server/do_something_cool");
|
|
assert_eq!(
|
|
tool.spec,
|
|
ToolSpec::Function(ResponsesApiTool {
|
|
name: "test_server/do_something_cool".to_string(),
|
|
parameters: JsonSchema::Object {
|
|
properties: BTreeMap::from([
|
|
(
|
|
"string_argument".to_string(),
|
|
JsonSchema::String { description: None }
|
|
),
|
|
(
|
|
"number_argument".to_string(),
|
|
JsonSchema::Number { description: None }
|
|
),
|
|
(
|
|
"object_argument".to_string(),
|
|
JsonSchema::Object {
|
|
properties: BTreeMap::from([
|
|
(
|
|
"string_property".to_string(),
|
|
JsonSchema::String { description: None }
|
|
),
|
|
(
|
|
"number_property".to_string(),
|
|
JsonSchema::Number { description: None }
|
|
),
|
|
]),
|
|
required: Some(vec![
|
|
"string_property".to_string(),
|
|
"number_property".to_string(),
|
|
]),
|
|
additional_properties: Some(
|
|
JsonSchema::Object {
|
|
properties: BTreeMap::from([(
|
|
"addtl_prop".to_string(),
|
|
JsonSchema::String { description: None }
|
|
),]),
|
|
required: Some(vec!["addtl_prop".to_string(),]),
|
|
additional_properties: Some(false.into()),
|
|
}
|
|
.into()
|
|
),
|
|
},
|
|
),
|
|
]),
|
|
required: None,
|
|
additional_properties: None,
|
|
},
|
|
description: "Do something cool".to_string(),
|
|
strict: false,
|
|
output_schema: Some(mcp_call_tool_result_output_schema(serde_json::json!({}))),
|
|
defer_loading: None,
|
|
})
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn code_mode_augments_builtin_tool_descriptions_with_typed_sample() {
|
|
let config = test_config();
|
|
let model_info = ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config);
|
|
let mut features = Features::with_defaults();
|
|
features.enable(Feature::CodeMode);
|
|
features.enable(Feature::UnifiedExec);
|
|
let available_models = Vec::new();
|
|
let tools_config = ToolsConfig::new(&ToolsConfigParams {
|
|
model_info: &model_info,
|
|
available_models: &available_models,
|
|
features: &features,
|
|
web_search_mode: Some(WebSearchMode::Cached),
|
|
session_source: SessionSource::Cli,
|
|
sandbox_policy: &SandboxPolicy::DangerFullAccess,
|
|
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
|
});
|
|
|
|
let (tools, _) = build_specs(&tools_config, None, None, &[]).build();
|
|
let ToolSpec::Function(ResponsesApiTool { description, .. }) =
|
|
&find_tool(&tools, "view_image").spec
|
|
else {
|
|
panic!("expected function tool");
|
|
};
|
|
|
|
assert_eq!(
|
|
description,
|
|
"View a local image from the filesystem (only use if given a full filepath by the user, and the image isn't already attached to the thread context within <image ...> tags).\n\nexec tool declaration:\n```ts\ndeclare const tools: { view_image(args: { path: string; }): Promise<unknown>; };\n```"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn code_mode_augments_mcp_tool_descriptions_with_namespaced_sample() {
|
|
let config = test_config();
|
|
let model_info = ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config);
|
|
let mut features = Features::with_defaults();
|
|
features.enable(Feature::CodeMode);
|
|
features.enable(Feature::UnifiedExec);
|
|
let available_models = Vec::new();
|
|
let tools_config = ToolsConfig::new(&ToolsConfigParams {
|
|
model_info: &model_info,
|
|
available_models: &available_models,
|
|
features: &features,
|
|
web_search_mode: Some(WebSearchMode::Cached),
|
|
session_source: SessionSource::Cli,
|
|
sandbox_policy: &SandboxPolicy::DangerFullAccess,
|
|
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
|
});
|
|
|
|
let (tools, _) = build_specs(
|
|
&tools_config,
|
|
Some(HashMap::from([(
|
|
"mcp__sample__echo".to_string(),
|
|
mcp_tool(
|
|
"echo",
|
|
"Echo text",
|
|
serde_json::json!({
|
|
"type": "object",
|
|
"properties": {
|
|
"message": {"type": "string"}
|
|
},
|
|
"required": ["message"],
|
|
"additionalProperties": false
|
|
}),
|
|
),
|
|
)])),
|
|
None,
|
|
&[],
|
|
)
|
|
.build();
|
|
|
|
let ToolSpec::Function(ResponsesApiTool { description, .. }) =
|
|
&find_tool(&tools, "mcp__sample__echo").spec
|
|
else {
|
|
panic!("expected function tool");
|
|
};
|
|
|
|
assert_eq!(
|
|
description,
|
|
"Echo text\n\nexec tool declaration:\n```ts\ndeclare const tools: { mcp__sample__echo(args: { message: string; }): Promise<{ _meta?: unknown; content: Array<unknown>; isError?: boolean; structuredContent?: unknown; }>; };\n```"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn code_mode_only_restricts_model_tools_to_exec_tools() {
|
|
let mut features = Features::with_defaults();
|
|
features.enable(Feature::CodeMode);
|
|
features.enable(Feature::CodeModeOnly);
|
|
|
|
assert_model_tools(
|
|
"gpt-5.1-codex",
|
|
&features,
|
|
Some(WebSearchMode::Live),
|
|
&["exec", "exec_wait"],
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn code_mode_only_exec_description_includes_full_nested_tool_details() {
|
|
let config = test_config();
|
|
let model_info = ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config);
|
|
let mut features = Features::with_defaults();
|
|
features.enable(Feature::CodeMode);
|
|
features.enable(Feature::CodeModeOnly);
|
|
let available_models = Vec::new();
|
|
let tools_config = ToolsConfig::new(&ToolsConfigParams {
|
|
model_info: &model_info,
|
|
available_models: &available_models,
|
|
features: &features,
|
|
web_search_mode: Some(WebSearchMode::Cached),
|
|
session_source: SessionSource::Cli,
|
|
sandbox_policy: &SandboxPolicy::DangerFullAccess,
|
|
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
|
});
|
|
|
|
let (tools, _) = build_specs(&tools_config, None, None, &[]).build();
|
|
let ToolSpec::Freeform(FreeformTool { description, .. }) = &find_tool(&tools, "exec").spec
|
|
else {
|
|
panic!("expected freeform tool");
|
|
};
|
|
|
|
assert!(!description.contains("Enabled nested tools:"));
|
|
assert!(!description.contains("Nested tool reference:"));
|
|
assert!(description.starts_with(
|
|
"Use `exec/exec_wait` tool to run all other tools, do not attempt to use any other tools directly"
|
|
));
|
|
assert!(description.contains("### `update_plan` (`update_plan`)"));
|
|
assert!(description.contains("### `view_image` (`view_image`)"));
|
|
}
|
|
|
|
#[test]
|
|
fn code_mode_exec_description_omits_nested_tool_details_when_not_code_mode_only() {
|
|
let config = test_config();
|
|
let model_info = ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config);
|
|
let mut features = Features::with_defaults();
|
|
features.enable(Feature::CodeMode);
|
|
let available_models = Vec::new();
|
|
let tools_config = ToolsConfig::new(&ToolsConfigParams {
|
|
model_info: &model_info,
|
|
available_models: &available_models,
|
|
features: &features,
|
|
web_search_mode: Some(WebSearchMode::Cached),
|
|
session_source: SessionSource::Cli,
|
|
sandbox_policy: &SandboxPolicy::DangerFullAccess,
|
|
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
|
});
|
|
|
|
let (tools, _) = build_specs(&tools_config, None, None, &[]).build();
|
|
let ToolSpec::Freeform(FreeformTool { description, .. }) = &find_tool(&tools, "exec").spec
|
|
else {
|
|
panic!("expected freeform tool");
|
|
};
|
|
|
|
assert!(!description.starts_with(
|
|
"Use `exec/exec_wait` tool to run all other tools, do not attempt to use any other tools directly"
|
|
));
|
|
assert!(!description.contains("### `update_plan` (`update_plan`)"));
|
|
assert!(!description.contains("### `view_image` (`view_image`)"));
|
|
}
|
|
|
|
#[test]
|
|
fn chat_tools_include_top_level_name() {
|
|
let properties =
|
|
BTreeMap::from([("foo".to_string(), JsonSchema::String { description: None })]);
|
|
let tools = vec![ToolSpec::Function(ResponsesApiTool {
|
|
name: "demo".to_string(),
|
|
description: "A demo tool".to_string(),
|
|
strict: false,
|
|
defer_loading: None,
|
|
parameters: JsonSchema::Object {
|
|
properties,
|
|
required: None,
|
|
additional_properties: None,
|
|
},
|
|
output_schema: None,
|
|
})];
|
|
|
|
let responses_json = create_tools_json_for_responses_api(&tools).unwrap();
|
|
assert_eq!(
|
|
responses_json,
|
|
vec![json!({
|
|
"type": "function",
|
|
"name": "demo",
|
|
"description": "A demo tool",
|
|
"strict": false,
|
|
"parameters": {
|
|
"type": "object",
|
|
"properties": {
|
|
"foo": { "type": "string" }
|
|
},
|
|
},
|
|
})]
|
|
);
|
|
}
|