band-aider-pub/tests/band_aider/context/test_validation_utils.py
2026-10-17 11:57:37 -07:00

406 lines
12 KiB
Python

from __future__ import annotations
from band_aider.context.validation_utils import (
ITEMS,
ValidationError,
ValidationLocation,
ValidationResult,
validate_shape,
)
RESPONSE_LOCATION = ValidationLocation(["response"])
def test_validate_shape_accepts_complete_recursive_shape() -> None:
"""The reusable validator admits complete nested values and empty typed arrays."""
reference = {
"name": "string value",
"details": {"locations": ["string item"]},
}
assert validate_shape(
{"name": "example", "details": {"locations": ["one.py", "two.py"]}},
[reference],
location=RESPONSE_LOCATION,
) == ValidationResult()
assert validate_shape(
{"name": "example", "details": {"locations": []}},
[reference],
location=RESPONSE_LOCATION,
) == ValidationResult()
def test_validate_shape_requires_every_recursive_member() -> None:
"""A missing member is owned by the object that should contain it."""
assert validate_shape(
{},
[{"b": 1}],
location=ValidationLocation(["A"]),
) == ValidationResult(
errors=(ValidationError(
location=ValidationLocation(["A"]),
error="missing member b.",
),),
)
def test_validate_shape_requires_every_recursive_type() -> None:
"""The reusable validator rejects a mismatched leaf at its exact array index."""
reference = {
"name": "string value",
"details": {"locations": ["string item"]},
}
assert validate_shape(
{"name": "example", "details": {"locations": ["one.py", 2]}},
[reference],
location=RESPONSE_LOCATION,
) == ValidationResult(
errors=(ValidationError(
location=ValidationLocation([
"response", "details", "locations", ITEMS, "1",
]),
error="expected a string but found an int.",
),),
)
def test_validate_shape_does_not_compare_list_cardinality() -> None:
"""Zero, one, or many members satisfy one declared reusable list-member shape."""
reference: list[object] = [{"b": ["a_number"]}]
assert validate_shape({"b": []}, reference, location=RESPONSE_LOCATION).valid
assert validate_shape(
{"b": ["one"]}, reference, location=RESPONSE_LOCATION,
).valid
assert validate_shape(
{"b": ["one", "two"]}, reference, location=RESPONSE_LOCATION,
).valid
def test_validate_shape_reports_list_member_type_with_brackets() -> None:
"""List-member type belongs to its indexed leaf and ignores list cardinality."""
result = validate_shape(
{"b": ["one", "two"]},
[{"b": [1, 2]}],
location=ValidationLocation(["A"]),
)
assert result == ValidationResult(errors=(
ValidationError(
location=ValidationLocation(["A", "b", ITEMS, "0"]),
error="expected an int but found a string.",
),
ValidationError(
location=ValidationLocation(["A", "b", ITEMS, "1"]),
error="expected an int but found a string.",
),
))
assert result.errors[0].location.parts == ["A", "b", "__items__", "0"]
assert result.errors[0].stringify() == (
"A.b[0]: expected an int but found a string."
)
def test_validate_shape_reports_null_list_member_at_list_container() -> None:
"""A null list member is owned by its list rather than its indexed leaf."""
assert validate_shape(
{"b": [None]},
[{"b": [1]}],
location=ValidationLocation(["A"]),
) == ValidationResult(errors=(ValidationError(
location=ValidationLocation(["A", "b"]),
error="member [0] expected an int but found null.",
),))
def test_validate_shape_returns_invalid_for_unexpected_key_type() -> None:
"""An unexpected non-string object key remains invalid data rather than an exception."""
assert validate_shape(
{1: "example"},
[{"name": "example"}],
location=RESPONSE_LOCATION,
) == ValidationResult(
errors=(ValidationError(
location=RESPONSE_LOCATION,
error="missing member name.",
), ValidationError(
location=RESPONSE_LOCATION,
error="unexpected member 1.",
),),
)
def test_validate_shape_reports_unexpected_member_at_container() -> None:
"""An unexpected member is owned by the object that contains it."""
assert validate_shape(
{"b": 1, "c": 2},
[{"b": 1}],
location=ValidationLocation(["A"]),
) == ValidationResult(errors=(ValidationError(
location=ValidationLocation(["A"]),
error="unexpected member c.",
),))
def test_validate_shape_reports_member_failure_at_member_location() -> None:
"""An existing member's invalid type is owned by that member's location."""
result = validate_shape(
{"b": "not a number"},
[{"b": 1}],
location=ValidationLocation(["A"]),
)
assert result == ValidationResult(errors=(ValidationError(
location=ValidationLocation(["A", "b"]),
error="expected an int but found a string.",
),))
assert result.errors[0].location.parts == ["A", "b"]
assert result.errors[0].location.stringify() == "A.b"
assert result.errors[0].stringify() == (
"A.b: expected an int but found a string."
)
def test_validate_shape_reports_null_member_at_container() -> None:
"""An unexpected null is owned by the object containing that member."""
assert validate_shape(
{"b": None},
[{"b": 1}],
location=ValidationLocation(["A"]),
) == ValidationResult(errors=(ValidationError(
location=ValidationLocation(["A"]),
error="member b expected an int but found null.",
),))
def test_validate_shape_reports_member_shape_at_member_location() -> None:
"""An existing member's invalid nested shape is owned by that member."""
assert validate_shape(
{"b": {}},
[{"b": {"c": 1}}],
location=ValidationLocation(["A"]),
) == ValidationResult(errors=(ValidationError(
location=ValidationLocation(["A", "b"]),
error="missing member c.",
),))
def test_validate_shape_accepts_any_complete_reference() -> None:
"""Plural references form alternatives, including a nullable member shape."""
assert validate_shape(
{"b": None},
[{"b": "string value"}, {"b": None}],
location=ValidationLocation(["A"]),
) == ValidationResult()
def test_validate_shape_distinguishes_disjunctive_and_conjunctive_references() -> None:
"""Separate closed shapes reject their union until conjunctive overlay is selected."""
references: list[object] = [{"b": 1}, {"c": 1}]
actual = {"b": 1, "c": 1}
assert not validate_shape(
actual,
references,
location=ValidationLocation(["A"]),
).valid
assert validate_shape(
actual,
references,
location=ValidationLocation(["A"]),
conjunctive=True,
).valid
def test_validate_shape_conjunctive_overlay_recurses_only_through_objects() -> None:
"""Nested objects merge while later primitive and list schemas replace earlier ones."""
references: list[object] = [
{"a": {"b": 1, "value": 1}, "values": [1]},
{"a": {"c": 1, "value": "text"}, "values": ["text"]},
]
assert validate_shape(
{
"a": {"b": 1, "c": 1, "value": "later"},
"values": ["one", "two"],
},
references,
location=ValidationLocation(["A"]),
conjunctive=True,
).valid
assert validate_shape(
{
"a": {"b": 1, "c": 1, "value": 1},
"values": [1],
},
references,
location=ValidationLocation(["A"]),
conjunctive=True,
) == ValidationResult(errors=(
ValidationError(
location=ValidationLocation(["A", "a", "value"]),
error="expected a string but found an int.",
),
ValidationError(
location=ValidationLocation(["A", "values", ITEMS, "0"]),
error="expected a string but found an int.",
),
))
def test_validate_shape_conjunctive_overlay_replaces_cross_kind_from_right() -> None:
"""A later primitive reference replaces an earlier object at the same member."""
references: list[object] = [{"a": {"b": 1}}, {"a": "text"}]
assert validate_shape(
{"a": "later"},
references,
location=ValidationLocation(["A"]),
conjunctive=True,
).valid
def test_validate_shape_conjunctive_overlay_prefers_non_null_collisions() -> None:
"""A null collision yields its specified side without erasing null-only members."""
references: list[object] = [
{
"left": 1,
"right": None,
"left_null": None,
"both_null": None,
"nested": {
"left": 1,
"right": None,
"left_null": None,
"both_null": None,
},
},
{
"left": None,
"right": 2,
"right_null": None,
"both_null": None,
"nested": {
"left": None,
"right": 2,
"right_null": None,
"both_null": None,
},
},
]
actual = {
"left": 1,
"right": 2,
"left_null": None,
"right_null": None,
"both_null": None,
"nested": {
"left": 1,
"right": 2,
"left_null": None,
"right_null": None,
"both_null": None,
},
}
assert validate_shape(
actual,
references,
location=ValidationLocation(["A"]),
conjunctive=True,
).valid
assert validate_shape(
{"b": 1},
[None, {"b": 1}, None],
location=ValidationLocation(["A"]),
conjunctive=True,
).valid
assert validate_shape(
{"b": None},
[{"b": None}],
location=ValidationLocation(["A"]),
conjunctive=True,
).valid
assert validate_shape(
{},
[{"b": None}],
location=ValidationLocation(["A"]),
conjunctive=True,
) == ValidationResult(errors=(ValidationError(
location=ValidationLocation(["A"]),
error="missing member b.",
),))
def test_validate_shape_accepts_int_for_float_but_not_float_for_int() -> None:
"""A declared float widens an int, while a declared int rejects a float."""
assert validate_shape(
{"b": 1},
[{"b": 1.0}],
location=ValidationLocation(["A"]),
).valid
assert validate_shape(
{"b": 1.5},
[{"b": 1}],
location=ValidationLocation(["A"]),
) == ValidationResult(errors=(ValidationError(
location=ValidationLocation(["A", "b"]),
error="expected an int but found a float.",
),))
def test_validate_shape_rejects_only_after_every_reference_fails() -> None:
"""An invalid union reports the structured errors from every attempted shape."""
result = validate_shape(
{"b": 7},
[{"b": "string value"}, {"b": None}],
location=ValidationLocation(["A"]),
)
assert result == ValidationResult(errors=(
ValidationError(
location=ValidationLocation(["A", "b"]),
error="expected a string but found an int.",
),
ValidationError(
location=ValidationLocation(["A"]),
error="member b expected null but found an int.",
),
))
assert result.stringify() == (
"A.b: expected a string but found an int.\n"
"A: member b expected null but found an int."
)
def test_validate_shape_rejects_absent_references() -> None:
"""No alternative reference can admit an actual value."""
assert validate_shape(
{"b": None},
[],
location=ValidationLocation(["A"]),
) == ValidationResult(errors=(ValidationError(
location=ValidationLocation(["A"]),
error="at least one reference shape is required.",
),))