Compare commits

...

2 Commits

Author SHA1 Message Date
celia-oai
253e787179 test 2026-05-12 15:50:19 -07:00
celia-oai
9d3746f0c5 changes 2026-05-12 14:55:58 -07:00
4 changed files with 91 additions and 9 deletions

View File

@@ -19,6 +19,8 @@ use codex_protocol::protocol::SessionSource;
use codex_tools::AdditionalProperties;
use codex_tools::DiscoverableTool;
use codex_tools::JsonSchema;
use codex_tools::JsonSchemaPrimitiveType;
use codex_tools::JsonSchemaType;
use codex_tools::LoadableToolSpec;
use codex_tools::REQUEST_PLUGIN_INSTALL_TOOL_NAME;
use codex_tools::ResponsesApiNamespaceTool;
@@ -1139,7 +1141,7 @@ async fn unavailable_mcp_tools_are_exposed_as_dummy_function_tools() {
}
#[tokio::test]
async fn test_mcp_tool_property_missing_type_defaults_to_string() {
async fn test_mcp_tool_property_missing_type_defaults_to_open_object() {
let config = test_config().await;
let model_info = construct_model_info_offline("gpt-5.4", &config);
let mut features = Features::with_defaults();
@@ -1185,7 +1187,13 @@ async fn test_mcp_tool_property_missing_type_defaults_to_string() {
/*properties*/
BTreeMap::from([(
"query".to_string(),
JsonSchema::string(Some("search query".to_string())),
JsonSchema {
schema_type: Some(JsonSchemaType::Single(JsonSchemaPrimitiveType::Object)),
description: Some("search query".to_string()),
properties: Some(BTreeMap::new()),
additional_properties: Some(true.into()),
..Default::default()
},
)]),
/*required*/ None,
/*additional_properties*/ None

View File

@@ -29,7 +29,15 @@ fn parse_dynamic_tool_sanitizes_input_schema() {
input_schema: JsonSchema::object(
BTreeMap::from([(
"id".to_string(),
JsonSchema::string(Some("Ticket identifier".to_string()),),
JsonSchema {
schema_type: Some(crate::JsonSchemaType::Single(
crate::JsonSchemaPrimitiveType::Object,
)),
description: Some("Ticket identifier".to_string()),
properties: Some(BTreeMap::new()),
additional_properties: Some(true.into()),
..Default::default()
},
)]),
/*required*/ None,
/*additional_properties*/ None

View File

@@ -228,7 +228,10 @@ fn sanitize_json_schema(value: &mut JsonValue) {
{
schema_types.push(JsonSchemaPrimitiveType::Number);
} else {
schema_types.push(JsonSchemaPrimitiveType::String);
// With no schema hints, fall back to an open object so unknown
// tool argument shapes can still carry arbitrary named fields.
schema_types.push(JsonSchemaPrimitiveType::Object);
map.insert("additionalProperties".to_string(), JsonValue::Bool(true));
}
}

View File

@@ -34,7 +34,7 @@ fn parse_tool_input_schema_infers_object_shape_and_defaults_properties() {
//
// Expected normalization behavior:
// - `properties` implies an object schema when `type` is omitted.
// - The child property keeps its description and defaults to a string type.
// - The child property keeps its description and defaults to an open object.
let schema = parse_tool_input_schema(&serde_json::json!({
"properties": {
"query": {"description": "search query"}
@@ -47,7 +47,13 @@ fn parse_tool_input_schema_infers_object_shape_and_defaults_properties() {
JsonSchema::object(
BTreeMap::from([(
"query".to_string(),
JsonSchema::string(Some("search query".to_string())),
JsonSchema {
schema_type: Some(JsonSchemaType::Single(JsonSchemaPrimitiveType::Object)),
description: Some("search query".to_string()),
properties: Some(BTreeMap::new()),
additional_properties: Some(true.into()),
..Default::default()
},
)]),
/*required*/ None,
/*additional_properties*/ None
@@ -250,16 +256,73 @@ fn parse_tool_input_schema_infers_string_from_enum_const_and_format_keywords() {
}
#[test]
fn parse_tool_input_schema_defaults_empty_schema_to_string() {
fn parse_tool_input_schema_defaults_empty_schema_to_open_object() {
// Example schema shape:
// {}
//
// Expected normalization behavior:
// - With no structural hints at all, the normalizer falls back to a
// permissive string schema.
// permissive object schema.
let schema = parse_tool_input_schema(&serde_json::json!({})).expect("parse schema");
assert_eq!(schema, JsonSchema::string(/*description*/ None));
assert_eq!(
schema,
JsonSchema::object(BTreeMap::new(), /*required*/ None, Some(true.into()))
);
}
#[test]
fn parse_tool_input_schema_defaults_nested_empty_schema_to_open_object() {
// Example schema shape:
// {
// "type": "object",
// "properties": {
// "metadata": {
// "properties": {
// "extra": {}
// }
// }
// }
// }
//
// Expected normalization behavior:
// - The sanitizer recurses through nested object properties.
// - The innermost `extra` field has no hints, so it falls back to an open
// object.
let schema = parse_tool_input_schema(&serde_json::json!({
"type": "object",
"properties": {
"metadata": {
"properties": {
"extra": {}
}
}
}
}))
.expect("parse schema");
assert_eq!(
schema,
JsonSchema::object(
BTreeMap::from([(
"metadata".to_string(),
JsonSchema::object(
BTreeMap::from([(
"extra".to_string(),
JsonSchema::object(
BTreeMap::new(),
/*required*/ None,
Some(true.into()),
),
)]),
/*required*/ None,
/*additional_properties*/ None,
)
)]),
/*required*/ None,
/*additional_properties*/ None,
)
);
}
#[test]