"""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", ]