154 lines
5.2 KiB
Python
154 lines
5.2 KiB
Python
"""Tests for strict-mode JSON-schema normalization."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import Any
|
|
|
|
from omnigent_client.tools._strict import ensure_strict_schema
|
|
|
|
|
|
def test_strict_object_schema_sets_additional_properties_false() -> None:
|
|
"""Object schemas get ``additionalProperties: false`` added."""
|
|
schema = {
|
|
"type": "object",
|
|
"properties": {"x": {"type": "integer"}},
|
|
}
|
|
result = ensure_strict_schema(schema)
|
|
assert result["additionalProperties"] is False
|
|
|
|
|
|
def test_strict_object_schema_marks_all_properties_required() -> None:
|
|
"""Strict mode forces every property into ``required``."""
|
|
schema = {
|
|
"type": "object",
|
|
"properties": {
|
|
"x": {"type": "integer"},
|
|
"y": {"type": "string"},
|
|
},
|
|
"required": ["x"], # Original only requires x.
|
|
}
|
|
result = ensure_strict_schema(schema)
|
|
# Both x and y should now be required, regardless of original.
|
|
# Use sorted comparison since ordering is not guaranteed.
|
|
assert sorted(result["required"]) == ["x", "y"]
|
|
|
|
|
|
def test_strict_empty_object_schema_no_required() -> None:
|
|
"""An object with no properties keeps required empty (or absent)."""
|
|
schema: dict[str, Any] = {
|
|
"type": "object",
|
|
"properties": {},
|
|
}
|
|
result = ensure_strict_schema(schema)
|
|
# No properties means no required entries to add.
|
|
# The implementation skips writing 'required' when properties is empty;
|
|
# callers can rely on `required` being empty/absent in this case.
|
|
assert result.get("required", []) == []
|
|
assert result["additionalProperties"] is False
|
|
|
|
|
|
def test_strict_does_not_mutate_input() -> None:
|
|
"""The normalizer must return a new dict, leaving input unchanged."""
|
|
schema: dict[str, Any] = {
|
|
"type": "object",
|
|
"properties": {"x": {"type": "integer"}},
|
|
"required": [],
|
|
}
|
|
original_required = schema["required"]
|
|
result = ensure_strict_schema(schema)
|
|
# The input's required list must be unchanged (we returned a new dict).
|
|
assert schema["required"] is original_required
|
|
assert schema["required"] == []
|
|
# Result has the new constraints.
|
|
assert result["required"] == ["x"]
|
|
|
|
|
|
def test_strict_recurses_into_nested_object_property() -> None:
|
|
"""Nested object schemas under properties are also normalized."""
|
|
schema = {
|
|
"type": "object",
|
|
"properties": {
|
|
"inner": {
|
|
"type": "object",
|
|
"properties": {"a": {"type": "integer"}},
|
|
},
|
|
},
|
|
}
|
|
result = ensure_strict_schema(schema)
|
|
inner = result["properties"]["inner"]
|
|
# Inner object also gets strict-mode constraints applied.
|
|
assert inner["additionalProperties"] is False
|
|
assert inner["required"] == ["a"]
|
|
|
|
|
|
def test_strict_recurses_into_array_items() -> None:
|
|
"""Object schemas inside ``items`` are normalized too."""
|
|
schema = {
|
|
"type": "array",
|
|
"items": {
|
|
"type": "object",
|
|
"properties": {"k": {"type": "string"}},
|
|
},
|
|
}
|
|
result = ensure_strict_schema(schema)
|
|
item = result["items"]
|
|
assert item["additionalProperties"] is False
|
|
assert item["required"] == ["k"]
|
|
|
|
|
|
def test_strict_recurses_into_anyof_oneof_allof() -> None:
|
|
"""Union variants get normalized recursively."""
|
|
schema = {
|
|
"anyOf": [
|
|
{
|
|
"type": "object",
|
|
"properties": {"a": {"type": "integer"}},
|
|
},
|
|
{
|
|
"type": "object",
|
|
"properties": {"b": {"type": "string"}},
|
|
},
|
|
]
|
|
}
|
|
result = ensure_strict_schema(schema)
|
|
assert result["anyOf"][0]["additionalProperties"] is False
|
|
assert result["anyOf"][0]["required"] == ["a"]
|
|
assert result["anyOf"][1]["additionalProperties"] is False
|
|
assert result["anyOf"][1]["required"] == ["b"]
|
|
|
|
|
|
def test_strict_recurses_into_defs() -> None:
|
|
"""``$defs`` referenced types are normalized."""
|
|
schema = {
|
|
"type": "object",
|
|
"properties": {"r": {"$ref": "#/$defs/MyType"}},
|
|
"$defs": {
|
|
"MyType": {
|
|
"type": "object",
|
|
"properties": {"q": {"type": "integer"}},
|
|
},
|
|
},
|
|
}
|
|
result = ensure_strict_schema(schema)
|
|
inner = result["$defs"]["MyType"]
|
|
assert inner["additionalProperties"] is False
|
|
assert inner["required"] == ["q"]
|
|
|
|
|
|
def test_strict_passthrough_for_non_dict_input() -> None:
|
|
"""A non-dict (e.g. boolean True/False) is returned unchanged."""
|
|
# JSON Schema permits boolean schemas (true/false) at certain positions.
|
|
# The normalizer should not crash on these.
|
|
assert ensure_strict_schema(True) is True # type: ignore[arg-type]
|
|
assert ensure_strict_schema(False) is False # type: ignore[arg-type]
|
|
|
|
|
|
def test_strict_passthrough_for_non_object_type() -> None:
|
|
"""Schemas with non-object types are returned essentially unchanged."""
|
|
schema = {"type": "string", "minLength": 1}
|
|
result = ensure_strict_schema(schema)
|
|
# Should be a copy with the same content (no additionalProperties added
|
|
# since type is not object).
|
|
assert result == {"type": "string", "minLength": 1}
|
|
assert "additionalProperties" not in result
|