Skip to content

Commit 282b8c0

Browse files
committed
test(validate): cover base-path schema validation
The union-typed structural path is pinned in test_validate_union_types, but the rest of validate() — primitive type gating, the bool/number/integer interplay, array items, nested object paths, the _kind message reporter, multi-error accumulation, and untyped passthrough — had no direct test. This module gates every tool invocation (server.py _resolve_one), so pin the contract the resolver depends on.
1 parent acdcaa8 commit 282b8c0

1 file changed

Lines changed: 142 additions & 0 deletions

File tree

tests/test_validate.py

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
"""Base-path coverage for the JSON-Schema subset validator.
2+
3+
`validate()` gates every tool invocation: when an LLM emits arguments that
4+
don't match a connected capability's declared schema, the hub short-circuits
5+
the call (server.py `_resolve_one`). The union-typed structural path is pinned
6+
in test_validate_union_types; everything else — primitive type gating, the
7+
bool/number/integer interplay, array `items`, nested objects, the `_kind`
8+
message reporter, multi-error accumulation, and the untyped passthrough —
9+
had no direct test. These pin the contract the resolver depends on.
10+
"""
11+
12+
from zhub.validate import validate, _kind
13+
14+
15+
# --- primitive type gate + message shape ----------------------------------
16+
17+
def test_type_mismatch_reports_root_and_kinds():
18+
assert validate(5, {"type": "string"}) == [
19+
"<root>: expected string, got integer"
20+
]
21+
22+
23+
def test_int_validates_as_number_but_float_not_as_integer():
24+
# JSON has one numeric type; the validator treats any int as a valid
25+
# `number`, but an `integer` must be a real int (a float like 5.0 fails).
26+
assert validate(5, {"type": "number"}) == []
27+
assert validate(5.0, {"type": "integer"}) == [
28+
"<root>: expected integer, got number"
29+
]
30+
31+
32+
def test_bool_is_not_integer_or_number():
33+
# bool is an int subclass in Python; the validator must reject it for
34+
# numeric types or a schema expecting a count would accept True.
35+
assert validate(True, {"type": "integer"}) == [
36+
"<root>: expected integer, got boolean"
37+
]
38+
assert validate(True, {"type": "number"}) == [
39+
"<root>: expected number, got boolean"
40+
]
41+
42+
43+
def test_null_member_of_union_passes():
44+
assert validate(None, {"type": ["string", "null"]}) == []
45+
46+
47+
def test_unknown_type_string_is_lenient():
48+
# Unsupported keywords/types are ignored rather than crashing the gate.
49+
assert validate(5, {"type": "weird"}) == []
50+
51+
52+
def test_empty_or_non_dict_schema_passes():
53+
assert validate({"anything": 1}, {}) == []
54+
assert validate(5, "not-a-schema") == [] # type: ignore[arg-type]
55+
56+
57+
# --- structural: objects ----------------------------------------------------
58+
59+
def test_missing_required_accumulates_each_field():
60+
errs = validate({}, {"type": "object", "required": ["a", "b"]})
61+
assert errs == [
62+
"<root>: missing required field 'a'",
63+
"<root>: missing required field 'b'",
64+
]
65+
66+
67+
def test_property_type_error_carries_field_name():
68+
schema = {
69+
"type": "object",
70+
"properties": {"a": {"type": "string"}, "b": {"type": "string"}},
71+
}
72+
assert validate({"a": "x", "b": 3}, schema) == [
73+
"b: expected string, got integer"
74+
]
75+
76+
77+
def test_nested_object_path_is_dotted():
78+
schema = {
79+
"type": "object",
80+
"properties": {
81+
"x": {"type": "object", "properties": {"y": {"type": "string"}}}
82+
},
83+
}
84+
assert validate({"x": {"y": 1}}, schema) == [
85+
"x.y: expected string, got integer"
86+
]
87+
88+
89+
def test_required_ignored_when_value_is_not_an_object():
90+
# `required` is only meaningful for objects; a string value must not
91+
# trip a missing-field error.
92+
assert validate("hi", {"required": ["a"]}) == []
93+
94+
95+
# --- structural: arrays -----------------------------------------------------
96+
97+
def test_array_item_error_carries_index():
98+
schema = {"type": "array", "items": {"type": "integer"}}
99+
assert validate([1, "x", 3], schema) == [
100+
"[1]: expected integer, got string"
101+
]
102+
103+
104+
# --- type-mismatch short-circuit -------------------------------------------
105+
106+
def test_type_mismatch_short_circuits_structural_checks():
107+
# When the top-level type is wrong, only the type error is reported —
108+
# downstream required/properties checks assume the type matched.
109+
assert validate(5, {"type": "object", "required": ["a"]}) == [
110+
"<root>: expected object, got integer"
111+
]
112+
113+
114+
# --- untyped schemas still run structural checks ---------------------------
115+
116+
def test_untyped_schema_still_checks_required_and_properties():
117+
errs = validate(
118+
{}, {"required": ["a"], "properties": {"a": {"type": "string"}}}
119+
)
120+
assert errs == ["<root>: missing required field 'a'"]
121+
122+
123+
def test_untyped_schema_still_checks_array_items():
124+
assert validate([1, "x"], {"items": {"type": "integer"}}) == [
125+
"[1]: expected integer, got string"
126+
]
127+
128+
129+
# --- _kind reporter ---------------------------------------------------------
130+
131+
def test_kind_distinguishes_every_json_type():
132+
assert _kind(True) == "boolean"
133+
assert _kind(3) == "integer"
134+
assert _kind(1.5) == "number"
135+
assert _kind("s") == "string"
136+
assert _kind([]) == "array"
137+
assert _kind({}) == "object"
138+
assert _kind(None) == "null"
139+
140+
141+
def test_kind_falls_back_to_python_type_name():
142+
assert _kind((1, 2)) == "tuple"

0 commit comments

Comments
 (0)