diff --git a/tests/tools/test_schema_sanitizer.py b/tests/tools/test_schema_sanitizer.py index da6a1e30ad0f..e950049a59b1 100644 --- a/tests/tools/test_schema_sanitizer.py +++ b/tests/tools/test_schema_sanitizer.py @@ -867,3 +867,73 @@ def test_unrename_passes_unknown_keys_through(): def test_sanitize_property_key_empty_falls_back(): assert sanitize_property_key("~~~") == "___" assert sanitize_property_key("") == "param" + + +# --------------------------------------------------------------------------- +# dependentRequired -- literal property-name strings must survive +# --------------------------------------------------------------------------- + + +def test_dependent_required_preserved_through_public_api(): + """dependentRequired values are literal property names, not schemas.""" + schema = { + "type": "object", + "properties": { + "owner": {"type": "string"}, + "repo": {"type": "string"}, + "organization": {"type": "string"}, + }, + "dependentRequired": { + "owner": ["repo", "organization"], + "repo": ["owner"], + }, + } + tools = [_tool("t", copy.deepcopy(schema))] + out = sanitize_tool_schemas(tools) + params = out[0]["function"]["parameters"] + dep = params.get("dependentRequired", {}) + # Values are the original property-name strings unchanged. + assert dep.get("owner") == ["repo", "organization"] + assert dep.get("repo") == ["owner"] + # Normal property schemas are still present and valid. + assert params["properties"]["owner"] == {"type": "string"} + assert params["properties"]["repo"] == {"type": "string"} + assert params["properties"]["organization"] == {"type": "string"} + + +def test_dependent_required_does_not_mutate_original_input(): + """The original schema's dependentRequired must be unchanged after sanitize.""" + original_dep = {"owner": ["repo", "organization"], "repo": ["owner"]} + schema = { + "type": "object", + "properties": { + "owner": {"type": "string"}, + "repo": {"type": "string"}, + "organization": {"type": "string"}, + }, + "dependentRequired": {k: list(v) for k, v in original_dep.items()}, + } + saved_copy = copy.deepcopy(schema) + tools = [_tool("t", schema)] + _ = sanitize_tool_schemas(tools) + assert schema == saved_copy + assert schema["dependentRequired"] == original_dep + + +def test_dependent_schemas_still_recursively_sanitized(): + """dependentSchemas (real schemas, not literal lists) must still be sanitized.""" + schema = { + "type": "object", + "properties": { + "owner": {"type": "string"}, + }, + "dependentSchemas": { + "owner": {"type": "object"}, # bare object -- needs properties: {} + }, + } + tools = [_tool("t", copy.deepcopy(schema))] + out = sanitize_tool_schemas(tools) + dep_schemas = out[0]["function"]["parameters"]["dependentSchemas"] + assert dep_schemas["owner"] == {"type": "object", "properties": {}}, ( + f"dependentSchemas['owner'] was not fully sanitized: {dep_schemas['owner']!r}" + ) diff --git a/tools/schema_sanitizer.py b/tools/schema_sanitizer.py index 7dbcb1ef87fa..f7b331382bda 100644 --- a/tools/schema_sanitizer.py +++ b/tools/schema_sanitizer.py @@ -406,11 +406,13 @@ def _sanitize_node(node: Any, path: str) -> Any: _sanitize_node(item, f"{path}.{key}[{i}]") for i, item in enumerate(value) ] - elif key in {"required", "enum", "examples"}: + elif key in {"required", "enum", "examples", "dependentRequired"}: # Schema "sibling" keywords whose values are NOT schemas: # - ``required``: list of property-name strings # - ``enum``: list of literal values (any JSON type) # - ``examples``: list of example values (any JSON type) + # - ``dependentRequired``: mapping of property names to lists of + # required property-name strings (JSON Schema 2020-12) # Recursing into these with _sanitize_node() would mis-interpret # literal strings like "path" as bare-string schemas and replace # them with {"type": "object"} dicts. Pass through unchanged