Close gaps that full line+branch coverage does not catch:
- comfy_image_to_base64_png_url: assert RGBA (4-channel) tensor round-trips
correctly through PNG encoding (pixel values preserved).
- comfy_image_to_base64_png_url: assert non-square image dimensions (H x W)
are preserved in the encoded PNG — catches H/W transposition mutations.
- comfy_image_to_base64_png_url: assert batch[0] (red) is encoded, not
batch[1] (blue) — catches wrong-frame-index mutations.
- image_to_text_node._responses_kwargs: assert every image content entry uses
the key 'image_url' (not 'url') and contains a valid data:image/png;base64,
URL — for both single and batch paths.
- image_to_text_node._responses_kwargs: assert batch frames produce distinct
data URLs — catches mutations that pass the same index for all frames.
- image_to_text_node.execute: assert the status message contains the actual
model name for both the responses and chat.completions fallback paths.
- text_gen_node.execute: assert the status message contains the actual model
name for both paths — a hardcoded name mutation goes undetected otherwise.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
25 new tests across client.py, routes.py, connect_node.py, and text_gen_node.py.
Each test targets a specific behavioral contract that 100% line+branch coverage
did not verify and that a realistic mutation would silently break:
- normalize_server_url: whitespace-only input, trailing-slash stripping,
https acceptance
- coerce_seed: whitespace-only string returns None (not ValueError)
- resolve_request_seed: seed=0 passes through; large values not clipped
- build_responses_input_text: 'type' must be 'input_text' (not 'text')
- build_chat_messages: whitespace system prompt omitted; empty user included
- list_models: result is sorted AND deduplicated (both invariants checked)
- strip_think_content: surrounding text order preserved; block-only → ""
- _query_int: negative integers rejected; empty string uses default
- _models_handler: empty models → default_model="" not IndexError;
error prefix is 'Failed to list models' not 'Connection test failed'
- _test_handler: model_count matches actual list length; distinct error prefix
- connect_node execute: empty model with test_connectivity=False still probes;
no-probe status omits 'Found N model(s)'; probe status includes exact count
- _responses_kwargs: 'input' key present with user-role and correct prompt text;
temperature/max_tokens forwarded from payload
- text_gen_node execute: fallback path status includes resolved seed;
user_prompt=None raises clean ValueError
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
When a reasoning model returns a response consisting solely of
<think>...</think> content, the emptiness guard fired before
strip_think_content ran, so the node silently returned an empty
string rather than signalling an error. Add a post-strip check
in both LMStudioTextGen and LMStudioImageToText that raises
ValueError if strip_think_content reduces the text to nothing.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
`normalize_server_url` used a case-sensitive `endswith("/v1")` check, so
user-supplied URLs like `https://host/V1` passed through intact. The
downstream `to_openai_base_url` then appended another `/v1`, producing the
double-suffix broken URL `https://host/V1/v1`, causing every API call to
fail with a 404.
Fix: compare against `value.lower()` so any casing of the suffix is stripped.
Adds a targeted regression test covering lowercase, uppercase, and mixed-case
inputs as well as the end-to-end `to_openai_base_url` output.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Add four targeted tests hitting the previously uncovered branches in
`client.extract_responses_text` (lines 221–233): dict items with a
non-list content field, content list entries that are not dicts, dict
entries whose text value is not a str, and object items where
`getattr(entry, "text")` is non-str.
Also declare `aiohttp` in the optional [test] extra so the full suite
can import `routes.py` without a ModuleNotFoundError.
Branch coverage: 99% (4 branches missed) → 100% (146/146 branches).
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
_batch_image() was defined in tests/test_coverage_gaps.py but never called
in that file — an identical helper already exists in test_image_to_text_node.py
where it is actively used. Dead code with no callers; safe to remove.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
- Delete client.dump_openai_response (and its import json): never called
from any production path (connect_node, text_gen_node, image_to_text_node,
routes, __init__); existed only as a debugging aid.
- Delete its test (test_dump_openai_response_branches in test_client_full.py)
and the now-orphaned `import json` in that file.
- Delete make_response_with_output_text / make_response_with_output_items
from tests/helpers/fakes.py: defined in the initial test scaffolding
commit but never imported or referenced by any test module.
All 68 tests pass; branch coverage holds at 96.93 % (> 90 % required).
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
In `extract_responses_text`, the loop over non-dict output items extracted
text from `item.content[].text` AND unconditionally also from `item.text`.
When an item carried both attributes (e.g. an OpenAI Responses API message
object that exposes `.content` as a list and also a convenience `.text`
shorthand), the same content was appended twice, producing a response like
`"the-answer\nthe-answer"` instead of `"the-answer"`.
The dict-item branch already used `continue` to prevent this; mirror that
guard in the object branch by making `item.text` extraction an `else` clause
that only runs when `item.content` is NOT a list.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
When the responses endpoint fails and execution falls back to
chat.completions, only the first image in a batch can be forwarded
(chat.completions accepts a single image_url per message in this
implementation). Previously extra frames were silently dropped.
Now we record batch_size before entering the fallback and append a plain-
language warning to fallback_reason when batch_size > 1, so the status
preview shown in ComfyUI makes the degradation explicit.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
connect_node.py
- Fix UI label typo: "Enable Reasonning" → "Enable Reasoning"
- Use normalize_api_key() instead of an inline (api_token or "-").strip()
expression; the helper also checks DEFAULT_API_KEY_PLACEHOLDER so both
code paths now agree on the canonical fallback value
client.py
- Remove build_responses_input_with_image(): dead code; image_to_text_node
builds its own multi-image payload directly in _responses_kwargs() and
this function was never imported or called
routes.py
- _test_handler default timeout: 600 s → 15 s, consistent with
_models_handler. Both handlers probe connectivity identically; a 10-min
default on a "Test Connection" button is a foot-gun when called without
a timeout_seconds query param.
- Drop now-unused DEFAULT_TIMEOUT_SECONDS import
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- coerce_seed("") returns None, not ValueError; move "" out of the
invalid-values parametrize and give it its own explicit assertion
- strip_think_content tests used \\n (literal backslash-n) instead of
real newlines; .strip() cannot remove backslash or 'n' so the dangling-
tag test always failed. Replace with actual \n to match real LLM output.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>