Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1ebe193a3e |
@@ -0,0 +1,312 @@
|
||||
"""
|
||||
Tests targeting the remaining uncovered branches in client.py and image_to_text_node.py.
|
||||
|
||||
Coverage gaps addressed (as reported by pytest-cov on the main branch):
|
||||
client.py : lines 117-118, 142, 207, 222->228, 224->223, 226->223, 234->232, 291->293
|
||||
image_to_text_node: lines 112->114, 157, 181, 183->189
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from types import SimpleNamespace
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
import client
|
||||
from helpers.fakes import FakeTensor, make_chat_completion, ns
|
||||
from helpers.imports import import_repo_module
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# client.py – coerce_seed: .item() call raises, falls back to plain value
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class _BadItem:
|
||||
"""Numpy-scalar-like object whose .item() always raises."""
|
||||
|
||||
def item(self):
|
||||
raise RuntimeError("item() failed")
|
||||
|
||||
|
||||
def test_coerce_seed_item_raises_falls_back_to_unsupported_type() -> None:
|
||||
"""
|
||||
When value has .item() but the call raises, coerce_seed catches the exception
|
||||
(lines 117-118) and falls through to the type-check below. Since _BadItem is
|
||||
not int/float/str/bytes, it must then hit the final raise (line 142).
|
||||
"""
|
||||
with pytest.raises(ValueError, match="Unsupported seed type"):
|
||||
client.coerce_seed(_BadItem())
|
||||
|
||||
|
||||
def test_coerce_seed_unsupported_type_raises() -> None:
|
||||
"""
|
||||
A plain object with no recognised numeric/string interface must raise the
|
||||
'Unsupported seed type' error (line 142).
|
||||
"""
|
||||
with pytest.raises(ValueError, match="Unsupported seed type"):
|
||||
client.coerce_seed(object())
|
||||
|
||||
with pytest.raises(ValueError, match="Unsupported seed type"):
|
||||
client.coerce_seed({"a": 1})
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# client.py – extract_chat_completion_text: content=[] returns ""
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def test_extract_chat_completion_text_empty_list_content() -> None:
|
||||
"""
|
||||
When the message.content is an empty list the function should return "".
|
||||
This exercises the branch at line 207 where content is a list but parts
|
||||
ends up empty so "\n".join returns "".
|
||||
"""
|
||||
completion = make_chat_completion([])
|
||||
assert client.extract_chat_completion_text(completion) == ""
|
||||
|
||||
|
||||
def test_extract_chat_completion_text_list_with_no_usable_text() -> None:
|
||||
"""
|
||||
List content items where getattr(part, 'text', None) is None and the item
|
||||
is also not a dict – they are silently skipped, exercising the branches
|
||||
at lines 222->228 / 224->223 / 226->223.
|
||||
"""
|
||||
content = [
|
||||
ns(other="no-text"), # has no .text, not a dict – skipped
|
||||
ns(text=None), # text attr is None – skipped
|
||||
{"other": "ignored"}, # dict without 'text' key – skipped
|
||||
ns(text="kept"), # text attr is str – included
|
||||
]
|
||||
completion = make_chat_completion(content)
|
||||
assert client.extract_chat_completion_text(completion) == "kept"
|
||||
|
||||
|
||||
def test_extract_chat_completion_text_non_list_non_str_content() -> None:
|
||||
"""
|
||||
When message.content is neither a str nor a list (e.g. an int or None),
|
||||
the function should return "" via the final branch at line 207.
|
||||
"""
|
||||
assert client.extract_chat_completion_text(make_chat_completion(42)) == ""
|
||||
assert client.extract_chat_completion_text(make_chat_completion(None)) == ""
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# client.py – extract_responses_text: output list with no content at all
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def test_extract_responses_text_output_list_empty_items() -> None:
|
||||
"""
|
||||
output is a list but its items carry neither usable 'content' lists nor
|
||||
a 'text' attribute, so parts stays empty and the function returns "".
|
||||
This exercises the branch at line 234->232.
|
||||
"""
|
||||
response = ns(output_text="", output=[ns(content=None), ns()])
|
||||
assert client.extract_responses_text(response) == ""
|
||||
|
||||
|
||||
def test_extract_responses_text_output_none() -> None:
|
||||
"""output attribute is None – falls through to return "" at the end."""
|
||||
assert client.extract_responses_text(ns(output_text="", output=None)) == ""
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# client.py – build_chat_messages_with_image: no system prompt (line 291->293)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def test_build_chat_messages_with_image_no_system_prompt() -> None:
|
||||
"""
|
||||
When system_prompt is blank/empty the system message must be omitted so
|
||||
only the user message is present. This covers branch 291->293.
|
||||
"""
|
||||
messages = client.build_chat_messages_with_image(
|
||||
system_prompt=" ",
|
||||
user_prompt="describe it",
|
||||
image_data_url="data:image/png;base64,abc==",
|
||||
)
|
||||
assert len(messages) == 1
|
||||
assert messages[0]["role"] == "user"
|
||||
user_content = messages[0]["content"]
|
||||
assert user_content[0] == {"type": "text", "text": "describe it"}
|
||||
assert user_content[1]["type"] == "image_url"
|
||||
|
||||
# None system_prompt is treated the same way.
|
||||
messages_none = client.build_chat_messages_with_image(
|
||||
system_prompt=None,
|
||||
user_prompt="hello",
|
||||
image_data_url="data:image/png;base64,xyz==",
|
||||
)
|
||||
assert len(messages_none) == 1
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# image_to_text_node.py – _responses_kwargs: empty system_prompt (line 112->114)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _image_module():
|
||||
return import_repo_module("image_to_text_node", force_reload=True)
|
||||
|
||||
|
||||
def _connection_payload(*, reasoning_enabled: bool = False, use_tooling_mcp: bool = False):
|
||||
models = import_repo_module("models", force_reload=True)
|
||||
return models.LMStudioConnectionPayload(
|
||||
server_url="http://127.0.0.1:1234",
|
||||
base_url="http://127.0.0.1:1234/v1",
|
||||
api_key="token",
|
||||
model="vision-model",
|
||||
reasoning_enabled=reasoning_enabled,
|
||||
max_tokens=128,
|
||||
temperature=0.3,
|
||||
timeout_seconds=30,
|
||||
use_tooling_mcp=use_tooling_mcp,
|
||||
)
|
||||
|
||||
|
||||
def _single_image() -> FakeTensor:
|
||||
return FakeTensor(np.array([[[1.0, 0.0, 0.0]]], dtype=np.float32))
|
||||
|
||||
|
||||
def _batch_image() -> FakeTensor:
|
||||
return FakeTensor(
|
||||
np.array(
|
||||
[
|
||||
[[[1.0, 0.0, 0.0]]],
|
||||
[[[0.0, 0.0, 1.0]]],
|
||||
],
|
||||
dtype=np.float32,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def test_responses_kwargs_no_system_prompt_omits_instructions() -> None:
|
||||
"""
|
||||
When system_prompt is blank the 'instructions' key must NOT appear in
|
||||
the returned kwargs. This covers the branch at line 112->114.
|
||||
"""
|
||||
image_node = _image_module()
|
||||
kwargs = image_node.LMStudioImageToText._responses_kwargs(
|
||||
connection=_connection_payload(),
|
||||
image=_single_image(),
|
||||
system_prompt="",
|
||||
user_prompt="describe",
|
||||
seed=1,
|
||||
)
|
||||
assert "instructions" not in kwargs
|
||||
|
||||
kwargs_whitespace = image_node.LMStudioImageToText._responses_kwargs(
|
||||
connection=_connection_payload(),
|
||||
image=_single_image(),
|
||||
system_prompt=" ",
|
||||
user_prompt="describe",
|
||||
seed=1,
|
||||
)
|
||||
assert "instructions" not in kwargs_whitespace
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# image_to_text_node.py – execute: responses returns empty text (line 157)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def test_execute_responses_empty_text_falls_back_to_chat(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
"""
|
||||
When the responses endpoint returns an empty string the node must raise
|
||||
a ValueError internally and fall back to chat.completions.
|
||||
This covers line 157 ('raise ValueError("responses endpoint returned no text output")').
|
||||
"""
|
||||
image_node = _image_module()
|
||||
|
||||
completion = SimpleNamespace(
|
||||
choices=[SimpleNamespace(message=SimpleNamespace(content="chat fallback text"))]
|
||||
)
|
||||
fake_client = SimpleNamespace(
|
||||
responses=SimpleNamespace(create=lambda **_: SimpleNamespace(output_text="")),
|
||||
chat=SimpleNamespace(completions=SimpleNamespace(create=lambda **_: completion)),
|
||||
)
|
||||
monkeypatch.setattr(image_node, "resolve_request_seed", lambda _: 5)
|
||||
monkeypatch.setattr(image_node, "create_openai_client", lambda **_: fake_client)
|
||||
|
||||
output = image_node.LMStudioImageToText.execute(
|
||||
connection=_connection_payload(),
|
||||
image=_single_image(),
|
||||
system_prompt="sys",
|
||||
user_prompt="describe",
|
||||
seed=-1,
|
||||
)
|
||||
|
||||
assert output[0] == "chat fallback text"
|
||||
assert "via chat.completions" in output.ui.text
|
||||
assert "responses endpoint returned no text output" in output.ui.text
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# image_to_text_node.py – execute: chat fallback also empty (line 181)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def test_execute_both_endpoints_empty_raises(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
"""
|
||||
When both the responses endpoint and the chat.completions fallback return
|
||||
empty text, a ValueError must propagate to the caller.
|
||||
This covers line 181 ('raise ValueError("chat.completions fallback returned no text output")').
|
||||
"""
|
||||
image_node = _image_module()
|
||||
|
||||
empty_completion = SimpleNamespace(
|
||||
choices=[SimpleNamespace(message=SimpleNamespace(content=""))]
|
||||
)
|
||||
fake_client = SimpleNamespace(
|
||||
responses=SimpleNamespace(create=lambda **_: SimpleNamespace(output_text="")),
|
||||
chat=SimpleNamespace(completions=SimpleNamespace(create=lambda **_: empty_completion)),
|
||||
)
|
||||
monkeypatch.setattr(image_node, "resolve_request_seed", lambda _: 3)
|
||||
monkeypatch.setattr(image_node, "create_openai_client", lambda **_: fake_client)
|
||||
|
||||
with pytest.raises(ValueError, match="chat.completions fallback returned no text output"):
|
||||
image_node.LMStudioImageToText.execute(
|
||||
connection=_connection_payload(),
|
||||
image=_single_image(),
|
||||
system_prompt="sys",
|
||||
user_prompt="describe",
|
||||
seed=0,
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# image_to_text_node.py – execute: chat fallback single image, no dropped-frames note (line 183->189)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def test_execute_chat_fallback_single_image_no_dropped_frames_note(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
"""
|
||||
When the responses endpoint fails and the image is a *single* frame (not a
|
||||
batch), the fallback status message must NOT mention dropped frames.
|
||||
This covers the branch at line 183->189 where batch_size == 1.
|
||||
"""
|
||||
image_node = _image_module()
|
||||
|
||||
def raise_responses(**kwargs):
|
||||
raise RuntimeError("responses unavailable")
|
||||
|
||||
completion = SimpleNamespace(
|
||||
choices=[SimpleNamespace(message=SimpleNamespace(content="single fallback"))]
|
||||
)
|
||||
fake_client = SimpleNamespace(
|
||||
responses=SimpleNamespace(create=raise_responses),
|
||||
chat=SimpleNamespace(completions=SimpleNamespace(create=lambda **_: completion)),
|
||||
)
|
||||
monkeypatch.setattr(image_node, "resolve_request_seed", lambda _: 7)
|
||||
monkeypatch.setattr(image_node, "create_openai_client", lambda **_: fake_client)
|
||||
|
||||
output = image_node.LMStudioImageToText.execute(
|
||||
connection=_connection_payload(),
|
||||
image=_single_image(),
|
||||
system_prompt="sys",
|
||||
user_prompt="describe",
|
||||
seed=0,
|
||||
)
|
||||
|
||||
assert output[0] == "single fallback"
|
||||
assert "via chat.completions" in output.ui.text
|
||||
assert "dropped" not in output.ui.text
|
||||
Reference in New Issue
Block a user