Files
6krrt/tests/test_capabilities.py
adlee-was-taken 92adb5c29d test: pin iter_image_url_values contract on well-typed-but-unparseable input
The shared helper's fail-closed behavior (yielding the "" sentinel rather
than skipping a well-typed-but-unparseable image_url part) was only tested
indirectly through detect_capabilities. Add direct unit tests covering the
four input shapes of the shared-helper-unparseable-input-contract plan:
common/valid, alternate valid, well-typed-but-unparseable (-> sentinel), and
wrong-typed/absent (-> skip). Presence of the image_url key is the type tag:
a present-but-malformed payload still yields the sentinel.
2026-08-23 16:02:25 -04:00

169 lines
6.4 KiB
Python

"""Tests for capabilities.py — request-side capability detection.
Each case proves one detection fires and that the others stay False, mirroring
the "rejects and does not over-reject" shape of test_routing.py.
"""
from capabilities import detect_capabilities, iter_image_url_values
def test_no_parts_yields_no_capabilities():
caps = detect_capabilities({"messages": [{"content": "plain text"}]})
assert caps == detect_capabilities({})
assert not caps.has_images
assert not caps.require_json_mode
assert not caps.tools_present
assert not caps.has_reasoning_request
def test_image_url_part_sets_has_images():
body = {
"messages": [
{"content": [{"type": "image_url", "url": "https://x/img.png"}]}
]
}
assert detect_capabilities(body).has_images is True
def test_a_degenerate_image_url_part_still_counts():
# No `image_url` key and no bare `url` key -- spec-invalid, but a part
# that declares itself an image must still count as one rather than
# silently vanishing because its value can't be resolved. Skipping it
# would let a malformed part dodge the vision-capability gate.
body = {"messages": [{"content": [{"type": "image_url"}]}]}
assert detect_capabilities(body).has_images is True
def test_image_in_an_earlier_message_counts():
body = {
"messages": [
{"content": [{"type": "image_url", "url": "https://x/1.png"}]},
{"content": "now what do you make of it?"},
]
}
assert detect_capabilities(body).has_images is True
def test_non_image_parts_do_not_trigger():
body = {
"messages": [
{
"content": [
{"type": "text", "text": "hello"},
{"type": "tool_use", "id": "t1"},
]
}
]
}
assert detect_capabilities(body).has_images is False
def test_string_content_does_not_trigger_images():
# A raw base64 string in a text part is not an image_url part.
body = {"messages": [{"content": "data:image/png;base64,AA=="}]}
assert detect_capabilities(body).has_images is False
def test_json_object_response_format_sets_require_json_mode():
body = {"response_format": {"type": "json_object"}}
assert detect_capabilities(body).require_json_mode is True
def test_json_schema_response_format_sets_require_json_mode():
body = {"response_format": {"type": "json_schema", "json_schema": {}}}
assert detect_capabilities(body).require_json_mode is True
def test_text_or_missing_response_format_does_not_require_json_mode():
assert detect_capabilities({}).require_json_mode is False
assert detect_capabilities({"response_format": {"type": "text"}}).require_json_mode is False
def test_tools_array_sets_tools_present():
assert detect_capabilities({"tools": [{"type": "function"}]}).tools_present is True
# An empty array is no tools at all.
assert detect_capabilities({"tools": []}).tools_present is False
def test_reasoning_effort_sets_has_reasoning_request():
assert detect_capabilities({"reasoning_effort": "high"}).has_reasoning_request is True
def test_reasoning_param_sets_has_reasoning_request():
assert detect_capabilities({"reasoning": {"enabled": True}}).has_reasoning_request is True
# --- iter_image_url_values contract --------------------------------------
#
# Pins the shared helper's contract across the four input shapes of
# code_plans/shared-helper-unparseable-input-contract.md. The point of this
# test is the same as the contract: the well-typed-but-unparseable shape
# must YIELD ITS SENTINEL ("") rather than be skipped silently, because
# skipping drops the part out of the fail-closed image checks at once.
def test_iter_image_url_values_yields_valid_dict_url():
# Common/valid shape: image_url as a dict carrying `url`.
messages = [{"content": [{"type": "image_url", "image_url": {"url": "https://x/img.png"}}]}]
assert list(iter_image_url_values(messages)) == ["https://x/img.png"]
def test_iter_image_url_values_alternate_valid_shapes():
# Alternate valid shapes the four originals agreed on:
# image_url as a bare string, and a bare `url` key on the part itself.
messages = [
{"content": [{"type": "image_url", "image_url": "https://x/bare.png"}]},
{"content": [{"type": "image_url", "url": "https://x/barekey.png"}]},
]
assert list(iter_image_url_values(messages)) == [
"https://x/bare.png",
"https://x/barekey.png",
]
def test_iter_image_url_values_well_typed_unparseable_yields_sentinel():
# Well-typed but unparseable: the part declares itself an image_url but
# carries neither `image_url` nor a bare `url` that resolves. It must
# still yield SOMETHING (the "" sentinel) rather than vanish, so a
# degenerate part cannot slip out of the fail-closed checks. This is the
# shape the plan exists to pin down.
messages = [{"content": [{"type": "image_url"}]}]
assert list(iter_image_url_values(messages)) == [""]
def test_iter_image_url_values_wrong_payload_still_yields_sentinel():
# The `image_url` key is present but its value is not a dict or a string
# (an int, say). Presence of the key is enough to claim "this is an
# image"; the payload fails to resolve, so it yields the "" sentinel
# rather than being skipped -- the same fail-closed contract as a part
# with no key at all.
messages = [{"content": [{"type": "image_url", "image_url": 42}]}]
assert list(iter_image_url_values(messages)) == [""]
def test_iter_image_url_values_skips_wrong_typed_and_absent():
# Wrong-typed / absent shapes fail the coarse check and are skipped;
# they contribute nothing and yield nothing.
messages = [
"not a dict",
{"content": "plain string, not a list"},
{"content": [{"type": "text", "text": "hello"}]},
{"content": [{"type": "tool_use", "id": "t1"}]},
]
assert list(iter_image_url_values(messages)) == []
def test_iter_image_url_values_mixed_shapes_preserve_order():
# A degenerate part in the middle must still count (as "") and not
# disturb the ordering of the parts around it.
messages = [
{"content": [{"type": "image_url", "url": "https://x/1.png"}]},
{"content": [{"type": "image_url"}]},
{"content": [{"type": "image_url", "image_url": {"url": "https://x/2.png"}}]},
]
assert list(iter_image_url_values(messages)) == [
"https://x/1.png",
"",
"https://x/2.png",
]